# Isocast — Full Agent Reference (v1.1.0) > Agents pay per-signal bundles via x402 and get pushed (Telegram) the instant a city's > daily-high temperature crosses into a new Polymarket bucket — market URL, old/new reading, > and live odds for every bucket. 37 cities. This is the extended reference: every endpoint > with real-shaped JSON, the entitlement model, the full error catalogue, and integration notes. > Compact version: /llms.txt · Topic deep-dive: /llms/weather-signals · OpenAPI: /openapi.json ## Base URL & discovery surface - API base: https://api.isocast.dev - /llms.txt (compact) · /llms-full.txt (this) · /llms/weather-signals (semantics) - /discovery (JSON enumeration) · /openapi.json (OpenAPI 3.1) · /api (human quickstart) - /.well-known/x402 (+ /.well-known/x402.json) · /.well-known/agent-card.json (A2A) - /robots.txt · /sitemap.xml · Terms: https://isocast.dev/terms - Payment network: eip155:8453 (Base mainnet) · Asset: USDC · Scheme: x402 `exact` (EIP-3009) Every JSON **object** response carries a top-level `disclaimer` string (omitted from arrays/ primitives). It is informational only, not financial or betting advice. =============================================================================== FREE ENDPOINTS (no auth, no payment) =============================================================================== ## GET /health Liveness + configured payment mode + sanctions-screening status. ```json { "ok": true, "service": "isocast-api", "mode": "live", "ofac": { "listLoaded": true, "extraConfigured": false, "count": 12000 }, "ts": "2026-07-26T12:00:00.000Z", "disclaimer": "…" } ``` `mode` is `live` or `mock`. In `live` mode a real non-zero settlement wallet is required. ## GET /v1/cities All active cities (30s server cache). ```json { "count": 37, "cities": [ { "slug": "istanbul", "displayName": "Istanbul", "country": "TR", "unit": "C", "bucketWidth": 1, "timezone": "Europe/Istanbul", "resolutionSource": "wu", "station": "LTBA", "latestSeq": 41 } ], "disclaimer": "…" } ``` ## GET /v1/cities/:slug One city plus today's city-local market day and the Polymarket event URL. ```json { "city": { "slug": "istanbul", "displayName": "Istanbul", "country": "TR", "unit": "C", "bucketWidth": 1, "timezone": "Europe/Istanbul", "resolutionSource": "wu", "station": "LTBA", "latestSeq": 41 }, "targetDate": "2026-07-26", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-july-26-2026", "disclaimer": "…" } ``` 404 → `{ "error": "unknown city", "disclaimer": "…" }`. ## GET /v1/sample?city=SLUG Promo signal ONLY: the fixed `seq: 1` signal (never the latest), so agents can inspect the shape before paying. If no real seq-1 exists it falls back to a synthetic Toronto example (`synthetic: true`). Always marked `sample: true`. This is the canonical Isocast signal shape — identical to the objects inside /v1/signals `signals[]`, minus the sample/synthetic flags. Real seq-1 example (Istanbul): ```json { "city": "istanbul", "displayName": "Istanbul", "seq": 1, "targetDate": "2026-06-01", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-june-1-2026", "conditionId": "0x9a1c…f3", "old": { "reading": 26.7, "unit": "C", "bucket": "26-27°C" }, "new": { "reading": 27.3, "unit": "C", "bucket": "27-28°C" }, "buckets": [ { "label": "26-27°C", "conditionId": "0x…26", "yes": 0.04, "no": 0.96, "midpoint": 0.04, "dead": true }, { "label": "27-28°C", "conditionId": "0x…27", "yes": 0.48, "no": 0.52, "midpoint": 0.48 }, { "label": "28-29°C", "conditionId": "0x…28", "yes": 0.33, "no": 0.67, "midpoint": 0.33 }, { "label": "29°C or higher", "conditionId": "0x…29", "yes": 0.15, "no": 0.85, "midpoint": 0.15 } ], "bucketsAsOf": "2026-06-01T14:03:07.000Z", "source": "wu", "observedAt": "2026-06-01T14:03:00.000Z", "sample": true, "disclaimer": "…" } ``` ## GET /v1/signals/meta?city=SLUG Pricing + payment metadata. NEVER returns signal rows. Without `?city` you get the base pricing block; with `?city` it additionally echoes `city` + `latestSeq`. ```json { "city": "istanbul", "latestSeq": 41, "unitPriceUsd": 0.005, "minSpendUsd": 0.01, "currency": "USDC", "network": "eip155:8453", "payTo": "0x…", "tiers": [ { "name": "Min", "minCount": 2, "unitPriceUsd": 0.005, "totalUsd": 0.01 }, { "name": "Starter", "minCount": 20, "unitPriceUsd": 0.005, "totalUsd": 0.1 }, { "name": "Standard", "minCount": 100, "unitPriceUsd": 0.0045, "totalUsd": 0.45 }, { "name": "Pro", "minCount": 500, "unitPriceUsd": 0.004, "totalUsd": 2 }, { "name": "Whale", "minCount": 2000, "unitPriceUsd": 0.0035, "totalUsd": 7 } ], "disclaimer": "…" } ``` ## GET /terms.txt · GET /terms.json The Terms of Service (v0.1 draft) as text/plain and JSON respectively. Both set an `X-Terms-URL: https://isocast.dev/terms` response header. /terms.json: ```json { "version": "0.1-draft", "effectiveDate": "2026-07-26", "url": "https://isocast.dev/terms", "summary": ["Isocast provides informational weather signals only …", "…"], "disclaimer": "…" } ``` ## POST /v1/delivery/telegram (FREE, wallet-signed — NEVER charges) Registers/changes/deletes the Telegram chat that receives your entitled signals. This path never touches entitlement — changing chat_id is free. Authorisation is an EIP-712 signature that must recover to `wallet`. ### Getting a chat_id Bot: **@IsoCast_bot** (https://t.me/IsoCast_bot). Open the bot and send `/start` — it replies with your `chat_id`. For a group, add @IsoCast_bot as a member and send `/id`; for a channel, add it as an admin with "post messages" permission (it posts the `chat_id` on join, or send `/id`). Telegram will not let a bot message a user who has not pressed Start, so do that before registering. `chat_id` formats: private = positive integer, group = negative integer (supergroups start with -100…), and a public channel's @handle is also accepted. The bot's reply looks like: ``` Your Telegram chat_id is 123456789. Register it (free, wallet-signed) with POST https://api.isocast.dev/v1/delivery/telegram — see https://isocast.dev/docs#telegram {"chatId":"123456789","type":"private"} ``` EIP-712: - Domain: `{ "name": "Isocast", "version": "1", "chainId": 8453 }` - primaryType: `TelegramDelivery` - Types.TelegramDelivery (order matters): `[{wallet:address}, {chatId:string}, {citySlug:string}, {action:string}, {timestamp:uint256}]` Request body: ```json { "wallet": "0xAbC…", "chatId": "123456789", "citySlug": "istanbul", "action": "set", "signature": "0x…", "timestamp": 1751545200 } ``` Rules: `timestamp` = unix seconds, must be within 600s of server time; `action` is `set` or `delete`; `chatId` required for `set`; empty `citySlug` = all cities (stored as null). On `set` a Telegram test message is sent to the chat BEFORE anything persists — if it fails you get 502 and nothing is stored. Success: ```json { "ok": true, "wallet": "0xabc…", "chatId": "123456789", "citySlug": "istanbul", "action": "set", "disclaimer": "…" } ``` Errors: 400 (invalid wallet / bad action / missing chatId|signature|timestamp), 401 (stale timestamp | signature verification failed), 404 (unknown city | nothing to delete), 502 (Telegram test send failed). Later fatal Telegram errors (blocked bot / removed from chat) deactivate the delivery row — re-register with a new `set`. =============================================================================== PAID PLANE (x402, USDC on Base — network eip155:8453) =============================================================================== There are two payment shapes on this product: - SPOT (one signal, stock x402, flat $0.01): `GET /v1/spot?city=SLUG` - SUBSCRIPTION (prepaid per-city bundles, binding nonce): `POST /v1/subscribe` + `GET /v1/signals` ## GET /v1/spot?city=SLUG (stock x402 — single latest-signal snapshot, $0.01) A SPOT read: a snapshot of the city's single LATEST observed signal, like a market-data quote. Unlike the subscription plane, it carries NO binding nonce, so a STOCK x402 client (plain `@x402/fetch` `wrapFetchWithPayment`) can pay it directly. It goes through the shared compute-first-settle-after handler: - unknown / missing `city` → **400** `{ "error": "unknown city — see GET /v1/cities" }`, NOT charged; - city exists but has no signal yet → **404** `{ "error": "no signals observed yet for this city" }`, NOT charged; - otherwise settle, then deliver EXACTLY ONE signal (the latest). It NEVER returns history, a feed, or any signal other than the most-recent one. This is a deliberate, bounded exception to the subscription plane's post-payment-only rule (a subscription only ever delivers signals emitted AFTER you paid): /v1/spot sells a single snapshot of the current latest, and nothing more. Success (200): ```json { "city": "istanbul", "displayName": "Istanbul", "unit": "C", "latest": { "city": "istanbul", "displayName": "Istanbul", "seq": 41, "targetDate": "2026-07-26", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-july-26-2026", "conditionId": "0x…", "old": { "reading": 29.4, "unit": "C", "bucket": "28-29°C" }, "new": { "reading": 30.1, "unit": "C", "bucket": "29-30°C" }, "buckets": [ … ], "bucketsAsOf": "…", "source": "wu", "observedAt": "…" }, "latestSeq": 41, "observedAt": "2026-07-26T13:22:00.000Z", "charged": true, "payment": { "txHash": "0x…", "network": "…", "priceUsd": 0.01, "payer": "0x…" }, "disclaimer": "…" } ``` A bare/unpaid `GET /v1/spot?city=SLUG` (or HEAD) returns the standard 402 challenge with `{ x402Version, accepts }` and NO `extra.binding` block (there is no nonce to bind). ## Subscription plane Signals are also sold in prepaid per-city bundles. The two subscription routes: - `POST /v1/subscribe?city=SLUG&count=N` — buy the next N signals - `GET /v1/signals?city=SLUG&since=S&count=N` — read what you're entitled to ## The 402 challenge (PaymentRequired) Any unpaid paid-route request returns 402 with: ```json { "x402Version": 2, "error": "Payment required", "resource": { "url": "…/v1/subscribe?city=istanbul&count=20", "description": "Isocast signal bundle purchase.", "mimeType": "application/json" }, "accepts": [ { "scheme": "exact", "network": "eip155:8453", "amount": "100000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x…", "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2" } } ], "extra": { "binding": { "formula": "keccak256(abi.encodePacked(payer, citySlug, uint256 count, bytes32 salt))", "citySlug": "istanbul", "count": 20, "salt": "0x7f3a…" } }, "terms": "https://isocast.dev/terms", "disclaimer": "…" } ``` ## Anti-grief nonce binding (VERBATIM) The EIP-3009 authorization `nonce` you sign MUST be: salt = // server-derived, keccak256(secret|citySlug|count); NOT client-computable nonce = keccak256(abi.encodePacked(payer, citySlug, uint256 count, bytes32 salt)) This binds the signed authorization to (payer, citySlug, count) so a replayer cannot re-point it at a different city or count. The server recomputes the expected nonce from the recovered payer and the requested (city, count) and REJECTS any mismatch before settling (`402 payment binding mismatch: nonce must bind (payer, city, count)`). Because the salt is derived from a server secret and published only inside the 402, a compliant client simply reads `extra.binding.salt` and plugs it into the formula. ## Settle-early flow (server side) verify (recover payer + nonce) → OFAC screen (listed → 403, never settle) → nonce-binding check (mismatch → 402, never settle) → settle → record exactly one payment → apply entitlement in an all-or-nothing DB tx. If the entitlement tx fails AFTER settlement, a refund is queued and you get 502 `{ refund: "queued" }`. Identity is ALWAYS the payer recovered from the payment — never a client-claimed field; client payment headers are never forwarded or logged. ## POST /v1/subscribe?city=istanbul&count=20 → 200 ```json { "ok": true, "city": "istanbul", "count": 20, "unitPriceUsd": 0.005, "amountUsd": 0.1, "latestSeq": 41, "paidThroughSeq": 61, "receiptToken": "eyJ…", "txHash": "0x…", "idempotent": false, "disclaimer": "…" } ``` `receiptToken` lets you read /v1/signals without re-paying. Re-submitting the same settlement (same txHash) is idempotent: `idempotent: true`, nothing re-applied, current state returned. ## The entitlement model (paidThroughSeq) - Each (wallet, city) has one entitlement with a `paidThroughSeq` ceiling. - A purchase of `count` sets `paidThroughSeq = max(current paidThroughSeq, city.latestSeq) + count`. Buying when you're already caught up (current ≥ latest) simply extends the ceiling by `count`; buying after falling behind first snaps the floor up to `latestSeq` so you always pay for the NEXT `count` unseen signals — you never pay for a backlog you skipped past. - You are entitled to a signal iff its `seq ≤ paidThroughSeq`. No expiry; entitlement is permanent. - Top-ups accumulate: repeated purchases keep pushing `paidThroughSeq` higher; `totalPaidSignals` tracks the lifetime count. ## GET /v1/signals?city=istanbul&since=41 (Authorization: Bearer ) → 200 Auth is EITHER a Bearer receipt token from a prior subscribe, OR an inline x402 payment header (which runs the full subscribe flow first, then reads). Returns signals with `since < seq ≤ paidThroughSeq`, ascending, capped at 100 per page. Never returns `seq > paidThroughSeq`. ```json { "signals": [ { "city": "istanbul", "displayName": "Istanbul", "seq": 42, "targetDate": "2026-07-26", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-july-26-2026", "conditionId": "0x…", "old": { "reading": 29.4, "unit": "C", "bucket": "28-29°C" }, "new": { "reading": 30.1, "unit": "C", "bucket": "29-30°C" }, "buckets": [ { "label": "28-29°C", "conditionId": "0x…", "yes": 0.02, "no": 0.98, "midpoint": 0.02, "dead": true }, { "label": "29-30°C", "conditionId": "0x…", "yes": 0.55, "no": 0.45, "midpoint": 0.55 }, { "label": "30-31°C", "conditionId": "0x…", "yes": 0.30, "no": 0.70, "midpoint": 0.30 }, { "label": "31°C or higher", "conditionId": "0x…", "yes": 0.08, "no": 0.92, "midpoint": 0.08 } ], "bucketsAsOf": "2026-07-26T13:22:05.000Z", "source": "wu", "observedAt": "2026-07-26T13:22:00.000Z" } ], "next_cursor": 61, "paid_through": 61, "latest": 62, "more": false, "disclaimer": "…" } ``` Page forward by passing `next_cursor` as the next `since`. When `rows === 100` the server checks for a further entitled row to set `more: true`. ## 402-on-exhaustion When `since ≥ paidThroughSeq` (nothing new you've paid for), /v1/signals returns 402 with the normal challenge PLUS `paid_through` and `latest`, so you know exactly how many more to buy: ```json { "x402Version": 2, "resource": { … }, "accepts": [ … ], "extra": { "binding": { … } }, "terms": "https://isocast.dev/terms", "paid_through": 61, "latest": 62, "disclaimer": "…" } ``` ## Backfill / replay Signals persist and never expire; re-read any entitled range by setting `since` lower (e.g. `since=0` replays from seq 1 up to `paidThroughSeq`, 100/page via `next_cursor`). `/v1/sample` always returns seq-1 for shape inspection without spending. =============================================================================== ERROR CATALOGUE =============================================================================== - 400 `count must be an integer of at least 2` — subscribe count < 2 or non-integer (has `minSpendUsd`) - 400 `minimum spend is 0.01 USDC` — bundle total below the $0.01 floor (has `minSpendUsd`) - 400 (delivery) — invalid wallet / bad action / missing chatId|signature|timestamp - 401 (delivery) — `stale timestamp` (>600s skew) or `signature verification failed` - 402 `payment verification failed — you were not charged` — could not recover payer - 402 `payment binding mismatch: nonce must bind (payer, city, count)` — wrong/missing nonce - 402 `settlement failed — you were not charged` — settle rejected (has `reason`) - 402 (exhaustion) — /v1/signals when `since ≥ paid_through` (has `paid_through`, `latest`) - 403 `wallet is sanctioned; payment refused` — OFAC hit, never settled - 404 `unknown city` — unknown/inactive slug - 502 `entitlement update failed after payment` — charged; `{ refund: "queued" }` - 503 `sanctions screening unavailable` — fail-closed; nothing settled ## Rate limits No per-key rate limit on the paid plane — x402 settlement is the access control. Free routes are rate-limited per client IP (60s fixed window), emitting standard `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` headers; over-limit → 429: - GET /v1/cities (and /v1/cities/:slug) — 30/min - GET /v1/sample — 10/min - GET /v1/signals/meta — 30/min - POST /v1/delivery/telegram — 10/min - Docs & discovery surfaces (/llms.txt, /llms-full.txt, /llms/weather-signals, /openapi.json, /discovery, /.well-known/*, /terms.txt, /terms.json, /api, /robots.txt, /sitemap.xml, /) — 60/min /health is not rate-limited. Free endpoints are cheap reads (with a 30s cache on /v1/cities); behave politely (poll /v1/cities and /v1/signals/meta rather than hammering). Use a city's `latestSeq` (free) to decide when to buy. ## Freshness caveat `bucketsAsOf` is the timestamp the bucket odds were sampled — it trails `observedAt` by at most ~10s. Treat `buckets` odds as a near-real-time snapshot at `bucketsAsOf`, not a live quote; a `dead: true` bucket is one the day's reading can no longer reach. ## Changelog - **v1.1.0** — Added `GET /v1/spot?city=SLUG`: a stock-x402 single-signal SPOT snapshot ($0.01, no binding nonce) returning the city's latest observed signal. Additive — no routes moved, removed, or repriced; the subscription plane (/v1/subscribe, /v1/signals) is unchanged. - **v1.0.0** — Initial public release: free browse plane, x402 bundle subscription (/v1/subscribe + /v1/signals), and free EIP-712 Telegram delivery registration. ## Disclaimer Informational data only; not financial or betting advice. No guarantee of accuracy or outcome. User assumes all risk. Prediction markets are restricted or prohibited in some jurisdictions — you are solely responsible for local compliance. Terms: https://isocast.dev/terms