SFOLN

Catalogue / Web toolkit for agents

Website Change Monitor – Text Diff Alerts

sfoln/website-change-monitor · Developer tools, Automation, SEO tools On request

Monitor web pages for changes: each run compares a page's text (or a CSS-selected part) with the last check and returns first seen, unchanged or changed with a line diff, similarity and added/removed lines. Ignore noisy text, add AI change summaries. $1 per 1,000 checks.

About

What it does

Monitor web pages for changes and get a clear answer per URL on every run: first_seen, unchanged or changed — with a unified text diff, a similarity score, added and removed lines, and optionally an AI summary of what changed. Watch a whole page's main text or just the part you care about with a CSS selector (prices, job lists, terms, release notes), and ignore noise such as timestamps and counters. Built for competitor and pricing intelligence, compliance and policy tracking, procurement and job-board watching, SEO, and AI agents that need to know "has this page changed since yesterday?".

Price

Price per delivered record

EventPricePer 1,000Charged for
Page check
page-check
$0.001$1 / 1,000Charged once per URL checked: fetched, compared with its last snapshot and reported as first seen, unchanged or changed with a diff.
AI change summary
change-summary
paid plans only
$0.003$3 / 1,000Charged once per changed page for which the AI summary of the change (summary and significance) was generated. Paid plans only.

Apify free plan: up to 10 items per run. Paid plans have no per-run cap; set maxTotalChargeUsd to cap spend.

Run

Call it from code, an agent or the Console

This API is private on Apify for now. Email us and we will publish it for you, usually within two working days. The calls below work once it is public. contact@sfoln.com

curl · synchronous run, rows in the response
curl -X POST "https://api.apify.com/v2/actors/sfoln~website-change-monitor/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://example.com/"]}'
mcp.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/website-change-monitor"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~website-change-monitor/runs?maxTotalChargeUsd=1
GET  https://api.apify.com/v2/actor-runs/{runId}            # until status is SUCCEEDED
GET  https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json

Authenticate with an Apify API token. MCP clients can instead sign in with OAuth when prompted. Example prompt for an agent: Use website-change-monitor to check https://example.com/ for changes since the last check and tell me what changed.

Input

Input fields

Pages to check. Each run compares every page with its snapshot from the previous run (kept in a named key-value store in your account) and returns one record per URL. Schedule the Actor to get change alerts.

