Watchbot · http api · v0.2

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.

live below is real credits issued settlements changes → webhooks

Endpoints

GET/healthliveness + occupancy
GET/tickerpublic earnings (settled only)
POST/keysregister → bearer key (shown once)
POST/watchadd a URL to watch (auth)
DELETE/watch/:idstop watching, own targets
GET/changesrecent change feed (auth)
GET/changes/:idpoll a target on demand (1 credit)
GET/usagebalance + ledger (auth)
POST/purchasequote a live invoice (auth)
GET/ordersyour orders + status (auth)
POST/grant-profilesarm the funding radar (auth)
GET/grant-matchesranked grant matches (auth)
POST/tender-profilesarm the tender radar (auth)
GET/tender-matchesdeadline-first tender matches (auth)
start here

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" automatically
the rail

02Authentication

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

PRIVATEKeep the key server-side. Anyone holding it can spend your credits and add targets.
ScenarioStatusBody
Missing or bad token401{"error":"missing/invalid bearer token — register via POST /keys"}
Public/open endpoints first/health, /ticker, POST /keys need no token
reference

03Endpoints

GET/healthopen

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
}
GET/tickeropen

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"
}
POST/keysopen

Register a customer and mint a bearer key. The key is shown once; only its SHA-256 hash is stored.

namestringOptional label, ≤ 60 chars. Default customer.
start_creditsint ≥ 0Initial balance. 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"
}
POST/watchauth

Start watching a URL. Optional webhook receives target.changed on every change. The target is polled immediately, then on the poll cadence.

urlstringHTTP/HTTPS only. Must already be registering a well-formed target.
webhook_urlstringCalled on change. Optional.
201Each check costs 1 credit from your balance. Empty balance → checks queue but don't fire; top up to resume.
DELETE/watch/:idauth

Stop watching a target. You can only delete targets you created.

// 200
{ "deleted": true }
// 404 — not your target, or no such target
GET/changesauth

Recent 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" }
]
GET/changes/:idauth

Poll a target right now (on demand). Debits 1 credit and records a fresh fingerprint. Returns the poll result — {hash, length, changed}.

GET/usageauth

Your balance and full ledger.

{
  "customer": "my-agent",
  "balance_credits": 834,
  "plan": null,
  "usage": [
    { "kind": "topup",    "credits": 900,  "at": "…" },
    { "kind": "check",    "credits": -1,   "at": "…" }
  ]
}
POST/purchaseauth

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.

creditsint ≥ 11 credit = 1¢, so credits: 900min_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 …"
}
CREDITS MATHUSDC/USDT credited at 6 decimals; native ETH at the live CoinGecko USD rate, flooring to whole credits. If an exchange withdrawal fee shaves a few cents below the minimum, you still get credited for the landed amount.
GET/ordersauth

Your orders and their lifecycle: pendingpaid as the watcher sees the chain confirm. Invoices auto-revoke after 24h if unused.

funding & tender radars · products #02 · #03

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.

POST/grant-profilesauth

Arm Grant Radar with a keyword profile. Each ranked match debits 5 credits (5¢).

namestringLabel, ≤ 60 chars. Default my grant watch.
keywordsstringComma/space separated, ≤ 500 chars — e.g. women climate education.
201{"profile_id":…, "cost_per_match":5, "capacity":…, …}capacity is your balance ÷ 5. GET lists your profiles, DELETE /grant-profiles/:id removes one you own.
GET/grant-matchesauth

Your ranked grant matches — title, summary, source, publish date, score. Every row was debited 5 credits at delivery; nothing is listed unpaid.

POST/tender-profilesauth

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.

GET/tender-matchesauth

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": "…" }
  ]
}
push

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"
}
PropertySemantics
from_hashto_hashTwo hashes differ ⟺ something changed. Compare, don't diff the page yourself.
eventAlways target.changed for now — a versioned event name for the future.
atDetection time, UTC.
BEST-EFFORTDelivery is a single attempt with a 5s timeout — no retry queue yet. If your endpoint is down, that one change event is lost; the next detected change fires fresh. Respond 2xx to acknowledge.
billing

05Rails · credits · invoices

The automaton earns on a self-custody wallet — no platform escrow between payer and earner.

AssetChainCredited at
USDCBase · Ethereum6 decimals — 0.01 USDC = 1 credit
USDTBase · Ethereum6 decimals
ETH (native)Ethereumlive CoinGecko USD rate → floor(USD × 100)
ANY AMOUNTSend any amount ≥ the quoted minimum on any accepted chain — not just the exact figure. Landing $0.07 under the minimum still credits the landed value.
ops

06Errors

StatusMeaning
400Bad request — e.g. missing url, non-HTTP scheme, credits < 1, unknown lead status.
401Missing / invalid bearer token.
404No such route, target, or resource.
500Something broke. The error field says what.
503Billing not configured (shouldn't happen on a live rail).

Errors are always JSON: {"error":"…"}.