# Change Watcher - Push Events for Web and API Changes (`ernestmarza/change-watcher`) Actor

Watches URLs with a CSS/XPath/regex selector or a JSONPath query, filters out timestamps, tokens, counters and other rendering noise, and emits a normalized change event only when the watched block really changed. No change, no event, no charge.

- **URL**: https://apify.com/ernestmarza/change-watcher.md
- **Developed by:** [Ernest Marzá](https://apify.com/ernestmarza) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 change event emitteds

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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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

## Change Watcher

**Watch a URL. Get an event when it really changes. Pay only for the events.**

An Apify Actor. Declare one or more URLs with an optional selector; it fetches them,
extracts only the block you named, strips the rendering noise, compares against the
value it stored last time, and emits a normalized event when — and only when — the
watched block actually moved.

A run where nothing changed writes nothing and charges nothing.

### Why this exists

The Model Context Protocol has no push. Issues
[#179](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/179) and
[#611](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/611) have been
open for over a year. Until that changes, every agent that needs to react to something
changing has exactly one option: poll it. Polling means pulling a page into the context
window on a schedule, paying tokens to read it, and paying again to conclude that nothing
happened. It is expensive, and it is slow in exactly the case that matters — the interval
between the change and the next poll.

This Actor inverts it. It does the polling once, off the agent's clock and off the agent's
token budget, and hands back a small structured event when there is something to say. The
fetch is the cost, and one fetch serves every subscriber watching the same target, so the
marginal cost of the second subscriber to a URL is close to zero.

### The hard part is not the diff

A naive diff of any live page fires on every single check. The header clock moved. The
CSRF token rotated. The view counter ticked. The CDN handed out a fresh cache-busting
query string. The server-side renderer emitted the attributes in a different order. None
of that is a change, and a watcher that reports all of it is worse than useless, because
a subscriber learns to ignore it.

So most of this Actor is the part that decides what *not* to report:

| Problem | What it does |
|---|---|
| Page-level noise | Hashes the **selected block**, never the whole document |
| Timestamps, dates, relative times | Masked with stable placeholders before hashing |
| Session ids, CSRF tokens, nonces, JWTs, UUIDs, hex and base64 blobs | Masked |
| View counters, "12 people are viewing this" | Masked |
| Cache-busting and tracking query parameters | Masked |
| HTML attribute order, build-hashed class names, framework-generated ids | Normalized away |
| `<script>`, `<style>`, HTML comments | Removed before the text is taken |
| JSON key order | Serialized in sorted key order |
| A/B buckets and half-deployed clusters | Stability window: N consecutive checks |
| Residual jitter no generic rule catches | `minChangeRatio` noise floor, plus custom patterns |

What is deliberately **not** masked: bare numbers. A price, a stock level, a version and
a score are all bare numbers and they are exactly what people watch. Masking them would
make the Actor detect nothing at all.

### Minimal input

```json
{
  "watchers": [
    { "url": "https://example.com", "selector": "h1" }
  ]
}
````

The first run records a baseline and emits nothing — there is nothing to compare against
yet. Every run after that emits an event if the block moved.

### A fuller example

```json
{
  "watchers": [
    {
      "label": "Pricing table",
      "url": "https://example.com/pricing",
      "selectorType": "css",
      "selector": "#pricing .plan-price"
    },
    {
      "label": "Service status",
      "url": "https://api.example.com/v1/status",
      "mode": "json",
      "selectorType": "jsonpath",
      "selector": "$.components[?(@.name == 'API')].status"
    },
    {
      "label": "Release tag",
      "url": "https://api.example.com/v1/releases/latest",
      "mode": "json",
      "selector": "$.tag_name",
      "headers": { "authorization": "Bearer REPLACE_ME" }
    }
  ],
  "stabilityChecks": 3,
  "minChangeRatio": 0.01,
  "topicPrefix": "change"
}
```

### What a watcher is

| Field | Meaning |
|---|---|
| `url` | Required, absolute http/https |
| `selector` | The query. Omit it to watch the whole body. |
| `selectorType` | `css`, `xpath`, `regex`, `jsonpath` or `wholeBody`. Inferred only when unambiguous: no selector means `wholeBody`, and a selector in `json` mode means `jsonpath`. |
| `mode` | `text` (default) or `json` |
| `extractAs` | `text` (default) or `html`. `html` compares markup structure, not just the words. |
| `label` | Used in logs and in the topic. Defaults to the hostname. |
| `id` | Stable identifier. Derived from host and selector when omitted. |
| `topic` | Overrides the generated topic entirely. |
| `method`, `headers`, `body` | For APIs that need a POST or an auth header. |
| `joinWith` | Separator when the selector matches several nodes. Defaults to a newline. |

#### Selector support, honestly

- **CSS** — full support, via cheerio.
- **XPath** — a documented subset, translated to CSS: `//tag`, `/a/b`, `//*`,
  `[@attr]`, `[@attr='v']`, `[n]`, `contains(@attr,'v')`, `starts-with(@attr,'v')`,
  trailing `/text()` and `/@attr`. Axes, unions, `position()` arithmetic and
  `[text()='x']` are **rejected with an error**, not silently mistranslated. A silently
  wrong selector would compare the wrong block forever, which is much worse than a
  loud failure.
- **Regex** — capture group 1 when the pattern has one, the whole match otherwise.
- **JSONPath** — `$`, `.name`, `['name']`, `[n]`, `[-1]`, `[*]`, `[a:b]`, `..name`,
  and filters with `==`, `!=`, `>`, `>=`, `<`, `<=`, `=~` and bare presence.

### The event

```json
{
  "topic": "change.example-com.pricing-table",
  "schema": "apify.change-watcher/change-event/v1",
  "timestamp": "2026-07-27T15:00:37.455Z",
  "url": "https://example.com/pricing",
  "watcherId": "example-com-pricing-plan-price",
  "label": "Pricing table",
  "changeType": "updated",
  "oldValue": "49.99 EUR",
  "newValue": "54.99 EUR",
  "diff": {
    "similarity": 0.818,
    "addedLines": ["54.99"],
    "removedLines": ["49.99"],
    "unifiedDiff": "-49.99\n+54.99",
    "changedCharacters": 10,
    "truncated": false
  },
  "confidence": 0.78,
  "meta": {
    "checkCount": 42,
    "confirmations": 3,
    "requiredConfirmations": 3,
    "hashBefore": "535fa30d...",
    "hashAfter": "6b51d431...",
    "selectorType": "css",
    "selector": "#pricing .plan-price",
    "mode": "text",
    "appliedIgnoreRules": ["unicode", "isoTimestamp", "..."],
    "candidateFirstSeenAt": "2026-07-27T14:58:31.002Z",
    "previousChangeAt": "2026-07-19T09:12:00.000Z",
    "confidenceFactors": {
      "base": 0.9,
      "stabilityFactor": 1,
      "magnitudeFactor": 1,
      "integrityFactor": 1
    }
  }
}
```

`changeType` is `created` when the block appears (or the selector starts matching),
`removed` when it stops matching, `updated` otherwise.

#### Confidence

Confidence is a product of four independent factors, all of them reported in
`meta.confidenceFactors` so nobody has to take the number on faith:

- **stability** — how many consecutive checks the value survived. One check is a guess;
  three is evidence.
- **magnitude** — a change of a handful of characters in a large block is more likely to
  be noise that slipped past the filters than real news (scored down), and a change that
  replaces almost the entire block usually means the page broke rather than that the
  content was rewritten (also scored down).
- **integrity** — the block collapsing to nothing, or the selector losing its match
  entirely, are reported as changes because they are, but they are the outcomes most
  likely to be an upstream problem.

### The stability window

A differing value does not become an event. It becomes a *candidate*, and it has to show
up again unchanged on `stabilityChecks` consecutive checks before it is confirmed.

- A candidate replaced by yet another value resets the counter to one.
- A candidate that reverts to the confirmed value is dropped outright — that is the A/B
  bucket and rolling-deploy case, and it is the single most common source of phantom
  events.

The window can close across scheduled runs (state persists) or inside one run by setting
`checksPerRun` above 1. Note that a run with `checksPerRun` above 1 stays alive for
`(checksPerRun - 1) x checkIntervalSeconds` and pays compute for the wait.

### State

State lives in a **named** key-value store, `change-watcher-state` by default. The default
store of a run is scoped to that run, so state written there would be gone by the next
check; the named store is what turns a series of one-shot runs into a watcher with a
memory.

Each watcher gets its own record, keyed by its id **and** a fingerprint of its extraction
settings (`url`, `mode`, `selectorType`, `selector`, `extractAs`, `joinWith`, `method`,
`body`, `headers`). Two consequences, both deliberate:

- Two watchers on the same URL with different selectors never share a baseline. Sharing
  one would make each report the other's value as a change on every single check.
- Editing a selector starts a clean baseline instead of producing one guaranteed false
  change against a value that was extracted a different way.

Set `resetState: true` to drop the baselines and start over — do that after changing the
noise filters, since the stored baseline was normalized under the old rules.

### Output

| Where | What |
|---|---|
| Dataset | One item per confirmed change event. **Signal only** — an empty dataset is an unambiguous "nothing happened". |
| `OUTPUT` (key-value store) | Run summary: per-watcher checks performed, baseline registered, change detected, pending candidate and its confirmation count, 304s, errors. |
| `EVENTS` (key-value store) | The same events as one JSON array, for consumers that want a single request. |
| `change-watcher-state` store | The persisted baselines. Inspect these to see the exact normalized value a watcher is comparing against. |

The dataset has three views: **Change events** (what moved), **Before and after** (the
values), and **Detection details** (hashes, confirmations, confidence factors).

### Cost behaviour

Two pay-per-event events:

| Event | Charged when |
|---|---|
| `watcher-registered` | The first time a target is baselined. Cheap. |
| `change-event-emitted` | Per confirmed change event. This is the one that pays. |

A check that finds nothing is **not charged at all** — not at a reduced rate, not a page
fee. That is the whole proposition: polling costs the agent tokens on every check whether
or not anything happened; this costs nothing until there is something to say.

Conditional requests are on by default. When the server supports ETag or Last-Modified, a
304 answers "did this change?" for the price of the headers — no body, no parsing, no
diff.

### Running it

```bash
npm install
npm test          # builds, then runs the suite
npm start         # builds and runs once against storage/key_value_stores/default/INPUT.json
apify run         # same, through the Apify CLI
apify push        # deploy to your account
```

To watch continuously, schedule the Actor on the Apify platform at whatever interval
suits the target. State carries over between runs automatically.

### Limits and known gaps

- **No JavaScript rendering.** The Actor fetches HTML; it does not run a browser. A value
  that only exists after client-side hydration will not be seen. Watch the API the page
  calls instead — it is cheaper and more stable anyway.
- **No proxy support.** Targets that block datacenter traffic will need one; it is not
  wired up yet.
- **XPath is a translated subset**, as described above.
- **`epochMillis` masking is off by default.** It would swallow any 10- or 13-digit
  number, and some of those are content.
- **`clockTime` masks ratios that look like times.** `16:9` reads as a clock. Turn the
  rule off if that matters for your target.
- Requests are made sequentially. Fifty watchers on slow hosts make for a slow run.

### Layout

```
src/
  main.ts              entry point, run summary, charging
  engine.ts            the check loop
  input.ts             input parsing and validation
  fetcher.ts           HTTP with retries and conditional requests
  extract/
    index.ts           extractor dispatch, stable JSON serialization
    xpath.ts           XPath subset to CSS translation
    jsonpath.ts        JSONPath evaluator
  normalize/
    rules.ts           the anti-noise catalogue
    text.ts            normalization pipeline and hashing
    html.ts            markup-level de-noising
  diff.ts              LCS diff and the confidence score
  stability.ts         the stability window (pure, no I/O)
  store.ts             persistent state and key namespacing
  event.ts             event construction and topics
  billing.ts           pay-per-event charging
tests/                 106 tests across normalization, extraction, diff, window, input, engine
```

# Actor input Schema

## `watchers` (type: `array`):

One entry per watched target. Each entry accepts: url (required), selector, selectorType (css | xpath | regex | jsonpath | wholeBody), mode (text | json), extractAs (text | html), label, id, topic, method, headers, body, joinWith. Omit the selector to watch the whole body. A plain URL string is also accepted and means 'watch this whole page'. Maximum 50 entries per run.

## `stabilityChecks` (type: `integer`):

How many consecutive checks a new value has to survive before it is reported. 1 fires on the first difference. 2 or 3 filters out A/B buckets, staggered cache invalidation and half-deployed clusters, which otherwise produce a change event every time the load balancer picks a different node.

## `checksPerRun` (type: `integer`):

How many times each target is checked within a single run. Set it above 1 to close the stability window inside one run instead of across several scheduled runs. Note that the run stays alive for (checksPerRun - 1) x check interval, which costs compute.

## `checkIntervalSeconds` (type: `integer`):

Only used when checks per run is greater than 1.

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

Ignore differences smaller than this fraction of the block. 0 reports every difference that survives the noise filters. 0.02 means 'at least two percent of the block has to move'. Use it for targets with residual jitter the generic rules cannot catch, such as a rotating advert or a randomised testimonial.

## `emitInitialState` (type: `boolean`):

By default the first time a target is seen it only records a baseline: there is nothing to compare against, so calling it a change would be noise. Turn this on to receive a 'created' event carrying the initial value, which is useful when the subscriber wants to seed its own copy.

## `ignoreRules` (type: `array`):

Which parts of the anti-noise catalogue to run before hashing. Leave empty for the recommended set (everything except epoch masking). Available ids: unicode, jwt, isoTimestamp, httpDate, epochMillis, dateNumeric, dateTextual, clockTime, relativeTime, uuid, tokenAssignment, hexBlob, base64Blob, cacheBusting, trackingParams, viewCounters, whitespace.

## `customIgnorePatterns` (type: `array`):

Regular expressions applied after the catalogue; every match is replaced with <CUSTOM>. Both 'pattern' and '/pattern/flags' forms are accepted. The global flag is forced on.

## `caseSensitive` (type: `boolean`):

Turn off to ignore changes that are only a difference in letter case.

## `includeDiff` (type: `boolean`):

Adds added lines, removed lines, a unified diff and a similarity score to each event. Turning it off makes events smaller but leaves the subscriber to work out what moved.

## `maxValueLength` (type: `integer`):

Old and new values longer than this are truncated with an explicit marker. Guards against a watcher on a large page filling the dataset.

## `topicPrefix` (type: `string`):

Every event carries a topic built as prefix.host.label, for example change.example-com.pricing-table. Subscribers route on it without opening the payload. A per-watcher 'topic' field overrides it entirely.

## `stateStoreName` (type: `string`):

Named key-value store that holds the baseline between runs. This is what turns a series of one-shot runs into a watcher with a memory. Use different names to keep unrelated watch sets apart.

## `resetState` (type: `boolean`):

Drops the stored baseline for every watcher in this run and starts over. Use after changing the noise filters, since the old baseline was normalized with different rules.

## `useConditionalRequests` (type: `boolean`):

Sends the validators from the previous fetch so a server that supports them can answer 304 with no body at all. Much cheaper on targets that support it. Turn it off for servers that return misleading validators.

## `requestTimeoutSecs` (type: `integer`):

How long a single request may take before it is aborted and retried.

## `maxRetries` (type: `integer`):

Attempts before a check is recorded as failed. 5xx and timeouts are retried with backoff; 4xx is not, because it will not get better.

## `userAgent` (type: `string`):

Sent with every request. Identify yourself honestly; some sites block generic agents.

## Actor input object example

```json
{
  "watchers": [
    {
      "label": "Page heading",
      "url": "https://example.com",
      "selectorType": "css",
      "selector": "h1"
    },
    {
      "label": "Release tag",
      "url": "https://api.github.com/repos/apify/apify-sdk-js/releases/latest",
      "mode": "json",
      "selectorType": "jsonpath",
      "selector": "$.tag_name"
    }
  ],
  "stabilityChecks": 2,
  "checksPerRun": 1,
  "checkIntervalSeconds": 30,
  "minChangeRatio": 0,
  "emitInitialState": false,
  "ignoreRules": [],
  "customIgnorePatterns": [],
  "caseSensitive": true,
  "includeDiff": true,
  "maxValueLength": 8000,
  "topicPrefix": "change",
  "stateStoreName": "change-watcher-state",
  "resetState": false,
  "useConditionalRequests": true,
  "requestTimeoutSecs": 30,
  "maxRetries": 3,
  "userAgent": "Mozilla/5.0 (compatible; ApifyChangeWatcher/0.1; +https://apify.com)"
}
```

# Actor output Schema

## `changeEvents` (type: `string`):

One dataset item per confirmed change event, in the apify.change-watcher/change-event/v1 shape: topic, schema, timestamp, url, watcherId, label, changeType (created | updated | removed), oldValue, newValue, diff and confidence. This is the primary output and the only one that is charged for. An empty array means no watched block changed during this run.

## `changeEventsJson` (type: `string`):

The same events as one JSON array in the key-value store, for consumers that want a single request rather than a paginated dataset.

## `runSummary` (type: `string`):

Per-watcher outcome for the whole run: checks performed, whether a baseline was registered, whether a change was confirmed, whether an unconfirmed candidate is still pending and how many confirmations it has, whether the server answered 304, and any error. Read this to answer 'did the watcher work?' as opposed to 'did the page change?'.

## `watcherState` (type: `string`):

The baselines that survive between runs, one record per watcher, in the named state store (change-watcher-state by default). Keys are prefixed with watcher-state-. Inspect these to see the exact normalized value a watcher is comparing against.

# 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 = {
    "watchers": [
        {
            "label": "Page heading",
            "url": "https://example.com",
            "selectorType": "css",
            "selector": "h1"
        },
        {
            "label": "Release tag",
            "url": "https://api.github.com/repos/apify/apify-sdk-js/releases/latest",
            "mode": "json",
            "selectorType": "jsonpath",
            "selector": "$.tag_name"
        }
    ],
    "stabilityChecks": 2,
    "ignoreRules": [],
    "customIgnorePatterns": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("ernestmarza/change-watcher").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 = {
    "watchers": [
        {
            "label": "Page heading",
            "url": "https://example.com",
            "selectorType": "css",
            "selector": "h1",
        },
        {
            "label": "Release tag",
            "url": "https://api.github.com/repos/apify/apify-sdk-js/releases/latest",
            "mode": "json",
            "selectorType": "jsonpath",
            "selector": "$.tag_name",
        },
    ],
    "stabilityChecks": 2,
    "ignoreRules": [],
    "customIgnorePatterns": [],
}

# Run the Actor and wait for it to finish
run = client.actor("ernestmarza/change-watcher").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 '{
  "watchers": [
    {
      "label": "Page heading",
      "url": "https://example.com",
      "selectorType": "css",
      "selector": "h1"
    },
    {
      "label": "Release tag",
      "url": "https://api.github.com/repos/apify/apify-sdk-js/releases/latest",
      "mode": "json",
      "selectorType": "jsonpath",
      "selector": "$.tag_name"
    }
  ],
  "stabilityChecks": 2,
  "ignoreRules": [],
  "customIgnorePatterns": []
}' |
apify call ernestmarza/change-watcher --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Change Watcher - Push Events for Web and API Changes",
        "description": "Watches URLs with a CSS/XPath/regex selector or a JSONPath query, filters out timestamps, tokens, counters and other rendering noise, and emits a normalized change event only when the watched block really changed. No change, no event, no charge.",
        "version": "0.1",
        "x-build-id": "zkemszEI4pYTGHJtV"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ernestmarza~change-watcher/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ernestmarza-change-watcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ernestmarza~change-watcher/runs": {
            "post": {
                "operationId": "runs-sync-ernestmarza-change-watcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ernestmarza~change-watcher/run-sync": {
            "post": {
                "operationId": "run-sync-ernestmarza-change-watcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "watchers"
                ],
                "properties": {
                    "watchers": {
                        "title": "Watchers",
                        "type": "array",
                        "description": "One entry per watched target. Each entry accepts: url (required), selector, selectorType (css | xpath | regex | jsonpath | wholeBody), mode (text | json), extractAs (text | html), label, id, topic, method, headers, body, joinWith. Omit the selector to watch the whole body. A plain URL string is also accepted and means 'watch this whole page'. Maximum 50 entries per run."
                    },
                    "stabilityChecks": {
                        "title": "Stability window (consecutive checks)",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "How many consecutive checks a new value has to survive before it is reported. 1 fires on the first difference. 2 or 3 filters out A/B buckets, staggered cache invalidation and half-deployed clusters, which otherwise produce a change event every time the load balancer picks a different node."
                    },
                    "checksPerRun": {
                        "title": "Checks per run",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "How many times each target is checked within a single run. Set it above 1 to close the stability window inside one run instead of across several scheduled runs. Note that the run stays alive for (checksPerRun - 1) x check interval, which costs compute.",
                        "default": 1
                    },
                    "checkIntervalSeconds": {
                        "title": "Seconds between checks in a run",
                        "minimum": 1,
                        "maximum": 900,
                        "type": "integer",
                        "description": "Only used when checks per run is greater than 1.",
                        "default": 30
                    },
                    "minChangeRatio": {
                        "title": "Minimum change ratio (0-1)",
                        "minimum": 0,
                        "maximum": 1,
                        "type": "number",
                        "description": "Ignore differences smaller than this fraction of the block. 0 reports every difference that survives the noise filters. 0.02 means 'at least two percent of the block has to move'. Use it for targets with residual jitter the generic rules cannot catch, such as a rotating advert or a randomised testimonial.",
                        "default": 0
                    },
                    "emitInitialState": {
                        "title": "Emit an event for the first observation",
                        "type": "boolean",
                        "description": "By default the first time a target is seen it only records a baseline: there is nothing to compare against, so calling it a change would be noise. Turn this on to receive a 'created' event carrying the initial value, which is useful when the subscriber wants to seed its own copy.",
                        "default": false
                    },
                    "ignoreRules": {
                        "title": "Noise filters to apply",
                        "type": "array",
                        "description": "Which parts of the anti-noise catalogue to run before hashing. Leave empty for the recommended set (everything except epoch masking). Available ids: unicode, jwt, isoTimestamp, httpDate, epochMillis, dateNumeric, dateTextual, clockTime, relativeTime, uuid, tokenAssignment, hexBlob, base64Blob, cacheBusting, trackingParams, viewCounters, whitespace.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "customIgnorePatterns": {
                        "title": "Extra patterns to ignore",
                        "type": "array",
                        "description": "Regular expressions applied after the catalogue; every match is replaced with <CUSTOM>. Both 'pattern' and '/pattern/flags' forms are accepted. The global flag is forced on.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "caseSensitive": {
                        "title": "Case sensitive comparison",
                        "type": "boolean",
                        "description": "Turn off to ignore changes that are only a difference in letter case.",
                        "default": true
                    },
                    "includeDiff": {
                        "title": "Include the diff in every event",
                        "type": "boolean",
                        "description": "Adds added lines, removed lines, a unified diff and a similarity score to each event. Turning it off makes events smaller but leaves the subscriber to work out what moved.",
                        "default": true
                    },
                    "maxValueLength": {
                        "title": "Maximum characters per value",
                        "minimum": 100,
                        "maximum": 200000,
                        "type": "integer",
                        "description": "Old and new values longer than this are truncated with an explicit marker. Guards against a watcher on a large page filling the dataset.",
                        "default": 8000
                    },
                    "topicPrefix": {
                        "title": "Topic prefix",
                        "type": "string",
                        "description": "Every event carries a topic built as prefix.host.label, for example change.example-com.pricing-table. Subscribers route on it without opening the payload. A per-watcher 'topic' field overrides it entirely.",
                        "default": "change"
                    },
                    "stateStoreName": {
                        "title": "State store name",
                        "type": "string",
                        "description": "Named key-value store that holds the baseline between runs. This is what turns a series of one-shot runs into a watcher with a memory. Use different names to keep unrelated watch sets apart.",
                        "default": "change-watcher-state"
                    },
                    "resetState": {
                        "title": "Reset stored state before this run",
                        "type": "boolean",
                        "description": "Drops the stored baseline for every watcher in this run and starts over. Use after changing the noise filters, since the old baseline was normalized with different rules.",
                        "default": false
                    },
                    "useConditionalRequests": {
                        "title": "Use conditional requests (ETag / If-Modified-Since)",
                        "type": "boolean",
                        "description": "Sends the validators from the previous fetch so a server that supports them can answer 304 with no body at all. Much cheaper on targets that support it. Turn it off for servers that return misleading validators.",
                        "default": true
                    },
                    "requestTimeoutSecs": {
                        "title": "Request timeout (seconds)",
                        "minimum": 5,
                        "maximum": 300,
                        "type": "integer",
                        "description": "How long a single request may take before it is aborted and retried.",
                        "default": 30
                    },
                    "maxRetries": {
                        "title": "Retries per check",
                        "minimum": 1,
                        "maximum": 6,
                        "type": "integer",
                        "description": "Attempts before a check is recorded as failed. 5xx and timeouts are retried with backoff; 4xx is not, because it will not get better.",
                        "default": 3
                    },
                    "userAgent": {
                        "title": "User agent",
                        "type": "string",
                        "description": "Sent with every request. Identify yourself honestly; some sites block generic agents.",
                        "default": "Mozilla/5.0 (compatible; ApifyChangeWatcher/0.1; +https://apify.com)"
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
