PageWatch
Trích xuất giá cả, danh sách và bảng từ các trang web công khai và nhận cảnh báo thay đổi qua RSS, ICS hoặc webhooks. Gói miễn phí cộng thêm x402 trả phí theo mỗi lần gọi.
Máy chủ MCP được lưu trữ
npx add-mcp 'https://pagewatch.erikssonanton2001.workers.dev/mcp'Cài vào Claude Code, Codex, Cursor và nhiều công cụ khác
Tài liệu
PageWatch: static-page extraction (price, list, table, events) + change monitoring on Cloudflare Workers, with MCP and x402
- JavaScript 100%
Zenith Research Desk e5ab3a050a Initial public release | 2026-10-18 18:43:43 +02:00 | |
| registry | Initial public release | 2026-10-18 18:43:43 +02:00 |
| scripts | Initial public release | 2026-10-18 18:43:43 +02:00 |
| shims | Initial public release | 2026-10-18 18:43:43 +02:00 |
| src | Initial public release | 2026-10-18 18:43:43 +02:00 |
| test | Initial public release | 2026-10-18 18:43:43 +02:00 |
| testlib | Initial public release | 2026-10-18 18:43:43 +02:00 |
| .gitignore | Initial public release | 2026-10-18 18:43:43 +02:00 |
| LICENSE | Initial public release | 2026-10-18 18:43:43 +02:00 |
| package-lock.json | Initial public release | 2026-10-18 18:43:43 +02:00 |
| package.json | Initial public release | 2026-10-18 18:43:43 +02:00 |
| README.md | Initial public release | 2026-10-18 18:43:43 +02:00 |
| schema.sql | Initial public release | 2026-10-18 18:43:43 +02:00 |
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.zenith-ops.workers.dev/v1/extract' \
--data-urlencode 'url=https://shop.example/product/1' -d mode=price
A list as CSV:
curl -G 'https://pagewatch.zenith-ops.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.zenith-ops.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.zenith-ops.workers.dev/v1/monitors/<id>/check-now' -H 'Authorization: Bearer <manage_token>'
curl -X POST 'https://pagewatch.zenith-ops.workers.dev/v1/monitors/<id>/renew' -H 'Authorization: Bearer <manage_token>'
curl -X DELETE 'https://pagewatch.zenith-ops.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.