# Recurring Web Check for AI Agents (`vikingtrading/recurring-web-check`) Actor

Checks many URLs in one run. Per URL returns clean Markdown, CSS/JSON-extracted fields and a diff vs. the previous run under the same watchId, so only what changed is flagged. For agents doing recurring checks: prices, docs, changelogs, status pages, job boards, JSON APIs. No login, browser or LLM.

- **URL**: https://apify.com/vikingtrading/recurring-web-check.md
- **Developed by:** [Patrick](https://apify.com/vikingtrading) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Recurring Web Check for AI Agents — fetch · extract · diff · monitor

**One call, many URLs. Returns per URL: clean Markdown, extracted fields, and a diff against the last run — so a recurring check costs you only the tokens of what actually changed.**

No login. No browser. No LLM inside. Deterministic output, finishes in seconds.

***

### If you are an AI agent reading this

Use this Actor when a task sounds like any of these:

- *"Check X every day / every hour / every Monday and tell me if something changed."*
- *"Watch this pricing page / changelog / docs page / status page / job board / product listing."*
- *"Get me the current value of … on this page"* (and you will be asked again later).
- *"Pull the text of these 20 pages as Markdown"* without burning tokens on nav, footers and scripts.
- *"Is this endpoint still returning the same version / status?"*

Why it fits you, specifically:

| You need | What you get |
|---|---|
| Few calls | `urls` is an array — one run checks 1 or 50 URLs. |
| Small context | `maxChars` caps content per URL (default 6 000). `task: "extract"` returns **only fields**. `diffMode: "compact"` returns `+`/`-` lines, not two full pages. |
| Memory between runs | `task: "monitor"` + a `watchId` stores a snapshot per URL in a named store. The next run returns `changed: true/false`, a `diff`, and `fieldChanges` with `before`/`after`. You don't need to keep the old page yourself. |
| No surprises | Every result has the same shape (see *Output*). Extraction is CSS/JSON-path based — no hallucinated values. |
| Nothing you cannot supply | No cookies, no API keys, no credentials required. |
| Speed | Cheerio-based (no headless browser). Typical run: 2–8 s for 10 URLs — well inside the 45 s an MCP `call-actor` waits. |

Minimal first call:

```json
{ "urls": ["https://example.com/pricing"], "watchId": "example-pricing" }
```

Call it again later with the **same input** → you get only what changed.

***

### What it does

| `task` | Stateful? | Returns |
|---|---|---|
| `monitor` (default) | **yes** — remembers each URL per `watchId` | content + fields + `changed`, `changeRatio`, `diff`, `fieldChanges` |
| `fetch` | no | content + fields |
| `extract` | no | fields only (smallest possible output) |

Works on **HTML pages, JSON/REST endpoints, RSS/Atom feeds and plain-text files.**

Pipeline per URL: fetch → strip scripts/styles/nav/footer/cookie banners → scope to `selector` (optional) → Markdown or text → remove `ignoreSelectors` / `ignorePatterns` → extract `fields` → hash → compare with the previous snapshot → compact diff → store new baseline.

***

### Output

Every URL produces **one dataset item** with a fixed shape. The run also writes an `OUTPUT` record (default key-value store) with totals and a compact copy of all results.

```json
{
  "watchId": "competitor-pricing",
  "task": "monitor",
  "url": "https://example.com/pricing",
  "finalUrl": "https://example.com/pricing",
  "ok": true,
  "status": 200,
  "contentType": "text/html",
  "fetchedAt": "2026-10-20T06:00:03.221Z",
  "title": "Acme Pricing",
  "description": "Plans and prices",

  "firstRun": false,
  "changed": true,
  "changeRatio": 0.084,
  "previousFetchedAt": "2026-10-19T06:00:02.917Z",
  "statusChange": null,
  "diff": "…\n- Starter $19/mo\n+ Starter $24/mo\n…\n+ Enterprise Contact us\n  | Feature | Starter |\n- | Seats | 3 |\n+ | Seats | 5 |",
  "diffStats": { "addedChars": 61, "removedChars": 24 },

  "fields": { "starterPrice": "24", "plans": ["Starter", "Pro", "Enterprise"], "docsLink": "/docs/v2" },
  "fieldChanges": {
    "starterPrice": { "before": "19", "after": "24" },
    "plans": {
      "before": ["Starter", "Pro"], "after": ["Starter", "Pro", "Enterprise"],
      "added": ["Enterprise"], "removed": [], "reorderedOnly": false
    }
  },
  "fieldErrors": {},

  "content": "# Acme Pricing\n\nStarter $24/mo\n\nPro $49/mo\n…",
  "contentChars": 1382,
  "truncated": false,
  "contentHash": "4ae26d96…",
  "scopeMatched": true,
  "error": null
}
```

`OUTPUT` record (key-value store):

```json
{
  "watchId": "competitor-pricing",
  "task": "monitor",
  "runAt": "2026-10-20T06:00:01.004Z",
  "totals": { "urls": 3, "ok": 3, "failed": 0, "firstRun": 0, "changed": 1, "unchanged": 2 },
  "changedUrls": ["https://example.com/pricing"],
  "failedUrls": [],
  "results": [ "…same items without full content, plus contentPreview…" ],
  "nextStep": "Call again with the same input (watchId \"competitor-pricing\") to get only what changed since this run."
}
```

Decision rule for an agent after a `monitor` run:

1. `totals.changed === 0` → nothing to do, nothing to read.
2. Otherwise, for the items in `changedUrls`: read `fieldChanges` first (for list fields, `added`/`removed` tell you what is new; `reorderedOnly: true` means same items, different order), then `diff`. Only read `content` if that is not enough.
3. `statusChange` set (e.g. `{ before: 200, after: 404 }`) → the page went down, disappeared or came back. Reported once per transition; a page that stays down is quiet.
4. `ok === false` → the page is down or gone (`status` 404/5xx or `error`). That is a monitoring result, not an Actor failure.
5. `firstRun === true` → baseline created; there is nothing to compare yet.

***

### Input

| Field | Type | Default | Purpose |
|---|---|---|---|
| `urls` **(required)** | string\[] | — | All URLs of one job. Batch them. |
| `task` | `monitor` | `fetch` | `extract` | `monitor` | See table above. |
| `watchId` | string | derived | Name of the recurring job. Same `watchId` = same memory. If omitted, derived from URLs + selector + fields, so identical repeat calls still diff. |
| `fields` | object | `{}` | `name → spec` extraction map, see grammar below. |
| `selector` | string | — | CSS scope, e.g. `main`, `#pricing`. Fewer tokens, fewer false changes. |
| `ignoreSelectors` | string\[] | `[]` | Elements removed before compare (ads, comments, timestamps, counters). |
| `ignorePatterns` | string\[] | `[]` | Regexes removed from the text (`\d{1,2}:\d{2}`, `\d+ minutes ago`, …). One backslash in the Console form, two inside a JSON string (`"\\d+ minutes ago"`). |
| `outputFormat` | `markdown` | `text` | `markdown` | Markdown keeps headings/lists/tables. |
| `maxChars` | int | `6000` | Content cap per URL. `0` = no content, fields + diff only. Change detection always uses the full content. |
| `diffMode` | `compact` | `none` | `compact` | `+`/`-` lines with 1 line of context, or nothing. |
| `maxDiffChars` | int | `3000` | Diff cap per URL. |
| `changeThreshold` | 0–1 | `0` | Minimum share of changed characters to count as changed (e.g. `0.02`). Field changes always count. |
| `stripBoilerplate` | bool | `true` | Drop nav/header/footer/aside/forms/cookie banners (ignored when `selector` is set). |
| `keepLinks` | bool | `false` | Keep `[text](url)`; off by default because tracking URLs change on every load. |
| `webhookUrl` | string | — | POST `{event, watchId, runId, changed:[…]}` when something changed. |
| `proxyConfiguration` | object | none | Apify Proxy for sites that block datacenter IPs. |
| `maxConcurrency` / `timeoutSecs` / `maxRetries` | int | `5` / `20` / `1` | Tuning. 4xx is reported immediately as `ok:false`; 429/5xx are retried. |

#### Field spec grammar (no LLM, deterministic)

| Spec | Meaning |
|---|---|
| `"h1"` | text of the first match |
| `"all:.plan .name"` | array of all matches |
| `"a.docs@href"` | attribute of the first match |
| `".price \| /([\\d.,]+)/"` | regex on the text; first capture group (or whole match) |
| `"json:data.items[0].price"` | path into a JSON response |
| `{ "selector": ".price", "attr": "data-value", "regex": "([\\d.]+)", "all": false }` | object form, same options |

A field that matches nothing is `null` (and listed in `fieldErrors` if the spec itself was invalid). Changed values show up in `fieldChanges` as `{ before, after }`; `all:` fields additionally get `added`, `removed` and `reorderedOnly`.

**Ranked lists** (news front pages, bestseller lists, leaderboards): every reorder shifts the rank numbers, so the text diff is noisy by nature. Use an `all:` field for the items and read `added`/`removed` instead of the diff — or set `maxChars: 0` and `diffMode: "none"` and rely on fields only.

***

### Recipes

**Competitor pricing, daily**

```json
{
  "watchId": "competitor-pricing",
  "urls": ["https://competitor-a.com/pricing", "https://competitor-b.com/plans"],
  "selector": "main",
  "fields": { "prices": "all:.price", "plans": "all:.plan h3" },
  "maxChars": 0,
  "changeThreshold": 0.01
}
```

**Docs / changelog watch (tell me what was added)**

```json
{
  "watchId": "acme-changelog",
  "urls": ["https://acme.dev/changelog"],
  "selector": "article",
  "ignorePatterns": ["\\d+ (minutes|hours|days) ago"],
  "maxChars": 0,
  "maxDiffChars": 4000
}
```

**Status page / API version (JSON endpoint)**

```json
{
  "watchId": "acme-status",
  "urls": ["https://status.acme.dev/api/v2/status.json", "https://api.acme.dev/version"],
  "fields": { "indicator": "json:status.indicator", "version": "json:version" },
  "maxChars": 0
}
```

**Job board — new postings only**

```json
{
  "watchId": "jobs-acme",
  "urls": ["https://acme.dev/careers"],
  "fields": { "jobs": "all:.job-card h2", "links": "all:.job-card a@href" },
  "keepLinks": true,
  "maxChars": 0
}
```

**Stock / availability**

```json
{
  "watchId": "gpu-stock",
  "urls": ["https://shop.example.com/p/gpu-4090"],
  "fields": { "price": ".price | /([\\d.,]+)/", "buyButton": "button.add-to-cart", "availability": "[itemprop=availability]@href" },
  "maxChars": 0,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

**Just read 20 pages as clean Markdown (stateless)**

```json
{ "task": "fetch", "urls": ["https://…", "https://…"], "maxChars": 8000, "outputFormat": "markdown" }
```

***

### Make it recurring

You have three patterns — pick by who should wake up.

**1. The agent calls it on its own cadence.** Same input, same `watchId`, whenever you want. State lives on Apify, so the agent needs no memory of the previous page.

**2. An Apify Schedule calls it; the agent reads results when convenient.** Create a schedule (cron) with this exact input. Each run writes a dataset + `OUTPUT`; read the latest run with `get-actor-run` / `get-dataset-items`, or via API:

```http
POST https://api.apify.com/v2/schedules
Authorization: Bearer <APIFY_TOKEN>
Content-Type: application/json

{
  "name": "competitor-pricing-daily",
  "cronExpression": "0 6 * * *",
  "timezone": "UTC",
  "isEnabled": true,
  "actions": [{
    "type": "RUN_ACTOR",
    "actorId": "YOUR_USERNAME/recurring-web-check",
    "runInput": { "body": "{\"watchId\":\"competitor-pricing\",\"urls\":[\"https://competitor-a.com/pricing\"],\"selector\":\"main\",\"maxChars\":0}", "contentType": "application/json; charset=utf-8" }
  }]
}
```

**3. A schedule calls it and *only changes* wake the agent.** Add `webhookUrl` to the input (your agent's inbound endpoint, Zapier/Make/n8n, Slack…). The Actor POSTs only when `totals.changed > 0`. Quiet days cost nothing but the run.

***

### How to call it

**MCP (Apify MCP server, `https://mcp.apify.com`)**

```text
call-actor  actorId="YOUR_USERNAME/recurring-web-check"
            input={"urls":["https://example.com/pricing"],"watchId":"example-pricing","maxChars":0}
```

The tool returns the dataset items. For large batches use `get-actor-run` + `get-dataset-items`, or read the `OUTPUT` record with `get-key-value-store-record`.

**HTTP (sync, returns dataset items directly)**

```http
POST https://api.apify.com/v2/acts/YOUR_USERNAME~recurring-web-check/run-sync-get-dataset-items?token=<APIFY_TOKEN>
Content-Type: application/json

{ "urls": ["https://example.com/pricing"], "watchId": "example-pricing" }
```

**JavaScript**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('YOUR_USERNAME/recurring-web-check').call({
  urls: ['https://example.com/pricing'], watchId: 'example-pricing', maxChars: 0,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const changed = items.filter((i) => i.changed);
```

**Python**

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("YOUR_USERNAME/recurring-web-check").call(run_input={
    "urls": ["https://example.com/pricing"], "watchId": "example-pricing", "maxChars": 0})
items = client.dataset(run["defaultDatasetId"]).list_items().items
changed = [i for i in items if i["changed"]]
```

***

### For agent builders: drop-in tool instructions

Paste into your agent's system prompt or tool description:

```text
Tool: recurring-web-check (Apify Actor YOUR_USERNAME/recurring-web-check)
Use when the user wants to check, watch, monitor, track or re-read web pages, prices,
docs, changelogs, status pages, job boards or JSON endpoints — especially on a schedule.
- Put ALL URLs of one job into `urls` (one call, not one per URL).
- Give every recurring job a stable `watchId`; reuse it on every later call.
- Prefer `maxChars: 0` + `fields` for monitoring; read `content` only when needed.
- After a run: if totals.changed == 0 report "no change"; else summarise `diff` and
  `fieldChanges` for the URLs in `changedUrls`. `ok:false` means the page is down/gone.
- To automate, create an Apify Schedule with the same input, optionally with `webhookUrl`.
```

***

### Pricing, limits, non-goals

- **Pay per result.** One result = one checked URL = one dataset item (also for failed URLs — a 404 is a result), plus Apify's tiny Actor-start fee. Run cost is therefore predictable: `urls.length × price per result`. Cap spend with `maxTotalChargeUsd` on the run as usual. No browser is started, so a run with many URLs stays cheap and fast.
- **Storage:** one small record per URL per `watchId` in a named key-value store (`recurring-check-<watchId>`). Delete that store to reset a watch.
- **Baseline rule:** the stored snapshot advances only on first sight or when a change is reported, so drift below `changeThreshold` accumulates instead of being silently absorbed.
- **Not for:** pages that need JavaScript rendering (use a browser-based Actor for those, then feed its text here if you still want diffs), logged-in content, CAPTCHAs, or extracting with natural-language instructions (this Actor is deterministic on purpose).

***

### FAQ

**Does it work without a watchId?** Yes. One is derived from the URL set + selector + fields. Identical repeat calls still get change detection. Give an explicit `watchId` when the URL list may grow over time.

**Why is `changed` true although the page looks the same?** Something in the compared text differs: a timestamp, a rotating banner, a counter. Scope with `selector`, remove it with `ignoreSelectors`/`ignorePatterns`, or set `changeThreshold: 0.02`. Inspect `diff` to see exactly what moved.

**Can I monitor JSON APIs?** Yes — JSON bodies are pretty-printed with sorted keys (so key order never counts as a change) and `json:` field specs extract values.

**What about redirects?** Followed; `finalUrl` tells you where you ended up. The snapshot key is the input URL.

**How do I reset a watch?** Delete the key-value store `recurring-check-<watchId>` (Console → Storage, or `DELETE /v2/key-value-stores/<id>`), or start using a new `watchId`.

**Run time?** Typically 2–8 s for 10 HTML URLs on the default settings; no browser is started.

# Actor input Schema

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

Absolute URLs to check in this run. Put ALL URLs of one recurring job here — one run handles many URLs, you do not need one run per URL. HTML pages, JSON/REST endpoints, RSS/Atom feeds and plain-text files all work.

## `task` (type: `string`):

monitor (default): remembers each URL per watchId and reports changed=true with a compact diff when the page or any field changed since the last run. fetch: stateless, returns content + fields. extract: stateless, returns only the fields (tiny output).

## `watchId` (type: `string`):

Stable name for a recurring job, e.g. competitor-pricing or docs-acme-api. Snapshots are stored under this ID, so every later run with the same watchId diffs against the previous one. Leave empty and an ID is derived from the URL set + selector + fields, so identical repeat calls still get change detection.

## `fields` (type: `object`):

Optional map of field name → extraction spec. Specs: 'h1' (text of first match), 'a.buy@href' (attribute), '.price | /(\[\d.,]+)/' (regex on the text, first capture group), 'all:.item .name' (array of all matches), 'json:data.items\[0].price' (path into a JSON response). Changed field values are reported in fieldChanges with before/after. No LLM involved — deterministic and free of hallucination.

## `selector` (type: `string`):

Optional CSS selector that limits the content to one part of the page, e.g. 'main', '#pricing', 'article'. Smaller scope = fewer tokens and fewer false 'changed' flags. If it matches nothing, the whole body is used and scopeMatched=false is returned.

## `ignoreSelectors` (type: `array`):

Elements to remove before comparing, e.g. ads, comments, timestamps, counters, 'related posts'. Use this to stop noisy page parts from triggering changed=true.

## `ignorePatterns` (type: `array`):

Regular expressions removed from the text before storing and comparing, e.g. clock times, '5 minutes ago', session IDs. Plain pattern or /pattern/flags.

## `outputFormat` (type: `string`):

markdown keeps headings/lists/tables so an LLM can still see structure; text is flatter and a bit smaller.

## `maxChars` (type: `integer`):

Token budget: page content returned per URL is cut at this many characters (truncated=true tells you when). Set 0 to omit content entirely and only receive fields + diff. Change detection always uses the FULL content, not the truncated one.

## `diffMode` (type: `string`):

compact returns a short +/- line diff of what changed; none returns only the change flags and field changes.

## `maxDiffChars` (type: `integer`):

Upper bound for the diff text per URL.

## `changeThreshold` (type: `number`):

Minimum share of changed characters (0–1) before content counts as changed. 0 = any difference. 0.02 ignores tiny edits like a rotated testimonial. Field changes always count regardless of this threshold.

## `stripBoilerplate` (type: `boolean`):

Remove nav, header, footer, aside, forms and cookie banners from the content (ignored when a scope selector is set).

## `keepLinks` (type: `boolean`):

By default links are reduced to their text (URLs with tracking parameters change on every load and inflate output). Enable to keep [text](url).

## `webhookUrl` (type: `string`):

Optional. When at least one URL changed, a JSON payload {event, watchId, runId, changed:\[...]} is POSTed here — handy for scheduled runs that should wake up an agent only when something happened.

## `proxyConfiguration` (type: `object`):

Optional. Leave empty for direct requests. Enable Apify Proxy for sites that block datacenter IPs.

## `maxConcurrency` (type: `integer`):

Parallel requests. Lower it for sites with strict rate limits.

## `timeoutSecs` (type: `integer`):

Per-URL timeout. Keep runs fast: an MCP call-actor waits at most 45 s for the whole run.

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

Retries for network errors, HTTP 429 and 5xx. 4xx responses are reported immediately as ok=false (a 404 is a meaningful monitoring result).

## Actor input object example

```json
{
  "urls": [
    "https://example.com/pricing",
    "https://example.com/changelog"
  ],
  "task": "monitor",
  "watchId": "competitor-pricing",
  "fields": {
    "price": ".price | /([\\d.,]+)/",
    "title": "h1",
    "docsLink": "a.docs@href",
    "plans": "all:.plan .name",
    "apiVersion": "json:data.version"
  },
  "ignoreSelectors": [
    ".ads",
    "#comments",
    "time",
    ".view-count"
  ],
  "ignorePatterns": [
    "\\d{1,2}:\\d{2}(:\\d{2})?",
    "\\d+ (seconds|minutes|hours) ago"
  ],
  "outputFormat": "markdown",
  "maxChars": 6000,
  "diffMode": "compact",
  "maxDiffChars": 3000,
  "changeThreshold": 0,
  "stripBoilerplate": true,
  "keepLinks": false,
  "maxConcurrency": 5,
  "timeoutSecs": 20,
  "maxRetries": 1
}
```

# Actor output Schema

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

Dataset items, one per URL: ok, status, title, fields, changed, changeRatio, diff, fieldChanges, content (truncated at maxChars), contentHash, error.

## `changes` (type: `string`):

Same dataset in the compact 'changes' view: url, changed, changeRatio, fieldChanges, diff, previousFetchedAt, fetchedAt — read this first after a monitor run and look at rows with changed=true.

## `summary` (type: `string`):

JSON record with watchId, totals {urls, ok, failed, firstRun, changed, unchanged}, changedUrls, failedUrls, compact results and nextStep.

# 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": [
        "https://example.com/pricing",
        "https://example.com/changelog"
    ],
    "watchId": "competitor-pricing",
    "fields": {
        "price": ".price | /([\\d.,]+)/",
        "title": "h1",
        "docsLink": "a.docs@href",
        "plans": "all:.plan .name",
        "apiVersion": "json:data.version"
    },
    "ignoreSelectors": [
        ".ads",
        "#comments",
        "time",
        ".view-count"
    ],
    "ignorePatterns": [
        "\\d{1,2}:\\d{2}(:\\d{2})?",
        "\\d+ (seconds|minutes|hours) ago"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("vikingtrading/recurring-web-check").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": [
        "https://example.com/pricing",
        "https://example.com/changelog",
    ],
    "watchId": "competitor-pricing",
    "fields": {
        "price": ".price | /([\\d.,]+)/",
        "title": "h1",
        "docsLink": "a.docs@href",
        "plans": "all:.plan .name",
        "apiVersion": "json:data.version",
    },
    "ignoreSelectors": [
        ".ads",
        "#comments",
        "time",
        ".view-count",
    ],
    "ignorePatterns": [
        "\\d{1,2}:\\d{2}(:\\d{2})?",
        "\\d+ (seconds|minutes|hours) ago",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("vikingtrading/recurring-web-check").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).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": [
    "https://example.com/pricing",
    "https://example.com/changelog"
  ],
  "watchId": "competitor-pricing",
  "fields": {
    "price": ".price | /([\\\\d.,]+)/",
    "title": "h1",
    "docsLink": "a.docs@href",
    "plans": "all:.plan .name",
    "apiVersion": "json:data.version"
  },
  "ignoreSelectors": [
    ".ads",
    "#comments",
    "time",
    ".view-count"
  ],
  "ignorePatterns": [
    "\\\\d{1,2}:\\\\d{2}(:\\\\d{2})?",
    "\\\\d+ (seconds|minutes|hours) ago"
  ]
}' |
apify call vikingtrading/recurring-web-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vikingtrading/recurring-web-check"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/CJlYW4Hqp0C2XuJ59/builds/MgKUdnh6KmcRmes7k/openapi.json
