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%

moneyman-code 9c7dce63fe x402: build payment middleware per request (cached init promise hung across requests in workerd)

2026-10-17 02:10:02 +02:00
registryPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
scriptsPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
shimsPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
srcx402: build payment middleware per request (cached init promise hung across requests in workerd)2026-10-17 02:10:02 +02:00
testPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
testlibPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
.gitignorePageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
LICENSEPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
package-lock.jsonPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
package.jsonPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
README.mdPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +02:00
schema.sqlPageWatch: page-to-data extraction and change monitoring (Cloudflare Worker, MCP, x402)2026-10-17 01:12:17 +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.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

ModeNeedsReturns
textselectortext of the first match (or all with all=true)
attrselector, attrattribute value
listselector, fieldsarray of objects; field values are css or css@attr
tableoptional selector, tableIndexheaders and rows (CSV supported)
priceoptional selectorprice, currency, availability, name, source (JSON-LD, Open Graph, microdata, text)
eventsnoneJSON-LD events (JSON, CSV or ICS)
pagenonetitle, description, canonical, h1, word count, content hash
linksoptional match, sameHostabsolute links with text
jsonoptional jsonpathvalue 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

WhatLimit
Page size600 KB per page, larger pages are rejected (422)
Free extractions30 per day per client, then 429 with instructions for the paid endpoint
Free monitors3 active per client, 30 days (renewable), interval 60 minutes or more (default 360)
Pro monitor30 days, interval from 15 minutes (default 15), up to 5 webhook URLs, no per-client cap
Global capacity400 active monitors (MAX_ACTIVE)
Politenessrobots.txt obeyed (cached 1 hour); scheduled checks fetch a host at most once per 15 minutes
Requestshttp/https, ports 80/443, public DNS names only; no credentials, no IP literals
Loop guardthis service, *.workers.dev and *.pages.dev are refused as targets and webhooks
Inputselector up to 300 characters, up to 20 fields, JSON bodies up to 8 KB
Failures3 failures in a row slow a monitor down (doubling, up to 24 h); 10 stop it with a broken event
Cronevery 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.

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