PageWatch
Extract prices, lists and tables from public web pages and get change alerts via RSS, ICS or webhooks. Free tier plus x402 pay-per-call.
Hosted MCP Server
npx add-mcp 'https://pagewatch.erikssonanton2001.workers.dev/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
PageWatch: static-page extraction (price, list, table, events) + change monitoring on Cloudflare Workers, with MCP and x402
- JavaScript 100%
Turn public web pages into data, and get told when they change.
PageWatch runs as a single Cloudflare Worker on the free plan (D1 for storage, one cron trigger). It reads a page, extracts what you ask for (a price, a list of items, a table, a calendar of events, a content hash), and can repeat that on a schedule and report changes through an RSS feed, a JSON event log, an ICS calendar or a signed webhook.
Static HTML and JSON only. There is no JavaScript rendering, no login, no cookies. Pages that build their content in the browser will return little or nothing. JSON-LD, Open Graph and microdata are read.
What people use it for
- Watch a product page and get a message when the price drops (price mode, webhook or RSS).
- Turn a club's event page into a calendar you can subscribe to (events mode, ICS).
- Keep a list of job postings or releases and see what was added or removed (list mode).
- Notice when a competitor changes their pricing or landing page (page mode: title, headings, content hash).
- Let an agent or an n8n/Zapier flow call one URL and get JSON back.
Quick start
One-off extraction (free, 30 per day per client):
curl -G 'https://pagewatch.erikssonanton2001.workers.dev/v1/extract' \
--data-urlencode 'url=https://shop.example/product/1' -d mode=price
A list as CSV:
curl -G 'https://pagewatch.erikssonanton2001.workers.dev/v1/extract' \
--data-urlencode 'url=https://example.org/jobs' -d mode=list \
--data-urlencode 'selector=li.job' \
--data-urlencode 'fields={"title":"h3","link":"a@href"}' -d format=csv
Watch a page (free: checks at most every 60 minutes, 3 active monitors per client, 30 days):
curl -X POST 'https://pagewatch.erikssonanton2001.workers.dev/v1/monitors' \
-H 'content-type: application/json' \
-d '{"url":"https://shop.example/product/1","mode":"price","interval_minutes":360,
"webhook_url":"https://hooks.example/pagewatch"}'
The response contains the monitor id, a manage_token and (with a webhook) a webhook_secret. Both are shown once. Read changes at /m/<id>/feed.rss, /m/<id>/events.json, /m/<id>/latest.json and, for events mode, /m/<id>/calendar.ics. Anyone who knows the id can read these, so the id is 128 bits of randomness.
Manage a monitor with the token:
curl -X POST 'https://pagewatch.erikssonanton2001.workers.dev/v1/monitors/<id>/check-now' -H 'Authorization: Bearer <manage_token>'
curl -X POST 'https://pagewatch.erikssonanton2001.workers.dev/v1/monitors/<id>/renew' -H 'Authorization: Bearer <manage_token>'
curl -X DELETE 'https://pagewatch.erikssonanton2001.workers.dev/v1/monitors/<id>' -H 'Authorization: Bearer <manage_token>'
check-now is limited to once per 10 minutes per monitor.
Extraction modes
| Mode | Needs | Returns |
|---|---|---|
| text | selector | text of the first match (or all with all=true) |
| attr | selector, attr | attribute value |
| list | selector, fields | array of objects; field values are css or css@attr |
| table | optional selector, tableIndex | headers and rows (CSV supported) |
| price | optional selector | price, currency, availability, name, source (JSON-LD, Open Graph, microdata, text) |
| events | none | JSON-LD events (JSON, CSV or ICS) |
| page | none | title, description, canonical, h1, word count, content hash |
| links | optional match, sameHost | absolute links with text |
| json | optional jsonpath | value at a JSONPath-lite expression such as $.a.b[0].c |
Selectors support tag, class, id, attribute operators, descendant and child combinators, comma lists, :first-child, :nth-child(n) and :not(simple). Monitors default to mode page.
Webhooks
One POST per change with JSON {monitor_id, url, kind, old, new, at, summary} and the header X-PageWatch-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body keyed with the monitor's webhook_secret. Webhook URLs must be https and pass the same SSRF checks as page URLs. The timeout is 5 seconds, redirects are not followed, and a failed delivery is retried on later cron runs, three attempts in total. After that the monitor is marked webhook_failed and stops sending until it is renewed. The RSS feed and events.json stay the source of truth. A broken event (monitoring stopped after 10 failed checks) is delivered the same way.
Limits
| What | Limit |
|---|---|
| Page size | 600 KB per page, larger pages are rejected (422) |
| Free extractions | 30 per day per client, then 429 with instructions for the paid endpoint |
| Free monitors | 3 active per client, 30 days (renewable), interval 60 minutes or more (default 360) |
| Pro monitor | 30 days, interval from 15 minutes (default 15), up to 5 webhook URLs, no per-client cap |
| Global capacity | 400 active monitors (MAX_ACTIVE) |
| Politeness | robots.txt obeyed (cached 1 hour); scheduled checks fetch a host at most once per 15 minutes |
| Requests | http/https, ports 80/443, public DNS names only; no credentials, no IP literals |
| Loop guard | this service, *.workers.dev and *.pages.dev are refused as targets and webhooks |
| Input | selector up to 300 characters, up to 20 fields, JSON bodies up to 8 KB |
| Failures | 3 failures in a row slow a monitor down (doubling, up to 24 h); 10 stop it with a broken event |
| Cron | every 15 minutes, up to 20 monitors per run within 45 outbound requests and 45 D1 statements |
Paid endpoints (x402)
Two endpoints are paid per call in USDC on Base with x402. No account, no API key. An unpaid request returns HTTP 402 with a PAYMENT-REQUIRED header. Payment is settled only when the response status is below 400, so validation errors, robots.txt blocks and upstream failures are free.
| Endpoint | Price |
|---|---|
GET /v1/pro/extract | $0.002, no daily limit |
POST /v1/pro/monitors | $0.25, 30 days, 15 minute checks, up to 5 webhooks |
OpenAPI with x-payment-info: /openapi.json. Listing: /.well-known/x402. Agent docs: /llms.txt.
MCP
POST /mcp speaks MCP over streamable HTTP (JSON-RPC 2.0). Tools: extract_page, watch_page, get_changes. The first two follow the free-tier rules; with paid: true they return the x402 payment requirements of the paid endpoint (or run the call when a PAYMENT-SIGNATURE header is sent).
Privacy
PageWatch stores each monitor's configuration, the latest extracted value and a change log, until the monitor is deleted or has been expired for 30 days. IP addresses are never stored: quotas use a salted SHA-256 hash that rotates daily, and the per-client monitor cap uses a salted hash of the creating IP (set the SALT secret; without it a fixed fallback salt is used and the hashes are only as private as that is). The manage token is stored only as a SHA-256 hash. The webhook secret is stored so deliveries can be signed. Do not monitor pages whose content you cannot share: feeds are public to anyone with the id.
Honest limits
- No JavaScript rendering, no login or paywalled pages, no bot-protection bypass.
- Checks are best effort on a free plan. A cron run handles about a dozen monitors, so with many due monitors some checks are late (they are picked up by the next run).
- Selectors break when a site changes. Monitors that keep failing slow down and eventually stop.
- The free plan allows few D1 statements per invocation; the cron job counts its own and stops early.
- The robots.txt parser follows RFC 9309 for Allow/Disallow, wildcards and longest match; it ignores Crawl-delay.
- A webhook or target on a public name that resolves to a private address cannot be detected from inside a Worker; Cloudflare does not route Worker fetches to private ranges, which is the actual protection there.
- The per-client cap is checked before insert, not in a transaction, so a burst of parallel requests can slightly exceed it.
Development
npm test # builds, then runs node --test (no network; fakes for fetch, clock, database, facilitator)
npm run build # dist/worker.js, fails if the bundle imports node: modules
npm run make-deploy # writes deploy-fn.js (one async function for the Cloudflare MCP execute tool)
node scripts/migrate-sql.mjs # prints schema.sql
Layout: src/index.js (wiring), src/routes/*, src/service/* (monitor lifecycle, checks, cron, webhooks, quotas), src/db/* (D1 adapter and an in-memory twin used in tests), src/extract/* (pure extraction engine), src/lib/* (SSRF guard, bounded fetch, robots.txt), src/docs.js and src/paid-routes.js (route tables).
Environment: PAY_TO, NETWORK, FACILITATOR_URL, PUBLIC_BASE_URL, optional MAX_ACTIVE and SALT; D1 binding DB.
MIT licensed.