x402-price-watch
Pay-per-poll price watching. Every check returns a fresh snapshot plus the delta against the cursor you sent — so a single cent buys you a complete, self-contained answer to “what changed since last time?”
USDC on BaseUSDC on SolanaHTTP 402Apache-2.0CoinGecko (live, keyless)Amadeusstateless cursors
Pay on either rail — your client picks. Every 402 challenge lists USDC on Base and USDC on Solana at the same price.
git clone https://github.com/nirholas/x402-price-watch && cd x402-price-watch
npm install && cp .env.example .env # pre-filled — runs with no edits
npm run dev # http://localhost:4044
Pricing
| Route | Price | Returns |
|---|---|---|
GET /check | $0.002 | Signed snapshot + delta vs cursor + trend + next cursor |
GET /watch-key | free | The normalized watch key your parameters map to |
GET /sources | free | Which price sources are live here |
POST /verify | free | Signature check for any check artifact |
GET /healthz | free | Liveness + accepted rails |
Payment rails
USDC on Base
EIP-3009 transferWithAuthorization, signed in the wallet. Nothing on-chain
until the facilitator settles.
- network
base-sepolia(default) ·base- payTo
0x40252CFDF8B20Ed757D61ff157719F33Ec332402- facilitator
https://x402.org/facilitator
USDC on Solana
Fee-sponsored SPL transferChecked. The facilitator's feePayer
covers the SOL fee — you need only USDC.
- network
solana(default) ·solana-devnet- payTo
WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW- facilitator
https://facilitator.payai.network
A rail with no configured address is dropped from the challenge instead of failing the request, so a single-rail deployment is a one-line change.
How a paid call works
The cursor is the whole idea
A subscription price-watcher needs an account, a database of your watches, and a retention policy. This one needs none of that, because the client carries the state. Each response hands back an opaque, HMAC-signed cursor encoding the prices you just saw:
$ curl "…/check?market=crypto&ids=bitcoin,ethereum" # first poll
{ "snapshot": { "items": [ { "id": "bitcoin", "priceUsd": 64266 } ] },
"delta": { "hasCursor": false, "cursorStatus": "none" },
"trend": { "direction": "down", "basis": "provider_24h", "pct": -0.11 },
"cursor": "eyJrIjoiY3J5cHRvOmJpdGNvaW4…" }
$ curl "…/check?market=crypto&ids=bitcoin,ethereum&cursor=eyJrIjoi…" # next poll
{ "delta": { "hasCursor": true, "cursorStatus": "applied", "elapsedSeconds": 3600,
"changes": [ { "id": "bitcoin", "previousUsd": 64266, "currentUsd": 64980,
"absUsd": 714, "pct": 1.111, "direction": "up" } ],
"summary": { "up": 1, "down": 1, "biggestMoverId": "bitcoin" } },
"trend": { "direction": "up", "basis": "cursor", "pct": 0.62 } }
Cursors are signed, so a client cannot forge a “previous price”, and scoped to their
watch key, so a bitcoin cursor can never be compared against a JFK→LAX snapshot. A mismatched or
tampered cursor is reported in delta.cursorStatus rather than failing the
call — you still get the snapshot you paid for.
Why pay-per-poll
Watching is bursty. You check a fare twice a day for a fortnight and then never again; you check a token every minute for an hour during a move. Subscriptions get that wrong in both directions — you pay through the quiet weeks and get rate-limited in the busy hour. At $0.002 a poll an agent can watch as often as the question deserves and stop the moment it doesn’t.
Sources
| Market | Provider | Live | Notes |
|---|---|---|---|
| crypto | CoinGecko /simple/price | always — keyless | Also returns each coin’s own 24h move, used for the trend on a first poll |
| flight | Amadeus /v2/shopping/flight-offers | with free credentials | Tracks cheapest fare and cheapest nonstop separately — they move independently |
Without Amadeus credentials, flight polls return deterministic fixtures that drift in
ten-minute buckets, labeled source: "fixture" and live: false — enough
to build a client against, never mistakable for a real quote.