Watch
Watch a web page, a part of it, a JSON value or a price for weeks. Get the changes by polling, signed webhook or email. REST and MCP on one base URL: https://watch.darmousseh.com
Quick start
curl -H "Authorization: Bearer $KEY" https://watch.darmousseh.com/v1/ping
curl -X POST -H "Authorization: Bearer $KEY" -H "Idempotency-Key: demo-1" -H "content-type: application/json" \
-d '{"url":"https://example.com/product","selector":".price","condition":"below","threshold":50,"interval_min":60}' https://watch.darmousseh.com/v1/watches
curl -H "Authorization: Bearer $KEY" "https://watch.darmousseh.com/v1/changes?since=0"
Keys are long-lived Bearer tokens issued by the operator and revocable at any time. Requests never redirect.
What a watch can look at
| Field | Meaning |
|---|---|
url | Public http(s) page or JSON endpoint (required). No localhost, IPs or internal names. |
selector | CSS selector: compare only that part of an HTML page. |
json_path | Value inside a JSON response, e.g. data.items[0].price. Use this for public-records APIs. |
keyword | Alert when the text appears or disappears (case-insensitive). |
condition + threshold | Number alert: below/above. Fires when the first number found crosses the threshold (and again after it clears and recrosses). |
regex | Narrow the extracted text before comparing (first capture group). |
interval_min | 5-10080; free plan minimum 60. Checks run from a 5-minute scheduler, so allow up to 5 minutes of slack. |
webhook_url | https endpoint. POST JSON, header X-Watch-Signature: t=UNIX,v1=HEX where HEX = HMAC-SHA256(webhook_secret, t + "." + body). 2xx = delivered; otherwise retried after 1m, 5m, 30m, 2h, 6h, then marked failed. |
email | Address that gets one email per change from vsm-bot@agentmail.to (automated). |
name | Label used in change summaries. |
With no selector or json_path the whole visible page text is compared, so pages with clocks or rotating ads look "changed" every time; use a selector. The first check happens when you create the watch and sets the baseline (no change is recorded, except a threshold that is already met).
REST
| Call | Does |
|---|---|
GET /v1/ping | Harmless first read: key id, plan, limits. |
POST /v1/watches | Create a watch. Header Idempotency-Key makes retries safe (24h). 201 with the watch (including webhook_secret) and the baseline reading. |
GET /v1/watches?limit=10&cursor= | Your watches, newest first; next_cursor pages on. |
GET /v1/watches/{id} | One watch with last value, status and last error. |
DELETE /v1/watches/{id} | Stop a watch. Its changes stay readable. |
GET /v1/changes?since=CURSOR&watch_id=&limit=10 | With since: changes after that cursor, oldest first (poll loop: pass back the returned cursor). Without: the 10 newest; page back with before=older_cursor. |
Errors
Always JSON: {"error":{"code","message","retry"}}.
| Status | Code | Retry? |
|---|---|---|
| 400 | invalid_request, invalid_selector | no, fix the input |
| 401 | unauthorized | no |
| 403 | key_revoked, plan_limit | no |
| 404 | not_found (unknown watch or route) | no |
| 405 | method_not_allowed | no |
| 429 | rate_limited with Retry-After seconds | yes, after the delay |
| 500 | internal | yes, with the same Idempotency-Key |
MCP
Streamable HTTP endpoint https://watch.darmousseh.com/mcp, same Bearer key. Tools: ping, create-watch, list-watches, get-watch, delete-watch, list-changes. Results are JSON text; errors set isError and carry the same error object. Writes accept idempotency_key. Call ping first.
Plans
| Free | Pro (not on sale yet) | |
|---|---|---|
| Watches | 5 | 100 |
| Min interval | 60 min | 5 min |
| Requests | 30/min, 2,000/day | 120/min, 50,000/day |
| Emails | 10/day | 200/day |
| Price | $0 | $12/month planned |
Fetching, honestly
Pages are fetched from Cloudflare's network as WatchBot/1.0 with no JavaScript rendering. Pages that need JavaScript to show their content, or sites that block datacenter traffic, show up on the watch as last_error (for example HTTP 403 (site may be blocking automated fetches)); three failures in a row are recorded as a change of kind error. Change kinds: content, keyword_appeared, keyword_gone, threshold, threshold_cleared, error.