SaaS Pricing & Plan Change Monitor
Pricing
from $30.00 / 1,000 page state checkeds
SaaS Pricing & Plan Change Monitor
Watch exact SaaS pricing URLs and report confirmed changes to plans, prices, limits, and features. Monthly and annual prices stay separate.
Pricing
from $30.00 / 1,000 page state checkeds
Rating
0.0
(0)
Developer
Olalekan Johnson
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
8 hours ago
Last modified
Categories
Share
Product marketing and competitive-intelligence teams use this Actor to watch a short list of public pricing pages. It returns a typed plan matrix — name, price, currency, billing period, per-seat or per-account, limits, and add-ons — and reports a change only after a second agreeing capture. A failed read or a suspected redesign is recorded on its own. It is not reported as a plan being removed.
Unofficial, and not affiliated with Basecamp (37signals), SavvyCal, or Notion. The default input only reads those companies' public pricing pages. Short quotations are kept as evidence. The page itself is not republished.
Contact: plusonecares@gmail.com
Who it is for
- A product marketer tracking a handful of competitors' published plans.
- An analyst who needs monthly and annual prices kept apart, including an annual total and its monthly equivalent, each labelled as published or derived.
What you provide
Exact pricing-page URLs. The default input is three public pages that parse from static HTML or embedded page data, without a browser:
https://basecamp.com/pricinghttps://savvycal.com/pricinghttps://www.notion.com/pricing
Leave the proxy off for those pages. Set Confirming captures to 2 (the default) so a one-off parse glitch cannot replace the baseline. Use 1 only when you accept a single read as final.
The baseline lives in a named key-value store (saas-pricing-state unless you rename it), keyed by monitor id and URL. Schedule the Actor on that same monitor id. An errored or blocked run does not overwrite a good baseline.
What you get
The default dataset mixes three flat record types. Filter on recordType.
| recordType | When |
|---|---|
plan_state | One plan at one billing period. Monthly and annual are different rows. |
change | A confirmed change: price up or down, plan added or removed, limit changed, feature moved, unit or currency changed. |
page_status | The page-level result, including blocked, extraction_failure, and redesign_suspected. |
priceDisplay says whether a figure is the published monthly price, the published annual total, the published monthly equivalent, or a derived figure. An annual row's priceAmount is the published annual total only. It is null when the page publishes a per-month equivalent and the annual total was calculated from that. A monthly row is never compared with an annual row.
The run key-value store also contains:
OUTPUT— counts, coverage, and charged unitsWEEKLY_DIGEST.mdandWEEKLY_DIGEST.html— included with the run, not a separate chargeEVIDENCE_REPORT.html— only when Write evidence report is on, at least one page was chargeable, and theevidence-reportcharge succeeds
Example plan row
{"recordType": "plan_state","monitorId": "default","sourceUrl": "https://savvycal.com/pricing","observedAt": "2026-10-23T04:00:00Z","coverage": "complete","confidence": 0.98,"planName": "Premium","billingPeriod": "annual","priceAmount": 200,"priceCurrency": "USD","annualTotal": 200,"annualTotalLabel": "published","monthlyEquivalent": 17,"monthlyEquivalentLabel": "published","pricingUnit": "per_seat","priceDisplay": "USD 200.00 per year (published annual total); USD 17.00 per month equivalent (published monthly equivalent), per_seat","status": "baseline_established"}
The first successful read is baseline_established, not a change. The next differing read is pending_confirmation and does not emit change rows. The following agreeing read emits change rows and replaces the baseline.
Example change row
{"recordType": "change","monitorId": "default","sourceUrl": "https://savvycal.com/pricing","observedAt": "2026-11-06T04:00:00Z","coverage": "complete","confidence": 0.98,"changeType": "price_up","severity": "major","planName": "Basic","billingPeriod": "monthly","field": "priceAmount","previousValue": "12.0","newValue": "14.0","confirmationCount": 2,"changeId": "stable-id-for-this-diff"}
Turn on Alert only on change to skip unchanged plan rows. Confirmed changes and page problems are still saved. A successful check is still a page-state-checked event, because the check itself is the delivered unit. The digest and OUTPUT record it.
Pricing
This Actor uses pay-per-event pricing. You pay only for these events:
| Event | When it is charged | Price |
|---|---|---|
| Actor start | Once per run, per GB of memory. | $0.005 per GB |
page-state-checked | A pricing URL was read and the matrix was chargeable (complete, or partial with confidence at least 0.65 and a comparable price). | $0.03 |
evidence-report | Optional. One consolidated HTML report for the run, only after at least one chargeable page. | $0.25 |
Blocked, empty, unsupported, rejected, and suspected-redesign reads are not charged. The same page is not charged twice inside one run, including a platform retry. There is no separate per-result charge.
The default run uses 512 MB. A browser is used only to click a monthly/annual control when that period is missing from the static page, and only when the run has 1024 MB or more. The demo pages do not need it. Extra memory raises the start event (it is priced per GB) and compute time. It does not change the event prices above.
Worked examples, before taxes and before any max-charge cap you set on the run:
- Demo, once. 3 pages, no evidence report, 512 MB. 3 × $0.03 = $0.09, plus the start event at $0.005 per GB (0.5 GB is $0.0025).
- Weekly watch, 25 pages, four weeks. No evidence report. 100 × $0.03 = $3.00, plus four start events.
- Twice a week so a change can be confirmed, plus a weekly evidence report. 25 pages × 8 runs = 200 × $0.03 = $6.00, plus 4 × $0.25 = $1.00. $7.00 plus start events. A real price change is not alerted until the second agreeing check.
If the run's max total charge is reached, later URLs are skipped, the evidence report is not written, and baselines already saved stay as they are.
Scope and limits
- The parser reads static HTML and embedded JSON, including Next.js and Nuxt payloads. It is a generic extractor, not a hand-written adapter for every vendor.
- Billing toggles are the only browser action. Currency switchers and seat sliders are not clicked.
- A page that disallows the fetch in
robots.txt, fails, or comes back as a challenge isblockedand is not billed. - If a previously good page comes back with a weak or overlapping-poor matrix, the status is
redesign_suspectedorextraction_failure. Existing plans are not marked removed. - Private IPs, localhost,
file://, credentialed URLs, and non-80/443 ports are rejected. - The Actor does not collect personal contact details. It does not train a model on the pages it fetches.
- Prices on the live demo pages can change. The dataset from a fresh run is the source of truth, not the example JSON above.
Integrations
- Webhook. On the Actor task, add an Apify webhook for run succeeded. Read the default dataset and keep items whose
recordTypeischange.changeIdis stable for the same plan, period, field, and values. - Make or n8n. Use the Apify module "Get dataset items", or HTTP GET
{datasetApi}/items?format=json. MapsourceUrl,planName,billingPeriod,changeType,previousValue, andnewValue. - Sheets. Download the dataset as CSV. The price, limit, feature, and add-on columns are flat text.
Point the schedule at one task per watchlist so the named state store keeps accumulating baselines.
Run locally
Python 3.12.
pip install -r requirements.txtpython -m src
An empty input uses the three demo URLs. Local runs have no pay-per-event price configured, so charges are recorded in the run summary and are not billed. The first local run creates ./storage.
$pytest
FAQ
Why did a price I can see not alert? The first difference is pending. With the default of two confirming captures, the next agreeing run emits the change. Check page_status for pending_confirmation and the digest section "Waiting for confirmation".
Why was a plan not marked removed? The new read was empty, low-confidence, or a poor overlap with the saved plan names. That is a redesign or extraction failure. The previous baseline is still there.
Why are there two prices for one plan? Monthly and annual are separate rows. The annual row shows the annual total and the monthly equivalent, and each number is labelled published or derived. They are not compared with the monthly plan's price.
The page says Custom, but a number was saved. Some pages hide a catalog amount in embedded data while the card says custom. The row note says the visible card says custom and the amount is the catalog list price.
Do I need a proxy or a browser? Not for the default pages. Raise memory to 1024 MB or more only if a target hides a requested period behind a toggle.