# Website Change Monitor – Text Diff Alerts (`sfoln/website-change-monitor`)

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.

- Family: Web toolkit for agents
- Categories: Developer tools, Automation, SEO tools
- Availability: on request (private on Apify). Email contact@sfoln.com with subject "Access request: Website Change Monitor – Text Diff Alerts (sfoln/website-change-monitor)" before calling; calls fail until it is public.
- JSON: https://www.sfoln.com/actors/website-change-monitor.json
- MCP: https://mcp.apify.com?tools=sfoln/website-change-monitor
- REST (sync): POST https://api.apify.com/v2/actors/sfoln~website-change-monitor/run-sync-get-dataset-items
- REST (async): POST https://api.apify.com/v2/actors/sfoln~website-change-monitor/runs

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 per delivered record

| Event | Price | Per 1,000 | Charged for |
| --- | --- | --- | --- |
| Page check (`page-check`) | $0.001 | $1 / 1,000 | Charged 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,000 | Charged once per changed page for which the AI summary of the change (summary and significance) was generated. Paid plans only. |

Pay-per-event, USD, billed by Apify; no platform usage charge. Only records with status `ok` are charged. Cap a run with `maxTotalChargeUsd`. Apify free plan: up to 10 per run.

## Run

```bash
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 client configuration:

```json
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/website-change-monitor"
    }
  }
}
```

Example prompt: Use website-change-monitor to check https://example.com/ for changes since the last check and tell me what changed.

## Input

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.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `urls` (required) | array | — | URLs: 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. |
| `selector` | string | — | 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. |
| `ignorePatterns` | array | — | 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. |
| `ignoreNumbers` | boolean | `false` | Ignore numbers: Treat all numbers as equal (counters, timestamps, prices), so only changes in wording count. |
| `summarizeChanges` | boolean | `false` | AI 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. |
| `diffContextLines` | integer | `2` | Diff context lines: Unchanged lines shown around each change in the unified diff. |
| `maxDiffChars` | integer | `5000` | Max diff length: Characters of unified diff per page; a longer diff is cut (diffTruncated: true). |
| `excerptChars` | integer | `1000` | Text excerpt length: Characters of the page's current text in textExcerpt (0 = none). |
| `includeText` | boolean | `false` | Include full text: Add the full normalized page text (up to 200,000 characters) to each record. |
| `storeName` | string | — | 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. |
| `maxUrls` | integer | `500` | Max URLs: Safety cap on URLs in this run; the rest are ignored and counted as inputItemsDropped in the OUTPUT record. |
| `concurrency` | integer | `4` | Concurrency: Pages checked in parallel. |

Example input:

```json
{
  "urls": [
    "https://example.com/"
  ]
}
```

## Output

One record per input item with `status` (`ok` is charged; `not_found`, `error`, `skipped_*` are free) and `charged`. The run stores an `OUTPUT` summary record.

| Field | Type | Meaning |
| --- | --- | --- |
| `input` | string | object | Input: The URL this record corresponds to. |
| `status` | string | Status: ok \| not_found \| error \| skipped_invalid \| skipped_budget \| skipped_free_plan \| skipped_unmetered |
| `charged` | boolean | Charged: Whether this record incurred a pay-per-event charge. |
| `chargedEvent` | string | Charged event: Pay-per-event event charged for this record (present only when charged). |
| `chargedCount` | integer | Events charged: Events actually charged when the charge came back short of the requested count (present only then; a warning explains it). |
| `source` | string | Source: Upstream data source. |
| `fetchedAt` | string | Fetched at: ISO timestamp. |
| `error` | object | Error: { 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. |
| `warnings` | array | Warnings: Notes, e.g. a page cut at 5 MB or a skipped AI summary. |
| `url` | string | URL: The monitored URL. |
| `finalUrl` | string | Final URL: The URL that answered, after redirects. |
| `httpStatus` | integer | HTTP status: Status of the final response. |
| `title` | string | null | Page title: The page's <title>. |
| `contentType` | string | null | Content type: Declared media type. |
| `selector` | string | null | Selector: The CSS selector watched (null: the page's main text). |
| `selectorMatches` | integer | null | Selector matches: Elements the selector matched. |
| `changeStatus` | string | Change status: first_seen (baseline saved) \| unchanged \| changed \| error (the page could not be checked; not charged). |
| `changed` | boolean | Changed: True when the page changed since the previous check. |
| `similarity` | number | null | Similarity: 0–1 share of unchanged lines (1 = identical); null on the first check. |
| `addedLines` | integer | Added lines: Lines added since the previous check. |
| `removedLines` | integer | Removed lines: Lines removed since the previous check. |
| `diff` | string | null | Diff: Unified diff of the page text (- removed, + added), at most maxDiffChars. |
| `diffTruncated` | boolean | Diff truncated: The diff was cut at maxDiffChars. |
| `addedSample` | array | Added lines (sample): Up to 10 added lines. |
| `removedSample` | array | Removed lines (sample): Up to 10 removed lines. |
| `textExcerpt` | string | null | Text excerpt: The start of the page's current text (excerptChars). |
| `textLength` | integer | Text length: Characters of normalized text compared. |
| `lineCount` | integer | Lines: Lines of normalized text. |
| `textTruncated` | boolean | Text truncated: The page or its text was cut (5 MB / 200,000 characters). |
| `contentHash` | string | Content hash: SHA-256 of the compared text (after ignore patterns). |
| `previousHash` | string | null | Previous hash: SHA-256 of the previous text, compared the same way. |
| `previousCheckedAt` | string | null | Previous check: When the snapshot compared with was taken. |
| `firstSeenAt` | string | First seen: When the page was first checked. |
| `lastChangedAt` | string | null | Last changed: The last check that found a change. |
| `checkedAt` | string | Checked at: This check. |
| `summary` | string | null | AI summary: What changed, in a few sentences (summarizeChanges, paid add-on). |
| `significance` | string | null | Significance: minor \| moderate \| major (AI summary). |
| `summaryCharged` | boolean | Summary charged: The change-summary add-on was charged for this page. |
| `text` | string | Text: The full normalized text (includeText). |

Example record:

```json
{
  "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

**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.


## Related

- [URL to Markdown & Article Extractor](https://www.sfoln.com/actors/url-to-markdown-summary.md) (`sfoln/url-to-markdown-summary`)
- [URL Metadata & Open Graph Extractor](https://www.sfoln.com/actors/url-metadata.md) (`sfoln/url-metadata`)
- [URL Safety Checker – Phishing & Malware](https://www.sfoln.com/actors/url-safety-check.md) (`sfoln/url-safety-check`)
- [Domain WHOIS & DNS Lookup](https://www.sfoln.com/actors/domain-intelligence.md) (`sfoln/domain-intelligence`)
- [SEO Competitor Analysis – Traffic & Keywords](https://www.sfoln.com/actors/seo-competitor-snapshot.md) (`sfoln/seo-competitor-snapshot`)
- [SEO Audit API – On-Page Checks for Any URL](https://www.sfoln.com/actors/onpage-seo-audit.md) (`sfoln/onpage-seo-audit`)
- [Brand Monitoring – X, Reddit, News & Bluesky](https://www.sfoln.com/actors/brand-mentions-monitor.md) (`sfoln/brand-mentions-monitor`)
- [Substack Scraper – Newsletter Posts](https://www.sfoln.com/actors/substack-scraper.md) (`sfoln/substack-scraper`)

Publisher: SFOLN LLC, contact@sfoln.com. Catalogue: https://www.sfoln.com/catalog.json. Agent guide: https://www.sfoln.com/agents.md.
