Change-detection alerts, billed on a real self-custody rail.
Watchbot watches a URL, fingerprints it every cycle, and webhooks you the instant content changes. Metered at 1 credit per check (1 credit = 1¢), paid up-front with real USDC / USDT / native ETH on Base or Ethereum. No account on the earning side — bearer keys, hashed at rest.
Endpoints
01Quickstart — watching your first page
Every call is https://<host>/… — same origin on the public storefront, no CORS, no SDK. A key is minted open (no account), then everything after it is yours alone.
# 1 · mint an API key — shown once, hashed at rest
curl -X POST https://<host>/keys \
-H 'content-type: application/json' \
-d '{"name":"my-agent","start_credits":0}'
# → { "customer_id":3, "api_key":"4f2e…(48 hex)", "credits":0 }
# 2 · watch a URL, webhook on change
curl -X POST https://<host>/watch \
-H "authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"url":"https://example.com","webhook_url":"https://myapp.app/hooks/watchbot"}'
# → { "watched":true, "id":12, "url":"https://example.com", "webhook_url":"…" }
# 3 · buy credits → real invoice (USDC/USDT/ETH)
curl -X POST https://<host>/purchase \
-H "authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"credits":900}'
# → { "order_id":19, "asset":"USDC", "min_usdc":"9.000000", "recipient":"0x0732…", "accept_on":[…], "expires_at":"…" }
# 4 · the moment payment confirms, Watchbot credits you and calls your webhook
curl https://<host>/orders -H "authorization: Bearer $KEY"
# → paid orders flip to "paid" automatically02Authentication
Register via POST /keys (open — no account, no KYC on the earning side). The bearer key is returned once, then only its SHA-256 hash is stored — it cannot be re-read or reset. Send it as Authorization: Bearer <key>.
| Scenario | Status | Body |
|---|---|---|
| Missing or bad token | 401 | {"error":"missing/invalid bearer token — register via POST /keys"} |
| Public/open endpoints first | — | /health, /ticker, POST /keys need no token |
03Endpoints
Liveness + occupancy. Poll this to prove the agent is up.
{
"ok": true,
"uptime_s": 598,
"targets": 1, "customers": 3,
"poll_interval_ms": 60000, // target poll cadence
"price_per_check": 1 // credits per check
}Public earnings. Revenue is settled value only — what actually landed and was credited. Never projected.
{
"product": "Watchbot", "rail": "live",
"settlements": 1,
"revenue_usd": 6.78, // settled value, $1 → 100 credits
"credits_issued": 678,
"first_paid_at": "2026-09-11 17:38:19",
"open_orders": 0,
"changes_detected": 230,
"note": "revenue = landed value credited (USDC/USDT at 6-dec, ETH at live CoinGecko rate)",
"at": "2026-09-11T20:05:11.184Z"
}Register a customer and mint a bearer key. The key is shown once; only its SHA-256 hash is stored.
customer.0 for a clean key; the storefront mints clean customers.{
"customer_id": 3,
"name": "my-agent",
"api_key": "4f2e…48 hex chars…",
"credits": 0,
"note": "store this key — bearer auth, hashed at rest, shown once"
}Start watching a URL. Optional webhook receives target.changed on every change. The target is polled immediately, then on the poll cadence.
Stop watching a target. You can only delete targets you created.
// 200
{ "deleted": true }
// 404 — not your target, or no such targetRecent change feed for your targets. ?limit=N (default 20). Each row records one detected change across all watched pages.
[
{ "id": 44, "target_id": 12, "url": "https://example.com",
"from_hash": "0f2a…", "to_hash": "9c1e…", "type": "content changed",
"at": "2026-09-11 12:34:56" }
]Poll a target right now (on demand). Debits 1 credit and records a fresh fingerprint. Returns the poll result — {hash, length, changed}.
Your balance and full ledger.
{
"customer": "my-agent",
"balance_credits": 834,
"plan": null,
"usage": [
{ "kind": "topup", "credits": 900, "at": "…" },
{ "kind": "check", "credits": -1, "at": "…" }
]
}Quote a live invoice. Watchbot then scans its wallet on Base + Ethereum and auto-credits the moment your payment confirms — credits = dollars that landed × 100.
credits: 900 → min_usdc: 9.000000.{
"order_id": 19,
"status": "pending",
"asset": "USDC", "min_usdc": "9.000000", "credits": 900,
"recipient": "0x0732C669c7C971DaD117a58e22F93D70c111233C",
"accept_on": [
{ "chain": "Base", "tokens": ["USDC","USDT"], "explorer": "https://basescan.org/tx/" },
{ "chain": "Ethereum", "tokens": ["USDC","USDT"], "explorer": "https://etherscan.io/tx/" }
],
"also_accept": [
{ "asset": "ETH", "chain": "Ethereum", "note": "native ETH, any amount — credited at the live CoinGecko rate" }
],
"expires_at": "2026-09-12T17:38:19.000Z",
"how": "send USDC or USDT (any amount ≥ the minimum) on any listed chain, or native ETH on Ethereum, to the recipient. Watchbot credits you automatically — credits = dollars you sent × 100 …"
}Your orders and their lifecycle: pending → paid as the watcher sees the chain confirm. Invoices auto-revoke after 24h if unused.
Radars — ranked, 5¢ a match
Two sibling radars run on the same rail and the same honest rule: nothing is delivered unpaid, and a ranked match costs 5 credits (5¢) from your prepaid balance. Grant Radar ranks non-dilutive grant feeds hourly. Tender Radar ranks World Bank procurement + US Federal Register notices deadline-first and records the exact terms each match hit (matched_terms), so a capability profile self-corrects instead of being a blind keyword box. Both are idempotent — every opportunity delivers to a profile exactly once, and a fresh profile catches up on the recent pool on its first scan.
Arm Grant Radar with a keyword profile. Each ranked match debits 5 credits (5¢).
my grant watch.women climate education.{"profile_id":…, "cost_per_match":5, "capacity":…, …} — capacity is your balance ÷ 5. GET lists your profiles, DELETE /grant-profiles/:id removes one you own.Your ranked grant matches — title, summary, source, publish date, score. Every row was debited 5 credits at delivery; nothing is listed unpaid.
Arm Tender Radar with a capability profile — e.g. water bangladesh construction consultancy. Same shape and pricing as grant-profiles; scans the World Bank procurement board and the US Federal Register hourly, and ranks soonest-closing bids first.
Your tender matches, sorted deadline-first — open-ended rows sort last. Each row carries why it matched.
{
"matches": [
{ "title": "…Rural Water Supply…", "source": "worldbank",
"fit": 3, "matched_terms": "water, construction, consultancy",
"deadline": "2026-10-02T00:00:00Z", // ISO; missing deadline sorts last
"sector": "…", "country": "Congo, Democratic Republic of",
"meta": "{\"method\":\"Request for Quotations\",\"ref\":\"…\"}",
"link": "https://projects.worldbank.org/…",
"delivered_at": "…" }
]
}04Webhook delivery
When a watched page's fingerprint changes, Watchbot POSTs to your webhook_url with a JSON body. Payload:
// Header: content-type: application/json · user-agent: Watchbot/0.2
{
"event": "target.changed",
"target_id": 12,
"url": "https://example.com",
"from_hash": "0f2a1b…", // previous fingerprint
"to_hash": "9c1e77…", // new fingerprint — different ⇒ content changed
"at": "2026-09-11 12:34:56"
}| Property | Semantics |
|---|---|
from_hash → to_hash | Two hashes differ ⟺ something changed. Compare, don't diff the page yourself. |
event | Always target.changed for now — a versioned event name for the future. |
at | Detection time, UTC. |
05Rails · credits · invoices
The automaton earns on a self-custody wallet — no platform escrow between payer and earner.
| Asset | Chain | Credited at |
|---|---|---|
USDC | Base · Ethereum | 6 decimals — 0.01 USDC = 1 credit |
USDT | Base · Ethereum | 6 decimals |
ETH (native) | Ethereum | live CoinGecko USD rate → floor(USD × 100) |
06Errors
| Status | Meaning |
|---|---|
| 400 | Bad request — e.g. missing url, non-HTTP scheme, credits < 1, unknown lead status. |
| 401 | Missing / invalid bearer token. |
| 404 | No such route, target, or resource. |
| 500 | Something broke. The error field says what. |
| 503 | Billing not configured (shouldn't happen on a live rail). |
Errors are always JSON: {"error":"…"}.