# Isocast — Full Agent Reference (v1.0.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-03T12: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-03", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-july-3-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-03", "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`. 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). =============================================================================== PAID PLANE (x402, USDC on Base — network eip155:8453) =============================================================================== Signals are sold in prepaid per-city bundles. The two 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": 1, "accepts": [ { "scheme": "exact", "network": "eip155:8453", "price": "$0.1", "payTo": "0x…", "description": "Isocast: next 20 istanbul weather signals", "mimeType": "application/json" } ], "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-03", "marketUrl": "https://polymarket.com/event/highest-temperature-in-istanbul-on-july-3-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-03T13:22:05.000Z", "source": "wu", "observedAt": "2026-07-03T13: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": 1, "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 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. ## 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