DEV Community

agenthustler
agenthustler

Posted on

How to Track SaaS Pricing Changes Across 100+ Tools

SaaS pricing changes constantly. If you manage software procurement or build comparison tools, tracking these changes automatically saves significant time and money.

The Opportunity

SaaS pricing pages are public but unstructured. By scraping them regularly, you can detect price increases before renewals, build competitive intelligence, and create comparison tools.

Core Setup

import requests
from bs4 import BeautifulSoup
import json
import hashlib
from datetime import datetime
from pathlib import Path
import re
import time
import random

SCRAPER_KEY = "YOUR_SCRAPERAPI_KEY"

def fetch_page(url):
    resp = requests.get(
        "http://api.scraperapi.com",
        params={"api_key": SCRAPER_KEY, "url": url, "render": "true"},
        timeout=30
    )
    return BeautifulSoup(resp.text, "html.parser")
Enter fullscreen mode Exit fullscreen mode

Scraping Pricing Pages

SAAS_WATCHLIST = [
    {"name": "Slack", "url": "https://slack.com/pricing", "category": "communication"},
    {"name": "Notion", "url": "https://www.notion.so/pricing", "category": "productivity"},
    {"name": "Linear", "url": "https://linear.app/pricing", "category": "project_mgmt"},
    {"name": "Vercel", "url": "https://vercel.com/pricing", "category": "hosting"},
]

def scrape_pricing_page(saas_entry):
    soup = fetch_page(saas_entry["url"])

    pricing_elements = soup.find_all(
        ["div", "section"],
        class_=lambda c: c and any(kw in str(c).lower() for kw in ["pricing", "plan", "tier"])
    )

    plans = []
    for el in pricing_elements:
        plan_name = el.find(["h2", "h3", "h4"])
        price_el = el.find(string=lambda t: t and "$" in t if t else False)

        if plan_name and price_el:
            price = extract_price(price_el)
            features = [li.text.strip() for li in el.find_all("li")[:10]]
            plans.append({
                "plan_name": plan_name.text.strip(),
                "price": price,
                "features": features
            })

    return {
        "saas": saas_entry["name"],
        "plans": plans,
        "scraped_at": datetime.now().isoformat(),
        "page_hash": hashlib.md5(soup.text.encode()).hexdigest()
    }

def extract_price(text):
    match = re.search(r"\$([\d,]+\.?\d*)", str(text))
    return float(match.group(1).replace(",", "")) if match else None
Enter fullscreen mode Exit fullscreen mode

Change Detection

class PricingTracker:
    def __init__(self, storage_dir):
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(exist_ok=True)

    def _history_path(self, saas_name):
        return self.storage_dir / f"{saas_name.lower().replace(' ', '_')}.json"

    def save_snapshot(self, pricing_data):
        path = self._history_path(pricing_data["saas"])
        history = json.loads(path.read_text()) if path.exists() else []
        history.append(pricing_data)
        path.write_text(json.dumps(history, indent=2))

    def detect_changes(self, current_data):
        path = self._history_path(current_data["saas"])
        if not path.exists():
            return {"type": "NEW", "saas": current_data["saas"]}

        previous = json.loads(path.read_text())[-1]
        if previous["page_hash"] == current_data["page_hash"]:
            return None

        changes = []
        prev_plans = {p["plan_name"]: p for p in previous.get("plans", [])}
        curr_plans = {p["plan_name"]: p for p in current_data.get("plans", [])}

        for plan_name, curr_plan in curr_plans.items():
            prev_plan = prev_plans.get(plan_name)
            if not prev_plan:
                changes.append({"plan": plan_name, "change": "NEW_PLAN"})
            elif prev_plan["price"] != curr_plan["price"]:
                pct = round(((curr_plan["price"] - prev_plan["price"]) / prev_plan["price"]) * 100, 1) if prev_plan["price"] else None
                changes.append({
                    "plan": plan_name, "change": "PRICE_CHANGE",
                    "old_price": prev_plan["price"], "new_price": curr_plan["price"],
                    "pct_change": pct
                })

        return {"saas": current_data["saas"], "changes": changes} if changes else None
Enter fullscreen mode Exit fullscreen mode

Running the Scan

def run_pricing_scan(watchlist):
    tracker = PricingTracker("pricing_data")

    for entry in watchlist:
        try:
            data = scrape_pricing_page(entry)
            change = tracker.detect_changes(data)
            tracker.save_snapshot(data)

            if change and change.get("changes"):
                for c in change["changes"]:
                    if c["change"] == "PRICE_CHANGE":
                        print(f"[CHANGE] {change['saas']} - {c['plan']}: ${c['old_price']} -> ${c['new_price']}")

            time.sleep(random.uniform(3, 6))
        except Exception as e:
            print(f"[ERROR] {entry['name']}: {e}")
Enter fullscreen mode Exit fullscreen mode

Proxy Infrastructure

  • ScraperAPI — JavaScript rendering for SPA pricing pages
  • ThorData — Residential proxies for aggressive bot detection
  • ScrapeOps — Monitor success rates across 100+ domains

Conclusion

Automated SaaS pricing tracking turns a tedious manual process into a competitive advantage. Start with your top 10 vendors, validate the scraper, then expand.

Top comments (0)