FieldTypeDefaultMeaning
urls *arrayrequiredURLs
Pages to monitor, one per line (public http(s) URLs). The first check of a URL saves its baseline. Accounts on the Apify free plan can process up to 10 URLs per run.
selectorstring—CSS selector
Watch only the elements matching this CSS selector (e.g. #pricing, .job-list li, table.prices tr). Empty: the page's main text (navigation, footer and scripts left out). Each selector keeps its own snapshot of a page.
ignorePatternsarray—Ignore patterns
Regular expressions (case-insensitive), one per line: matching text is removed from each line before comparing, e.g. Last updated: .* or \d+ views. Changing them later applies to the stored snapshot too. Patterns that backtrack catastrophically, such as (a+)+ or (\w|\d)+, are refused.
ignoreNumbersbooleanfalseIgnore numbers
Treat all numbers as equal (counters, timestamps, prices), so only changes in wording count.
summarizeChangesbooleanfalseAI change summary (paid add-on)
For changed pages, add a one-paragraph AI summary of what changed and its significance (minor, moderate, major). Charged per summary (change-summary); paid Apify plans only.
diffContextLinesinteger2Diff context lines
Unchanged lines shown around each change in the unified diff. Range 0–10.
maxDiffCharsinteger5000Max diff length
Characters of unified diff per page; a longer diff is cut (diffTruncated: true). Range 500–100000.
excerptCharsinteger1000Text excerpt length
Characters of the page's current text in textExcerpt (0 = none). Range 0–20000.
includeTextbooleanfalseInclude full text
Add the full normalized page text (up to 200,000 characters) to each record.
storeNamestring—Snapshot store name
Named key-value store for the snapshots (letters, digits, hyphens). Default: website-change-monitor-memory. Use different names to keep separate monitors of the same pages apart; delete a record (or the store) to reset a baseline.
maxUrlsinteger500Max URLs
Safety cap on URLs in this run; the rest are ignored and counted as inputItemsDropped in the OUTPUT record. Range 1–5000.
concurrencyinteger4Concurrency
Pages checked in parallel. Range 1–10.
Example input
{
  "urls": [
    "https://example.com/"
  ]
}

Output

One record per input item

Records land in the run's dataset; a synchronous call returns them directly. Each has a status (ok is charged; everything else is free) and a charged flag. The run's OUTPUT record totals items and charges.

FieldTypeMeaning
inputstring | objectInput
The URL this record corresponds to.
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan | skipped_unmetered e.g. "ok"
chargedbooleanCharged
Whether this record incurred a pay-per-event charge.
chargedEventstringCharged event
Pay-per-event event charged for this record (present only when charged).
chargedCountintegerEvents charged
Events actually charged when the charge came back short of the requested count (present only then; a warning explains it).
sourcestringSource
Upstream data source.
fetchedAtstringFetched at
ISO timestamp.
errorobjectError
{ code, message, provider?, status? } when status is not ok. Codes set by the runtime: budget_exhausted, free_plan_limit, unmetered, aborted, charge_failed, record_too_large, invalid_record, dataset_push_failed, memory_unavailable; fetch codes: http_<status>, invalid_url, blocked_url, dns_not_found, network, tls_error, timeout, too_many_redirects, unexpected_type, too_large; this Actor: selector_not_found, no_text, pattern_too_slow.
warningsarrayWarnings
Notes, e.g. a page cut at 5 MB or a skipped AI summary.
urlstringURL
The monitored URL.
finalUrlstringFinal URL
The URL that answered, after redirects.
httpStatusintegerHTTP status
Status of the final response.
titlestring | nullPage title
The page's <title>.
contentTypestring | nullContent type
Declared media type.
selectorstring | nullSelector
The CSS selector watched (null: the page's main text).
selectorMatchesinteger | nullSelector matches
Elements the selector matched.
changeStatusstringChange status
first_seen (baseline saved) | unchanged | changed | error (the page could not be checked; not charged).
changedbooleanChanged
True when the page changed since the previous check.
similaritynumber | nullSimilarity
0–1 share of unchanged lines (1 = identical); null on the first check.
addedLinesintegerAdded lines
Lines added since the previous check.
removedLinesintegerRemoved lines
Lines removed since the previous check.
diffstring | nullDiff
Unified diff of the page text (- removed, + added), at most maxDiffChars.
diffTruncatedbooleanDiff truncated
The diff was cut at maxDiffChars.
addedSamplearrayAdded lines (sample)
Up to 10 added lines.
removedSamplearrayRemoved lines (sample)
Up to 10 removed lines.
textExcerptstring | nullText excerpt
The start of the page's current text (excerptChars).
textLengthintegerText length
Characters of normalized text compared.
lineCountintegerLines
Lines of normalized text.
textTruncatedbooleanText truncated
The page or its text was cut (5 MB / 200,000 characters).
contentHashstringContent hash
SHA-256 of the compared text (after ignore patterns).
previousHashstring | nullPrevious hash
SHA-256 of the previous text, compared the same way.
previousCheckedAtstring | nullPrevious check
When the snapshot compared with was taken.
firstSeenAtstringFirst seen
When the page was first checked.
lastChangedAtstring | nullLast changed
The last check that found a change.
checkedAtstringChecked at
This check.
summarystring | nullAI summary
What changed, in a few sentences (summarizeChanges, paid add-on).
significancestring | nullSignificance
minor | moderate | major (AI summary).
summaryChargedbooleanSummary charged
The change-summary add-on was charged for this page.
textstringText
The full normalized text (includeText).
Example record
{
  "input": "https://shop.example.com/pricing",
  "url": "https://shop.example.com/pricing",
  "finalUrl": "https://shop.example.com/pricing",
  "httpStatus": 200,
  "title": "Pricing",
  "contentType": "text/html",
  "selector": null,
  "selectorMatches": null,
  "changeStatus": "changed",
  "changed": true,
  "similarity": 0.75,
  "addedLines": 1,
  "removedLines": 1,
  "diff": "--- previous\t2026-10-13T12:00:00.000Z\n+++ current\t2026-10-14T12:00:00.000Z\n@@ -1,4 +1,4 @@\n Pricing\n Simple plans for every team, billed monthly or yearly.\n-Starter | $10\n+Starter | $12\n Pro | $49",
  "diffTruncated": false,
  "addedSample": [
    "Starter | $12"
  ],
  "removedSample": [
    "Starter | $10"
  ],
  "textExcerpt": "Pricing\nSimple plans for every team, billed monthly or yearly.\nStarter | $12\nPro | $49",
  "textLength": 86,
  "lineCount": 4,
  "textTruncated": false,
  "contentHash": "4f1c…",
  "previousHash": "9a0b…",
  "previousCheckedAt": "2026-10-13T12:00:00.000Z",
  "firstSeenAt": "2026-10-01T12:00:00.000Z",
  "lastChangedAt": "2026-10-14T12:00:00.000Z",
  "checkedAt": "2026-10-14T12:00:00.000Z",
  "summary": "The Starter plan price rose from $10 to $12 per month; the Pro plan is unchanged.",
  "significance": "moderate",
  "summaryCharged": true,
  "status": "ok",
  "charged": true,
  "chargedEvent": "page-check",
  "source": "website",
  "fetchedAt": "2026-10-14T12:00:00.000Z"
}

FAQ

Questions

Where are the snapshots stored?
In a named key-value store in your own Apify account (website-change-monitor-memory unless you set storeName), one record per URL and selector. Named stores are kept between runs; delete a record or the whole store to reset the baselines.
How do I get alerts?
Schedule the Actor (hourly, daily…) and add a webhook or a Make/Zapier/n8n integration that filters the dataset on changed = true.
Does it render JavaScript?
No. It compares the HTML the server sends. Pages that build their content in the browser return no text (status error, not charged); point the Actor at the page's JSON API or a server-rendered URL instead.
How do I stop false alarms from timestamps and counters?
Add ignorePatterns such as Last updated: .* or \d+ views, or turn on ignoreNumbers. Patterns are applied to both the stored snapshot and the new text, so you can tune them at any time.

Machine-readable

This page for agents