# Page Watch — Website Change Monitor & Content Tracker (`mfapitools/page-watch`) Actor

Monitor any URL for content changes with hash-based change detection. Pay per check + per detected change. Ideal for watching competitor pages, docs, pricing, or any public web page.

- **URL**: https://apify.com/mfapitools/page-watch.md
- **Developed by:** [Mariano Ferreras](https://apify.com/mfapitools) (community)
- **Categories:** Automation, Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Page Watch — Website Change Monitor

> **Pay-per-event pricing.** Fetches URLs, hashes their content, and compares against the previous run's hash stored in Apify's KeyValueStore. Per-URL CSS selectors scope the check to specific page sections. A noise threshold (`minChangeRatio`) suppresses small changes from rotating widgets or ad slots. Several competitors offer similar features — this is a commodity market; pick based on price and reliability. $0.001/check + $0.01/change.

The most important limitation: ultimate page-watch is trivial to DIY — a cron job + `curl | sha256sum`. You're paying for the hosted scheduler, KVS persistence across runs, and the CSS-selector noise-threshold UX.

***

### What it does

This actor fetches one or more URLs, extracts the page text (optionally scoped to specific sections via CSS selectors), computes a content hash, and compares it against the previous check's hash stored in the actor's persistent KeyValueStore. An optional noise threshold (`minChangeRatio`, using Dice coefficient over word multisets) ignores small content churn so you only get charged for changes that matter. Each result includes the hashes and a diff excerpt around the first divergence. Several competitors offer similar features (e.g. `foo121/website-change-monitor` has CSS selectors + noise-filtered diffing, `ryanclinton/website-change-monitor` has 31 users).

Run it once to establish a baseline, then schedule it (e.g. every hour, daily) to **monitor web pages** continuously. Because it uses Apify's built-in KeyValueStore, the hash state persists across scheduled runs — no external storage needed. If you're doing **competitor page tracking** at scale, batch multiple URLs into a single run to work around Apify's schedule limit (100 per account).

***

### Use cases

- **Competitor page tracking** — watch competitor landing pages, product pages, or docs for changes. Get notified when they update pricing, features, or copy.
- **Price tracking** — watch pricing pages on e-commerce or SaaS sites. Detects when prices go up or down.
- **Documentation change alerts** — monitor API docs, terms of service, or compliance pages. Know the moment something changes.
- **Regulatory / legal monitoring** — watch government pages, procurement portals, or regulatory sites for new announcements.
- **Agent-powered watchlists** — wire this into an AI agent workflow: have your agent set up monitors, check diff excerpts, and summarize what changed.

***

### Input example

Watch only a price div on one page, the full body on another, and ignore under-2% churn everywhere:

```json
{
  "urls": [
    { "url": "https://example.com/pricing", "selector": ".pricing-table" },
    "https://example.com/docs"
  ],
  "minChangeRatio": 0.02,
  "maxItems": 50,
  "useResidentialProxy": false
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `urls` | array | required | One or more URLs to check. Each entry can be a plain URL string, `{ "url": "..." }`, or `{ "url": "...", "selector": "..." }` to scope just that URL to a CSS selector |
| `defaultSelector` | string | none | CSS selector applied to any URL that doesn't set its own `selector`. Example: `.pricing-table` |
| `minChangeRatio` | number | `0` | Noise threshold in `[0, 1]`. `0` (default) reports every content hash difference, same as before. `0.02` ignores under-2% content churn — the hash still notices the diff, but it won't be reported as `changed` or charged unless the changed fraction meets the threshold |
| `selectors` | array | `[]` | Legacy, global multi-selector mode — applied only to URLs with no per-URL `selector` and no `defaultSelector`. Kept for backward compatibility |
| `maxItems` | integer | `50` | Max URLs to check in this run. `0` = unlimited |
| `useResidentialProxy` | boolean | `false` | Upgrade to residential proxies if the target blocks datacenter IPs |

#### Per-URL selectors — behavior

- Selector resolution order per URL: its own `selector` → `defaultSelector` → the legacy global `selectors` array → full page body.
- **A bad selector never produces a silent phantom "change".** If a per-URL/default selector is invalid CSS or matches no content on the page, the actor logs a warning, sets `selectorWarning` on that URL's result row, and falls back to hashing the full page instead of a stable empty string.
- Switching a URL's selector (adding, removing, or changing it) changes what's being tracked, so the very next check may legitimately report one change as the new baseline is established — this is expected, not a bug.

***

### Output example

```json
[
  {
    "url": "https://example.com/pricing",
    "checkedAt": "2026-07-23T23:00:00.000Z",
    "changed": true,
    "hash": "a1b2c3d4e5f6g7h8",
    "previousHash": "9i8j7k6l5m4n3o2p",
    "title": "Example — Pricing",
    "contentLength": 4523,
    "selector": ".pricing-table",
    "selectorWarning": null,
    "diffSnippetPrevious": "...our starter plan is $10 per month with...",
    "diffSnippetCurrent": "...our starter plan is $15 per month with...",
    "changeRatio": 0.18,
    "belowChangeThreshold": false
  }
]
```

| Field | Type | Notes |
|---|---|---|
| `url` | string | The URL that was checked |
| `checkedAt` | string | ISO 8601 timestamp of the check |
| `changed` | boolean | `true` only when the content hash differs from the previous check AND (if `minChangeRatio` is set) the changed fraction meets the threshold |
| `hash` | string | SHA-256 hex digest (first 16 chars) of the current extracted content |
| `previousHash` | string|null | Hash from the previous check. `null` on the first run |
| `title` | string|null | Page `<title>` content |
| `contentLength` | integer | Character count of the extracted text |
| `selector` | string|null | CSS selector actually used to extract content (per-URL, `defaultSelector`, or the legacy `selectors` list joined). `null` for full-page checks |
| `selectorWarning` | string|null | Set when a per-URL/default selector was invalid or matched nothing — the check fell back to full-page text. `null` when no issue occurred |
| `diffSnippetPrevious` | string|null | Excerpt of the previous content around the first difference. `null` when unchanged or first check |
| `diffSnippetCurrent` | string|null | Excerpt of the current content around the first difference. `null` when unchanged or first check |
| `changeRatio` | number|null | Fraction of content changed vs. the previous check (`0`-`1`), computed only when the hash differs and a previous check exists. `null` otherwise |
| `belowChangeThreshold` | boolean | `true` when the hash differed but `changeRatio` was below `minChangeRatio` — a real change was detected but suppressed as noise (not reported, not charged) |

***

### Pricing

This actor uses **pay-per-event** pricing. You are never charged for failed requests or quarantined results.

| Event | Price | Description |
|---|---|---|
| `page-checked` | $1.00 / 1,000 checks | Charged once per URL checked, regardless of whether it changed |
| `page-changed` | $10.00 / 1,000 changes | Charged once per detected change (in addition to the check fee) |

The `page-changed` event is only triggered when content actually differs from the previous check. Use a small `maxItems` for test runs. At $0.001/check, monitoring 10 URLs daily costs ~$0.30/month.

***

### Scheduling

Page Watch is designed to be run on a schedule. Create a schedule in Apify Console (e.g. hourly, daily) and the actor will automatically compare against the previous run's hashes stored in its KeyValueStore.

To watch multiple URLs, batch them into a single run using the `urls` input array. This works around Apify's schedule limit (100 per account) and is more efficient than one schedule per URL.

***

### Use with AI agents (MCP)

This actor is available as a tool for AI assistants via the Apify MCP server. Agents can call it to **monitor web pages for content changes** directly — no Apify console needed, just a single function call.

#### MCP setup

```json
{
  "mcpServers": {
    "apify": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mcp.apify.com?tools=mfapitools/page-watch",
        "--header",
        "Authorization: Bearer <YOUR_API_TOKEN>"
      ]
    }
  }
}
```

#### Example agent prompts

- "Monitor these five competitor pricing pages and tell me if any changed"
- "Set up change detection for this government procurement portal URL"
- "Check all my watched URLs and summarize the diffs"

The actor returns structured JSON with the same fields shown above. Agentic workflows (Claude Code, Cursor, any MCP-compatible AI) can monitor pages and analyze change excerpts in the same run.

***

### Limitations

- **Text-based comparison** — the actor extracts normalized body text (with `<script>`, `<style>`, `<noscript>` stripped to avoid false positives from inline nonces/timestamps) and computes a hash. On change, you get a small excerpt around the *first* divergence — not a full structural or visual diff, and not an LLM "what changed" summary (planned for a future version).
- **First run is always a baseline** — the first check of any URL will show `changed: false` because there is no previous hash. The second run detects changes.
- **`minChangeRatio` compares only the first ~10,000 characters** of extracted text from each side (the same window used for diff-snippet storage) — a size-bounded, cheap comparison. For most pages this is representative of the whole page; for very large pages, changes past that window won't move the ratio.
- **Public data only** — this actor fetches public URLs. No login-gated content.
- **No notification delivery in v1.0** — changes are recorded in the dataset. You can wire up webhooks or Apify's email/slack integrations in Console to get notified when new results arrive.
- **Hash collisions are theoretically possible** — using SHA-256 truncated to 16 hex chars (64 bits). The collision probability is negligible for this use case (~2^-32 for 100k checks).

***

### Changelog

See [CHANGELOG.md](./CHANGELOG.md).

# Actor input Schema

## `urls` (type: `array`):

One or more URLs to check for content changes. Each URL is fetched, hashed, and compared against its previous hash (stored in the actor's KeyValueStore). Accepts plain strings, { "url": "..." } objects, or { "url": "...", "selector": "..." } to scope just that URL to a CSS selector (e.g. watch only a price div on one page while monitoring the full body on another). A bad/no-match selector falls back to full-page text with a loud warning — it never silently reports a phantom change.

## `defaultSelector` (type: `string`):

Fallback CSS selector applied to any URL in the list that doesn't set its own per-URL selector. Example: ".pricing-table". Leave empty to use the legacy `selectors` field (or full page) instead.

## `selectors` (type: `array`):

Deprecated in favor of per-URL { url, selector } entries or `defaultSelector` — kept for backward compatibility. Applied only to URLs that have no per-URL selector and no defaultSelector. If empty, the full page body text is monitored. Example: \[".pricing-table", ".product-description"]

## `minChangeRatio` (type: `number`):

Ignore small changes. 0 (default) reports every content hash difference, same as before. Set e.g. 0.02 to only report/charge a change when at least 2% of the tracked text differs from the previous check — useful for pages with small rotating widgets, ad slots, or view counters that would otherwise trigger noisy false positives.

## `maxItems` (type: `integer`):

Maximum number of URLs to check in this run. 0 = unlimited. You are charged per check and per detected change.

## `useResidentialProxy` (type: `boolean`):

Switch to residential proxies if datacenter IPs are blocked. Increases cost but may be needed for bot-protected targets.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com"
    }
  ],
  "selectors": [],
  "minChangeRatio": 0,
  "maxItems": 50,
  "useResidentialProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        {
            "url": "https://example.com"
        }
    ],
    "selectors": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("mfapitools/page-watch").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": [{ "url": "https://example.com" }],
    "selectors": [],
}

# Run the Actor and wait for it to finish
run = client.actor("mfapitools/page-watch").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    {
      "url": "https://example.com"
    }
  ],
  "selectors": []
}' |
apify call mfapitools/page-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=mfapitools/page-watch",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/q0G5nndypi3xfKQ1k/builds/p2dW8XuCXsX0iF9tm/openapi.json
