SaaS pricing page change tracker
Pricing
$3.00 / 1,000 pricing page trackeds
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
Maintained by CommunityActor 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)
- 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.
- 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.
- Input:
pricingPages(required),renderJs(default on),onlyChanged,snapshotStoreName,maxFeaturesPerPlan,requestDelaySecs—.actor/input_schema.json. - Output: one row per page:
vendor, url, pageTitle, fetchedAt, firstSeen, previousFetchedAt, changed, diff{addedPlans, removedPlans, priceChanges[], periodChanges[], featureChanges[], mentionsChanged}, plans[], priceMentions[], hash. - Pricing: one PPE event
page-trackedat $0.003 ($3 / 1,000 pages), charged after the row is saved; withonlyChangedon, unchanged pages cost nothing. - 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 inpriceMentions, so a change is caught even when a layout defeats the plan parser. - 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. - 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.
- 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. - 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)
| Event | When charged | Price |
|---|---|---|
page-tracked | after 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 (fromops/brands.json→ streams.A). Site owners who want the bot to stop can blockQuietfetchBotin robots.txt (honoured on the next request) or email the address — the Builder adds the domain tosrc/optout.jsonin 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 installnpm test # 20 tests, no networkFOUNDRY_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 withplans.length ≥ 1. A row withplans: []but non-emptypriceMentionsmeans the layout changed and the card/table heuristics need a look — open a P1, not a P0, unlesspriceMentionsis empty too. - To add a layout case: save the page HTML into
test/fixtures/, write the golden JSON, and extendsrc/extract.js. Never special-case a vendor by name.
Built and maintained with AI assistance under human review.