SaaS pricing page change tracker avatar

SaaS pricing page change tracker

Pricing

$3.00 / 1,000 pricing page trackeds

Go to Apify Store
SaaS pricing page change tracker

SaaS pricing page change tracker

Snapshots public SaaS pricing pages and reports price, tier and feature changes since the last run. Public pages only; robots.txt respected; ≤1 request/second per host.

Pricing

$3.00 / 1,000 pricing page trackeds

Rating

0.0

(0)

Developer

Quietfetch

Quietfetch

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

SaaS pricing page change tracker (Stream A · Actor 2)

Spec (10 lines)

  1. What: give it a list of public SaaS pricing pages; every run stores a structured snapshot (plans, prices, periods, per-seat/billing hints, feature bullets) and reports exactly what changed since the last run.
  2. Buyer: teams and analysts who pay for or track many tools and want to know the week a price, tier or limit moves — without re-reading twenty pricing pages.
  3. Input: pricingPages (required), renderJs (default on), onlyChanged, snapshotStoreName, maxFeaturesPerPlan, requestDelaySecs.actor/input_schema.json.
  4. Output: one row per page: vendor, url, pageTitle, fetchedAt, firstSeen, previousFetchedAt, changed, diff{addedPlans, removedPlans, priceChanges[], periodChanges[], featureChanges[], mentionsChanged}, plans[], priceMentions[], hash.
  5. Pricing: one PPE event page-tracked at $0.003 ($3 / 1,000 pages), charged after the row is saved; with onlyChanged on, unchanged pages cost nothing.
  6. How it parses: structure, not vendor rules — repeated sibling "cards" with a heading and a price, header-labelled comparison tables, Free/Custom marker tiers, hidden monthly/annual toggles kept as altPrices; every numeric price on the page is also listed in priceMentions, so a change is caught even when a layout defeats the plan parser.
  7. History: previous snapshots live in a named key-value store in the buyer's account (snapshotStoreName), keyed by URL hash; the actor never sees another buyer's data.
  8. Legal floor (from the template): public logged-out pages, robots.txt honoured, ≤1 request/second per host, no cookies, no proxy/fingerprint rotation, no CAPTCHA handling.
  9. Tests (28, no network): price parsing, period/billing detection, three golden fixtures (cards+toggle, table, prose), diff logic, snapshot truncation, a smoke test running the real crawler three times through a local server, an opt-in Playwright run, brand consistency against ops/brands.json, and site-owner opt-outs.
  10. Non-goals: logged-in pricing, per-customer quotes, currency conversion, judging whether a change is "good" — it reports; interpretation is the reader's.

Input example

{
"pricingPages": [
{ "url": "https://vendor-a.example/pricing" },
{ "url": "https://vendor-b.example/plans" }
],
"renderJs": true,
"onlyChanged": false,
"snapshotStoreName": "ai-tools-watchlist",
"maxFeaturesPerPlan": 12,
"requestDelaySecs": 1
}

Output example (one dataset row, second run, price moved)

{
"url": "https://vendor-a.example/pricing",
"vendor": "vendor-a.example",
"pageTitle": "Vendor A — Pricing",
"fetchedAt": "2026-09-12T06:00:04.120Z",
"firstSeen": false,
"previousFetchedAt": "2026-09-11T06:00:03.900Z",
"changed": true,
"diff": {
"changed": true,
"addedPlans": [],
"removedPlans": [],
"priceChanges": [{ "plan": "Plus", "from": "$10", "to": "$11", "fromAmount": 10, "toAmount": 11, "currency": "USD", "deltaPct": 10 }],
"periodChanges": [],
"featureChanges": [],
"mentionsChanged": true
},
"plans": [
{ "name": "Starter", "price": { "kind": "free", "amount": 0, "currency": null, "raw": "Free" }, "period": null, "perUnit": null, "billing": null, "altPrices": [], "features": ["Up to 3 notebooks", "Basic search"] },
{ "name": "Plus", "price": { "kind": "numeric", "amount": 11, "currency": "USD", "raw": "$11" }, "period": "month", "perUnit": "user", "billing": "annual", "altPrices": [{ "amount": 12, "currency": "USD", "raw": "$12" }], "features": ["Unlimited notebooks", "Advanced search & filters"] },
{ "name": "Enterprise", "price": { "kind": "custom", "amount": null, "currency": null, "raw": "Custom" }, "period": null, "perUnit": null, "billing": null, "altPrices": [], "features": ["Dedicated success manager"] }
],
"priceMentions": ["$12", "$11", "$5", "$0.50"],
"hash": "…sha256…",
"scrapedAt": "2026-09-12T06:00:04.120Z"
}

price.kind is numeric, free (amount 0 or the word Free) or custom (Custom / Contact sales). period is month, year, one-time or null; billing says how a displayed per-month price is actually billed (annual / monthly). RUN_SUMMARY in the default key-value store holds { saved (pages processed), charged, pagesOpened, pagesFailed, stopReason }.

Pricing (pay-per-event)

EventWhen chargedPrice
page-trackedafter a page's row is written to the dataset$0.003 ($3 / 1,000 pages)

A weekly watch-list of 60 tools costs about $0.18 per run — or less with onlyChanged, which charges only for pages whose pricing actually moved (and first sightings). PPE events are defined in Apify Console from .actor/pay_per_event.json.

Lawful and responsible use

  • Fetches public pricing pages exactly as a logged-out visitor sees them; it never logs in, never keeps cookies, and never works around a block, CAPTCHA or rate limit — a blocked page shows up as a failed request, not as a workaround.
  • robots.txt is fetched for every host and obeyed (QuietfetchBot, then *). Pages a site disallows are skipped and reported in the log.
  • Rate: at most one request per second per host, two per second overall, one page per vendor per run.
  • Data: published list prices and plan descriptions only — factual, non-personal information. No personal data is collected. Buyers are responsible for their own use of the output and for the target sites' terms.
  • Identification: the user agent names the bot and a contact address so site owners can reach the operator. Bot token QuietfetchBot; contact page https://quietfetch.com/bots, mailbox hello@quietfetch.com (from ops/brands.json → streams.A). Site owners who want the bot to stop can block QuietfetchBot in robots.txt (honoured on the next request) or email the address — the Builder adds the domain to src/optout.json in every actor, after which that host and its subdomains are never fetched, whatever a buyer's input says (test/optout.test.js).

Local development

npm install
npm test # 20 tests, no network
FOUNDRY_TEST_BROWSER=1 npm test # + the Playwright path (21)
npm run start:ppe # local run with a simulated PPE charging log

Local input: storage/key_value_stores/default/INPUT.json. Snapshots land in storage/key_value_stores/<snapshotStoreName>/.

Maintenance notes for the Sentinel

  • The nightly health run fetches one page (health.json) and expects ≥1 row with plans.length ≥ 1. A row with plans: [] but non-empty priceMentions means the layout changed and the card/table heuristics need a look — open a P1, not a P0, unless priceMentions is empty too.
  • To add a layout case: save the page HTML into test/fixtures/, write the golden JSON, and extend src/extract.js. Never special-case a vendor by name.

Built and maintained with AI assistance under human review.