# Isocast API

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.

- **Base URL**: https://api.isocast.dev
- **Website**: https://isocast.dev
- **Docs (agent-facing)**: https://api.isocast.dev/llms.txt · https://api.isocast.dev/llms-full.txt · https://api.isocast.dev/llms/weather-signals
- **OpenAPI 3.1**: https://api.isocast.dev/openapi.json
- **Discovery (JSON)**: https://api.isocast.dev/discovery
- **x402 manifest**: https://api.isocast.dev/.well-known/x402
- **Terms**: https://isocast.dev/terms

Informational data only; not financial or betting advice.

## What it is

Each covered city has a daily Polymarket "highest temperature" market whose outcomes are
temperature buckets. Isocast emits ONE signal the instant a city's daily-high crosses into a new
bucket — carrying the market URL + conditionId, the old→new reading/bucket, and live YES/NO/
midpoint odds for every bucket. Signals are sold per city in prepaid bundles via x402, or pushed
to Telegram. 37 cities.

## Free (no auth, no payment)

| Method | Path | What |
|--------|------|------|
| GET | /v1/cities | All active cities (latestSeq each) |
| GET | /v1/cities/:slug | One city + today's market URL |
| GET | /v1/sample?city=SLUG | Sample seq-1 signal (shape demo) |
| GET | /v1/signals/meta?city=SLUG | Pricing + tiers + payTo |
| GET | /terms.txt · /terms.json | Terms of Service |
| POST | /v1/delivery/telegram | Register Telegram push (EIP-712 signed, free) |

## Quickstart — buy a bundle (x402, USDC on Base)

```
# 1) Ask for a bundle with no payment → 402 challenge (note extra.binding.salt)
curl -s -X POST "https://api.isocast.dev/v1/subscribe?city=istanbul&count=20"
# → { "x402Version":1, "accepts":[{ "scheme":"exact","network":"eip155:8453",
#       "price":"$0.1","payTo":"0x…" }],
#     "extra":{ "binding":{ "formula":"keccak256(abi.encodePacked(payer, citySlug, uint256 count, bytes32 salt))",
#       "citySlug":"istanbul","count":20,"salt":"0x…" } }, "terms":"https://isocast.dev/terms" }

# 2) Build an EIP-3009 exact USDC authorization on Base whose nonce =
#    keccak256(abi.encodePacked(payer, citySlug, uint256 count, bytes32 salt))
#    using the served salt, then retry with the X-Payment header:
curl -s -X POST "https://api.isocast.dev/v1/subscribe?city=istanbul&count=20" -H "X-Payment: <payload>"
# → { "ok":true, "count":20, "amountUsd":0.1, "paidThroughSeq":61, "receiptToken":"…" }

# 3) Pull entitled signals with the receipt (no re-pay):
curl -s "https://api.isocast.dev/v1/signals?city=istanbul&since=41" -H "Authorization: Bearer <receiptToken>"
# → { "signals":[ … seq 42..61 … ], "next_cursor":61, "paid_through":61, "latest":62, "more":false }
```

## TypeScript (@x402/fetch)

```ts
import { wrapFetchWithPayment } from '@x402/fetch';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });
const fetchWithPay = wrapFetchWithPayment(fetch, wallet);

// NOTE: Isocast binds (payer, citySlug, count) into the EIP-3009 nonce (see extra.binding in the
// 402). A stock x402 client that lets the facilitator pick a random nonce will be REJECTED with
// "payment binding mismatch". Use a client that reads extra.binding.salt and sets
//   nonce = keccak256(encodePacked(['address','string','uint256','bytes32'], [payer, citySlug, BigInt(count), salt]))
// (the isocast-mcp package does this for you), then:
const res = await fetchWithPay('https://api.isocast.dev/v1/subscribe?city=istanbul&count=20', { method: 'POST' });
const { receiptToken, paidThroughSeq } = await res.json();

const page = await fetch('https://api.isocast.dev/v1/signals?city=istanbul&since=0', {
  headers: { Authorization: `Bearer ${receiptToken}` },
}).then((r) => r.json());
```

## Python (requests + eth_account)

```python
import requests
from eth_account import Account
from eth_abi.packed import encode_packed
from eth_utils import keccak

API = "https://api.isocast.dev"
acct = Account.from_key(PRIVATE_KEY)

# 1) Fetch the 402 challenge
ch = requests.post(f"{API}/v1/subscribe", params={"city": "istanbul", "count": 20}).json()
b = ch["extra"]["binding"]
salt = bytes.fromhex(b["salt"][2:])

# 2) Compute the anti-grief nonce the server expects
nonce = keccak(encode_packed(
    ["address", "string", "uint256", "bytes32"],
    [acct.address, b["citySlug"], b["count"], salt],
))
# 3) Sign an EIP-3009 transferWithAuthorization for USDC on Base using THIS nonce,
#    assemble the x402 X-Payment header, then:
r = requests.post(f"{API}/v1/subscribe", params={"city": "istanbul", "count": 20},
                  headers={"X-Payment": payment_header})
tok = r.json()["receiptToken"]

# 4) Read entitled signals
sigs = requests.get(f"{API}/v1/signals", params={"city": "istanbul", "since": 0},
                    headers={"Authorization": f"Bearer {tok}"}).json()
```

## Telegram delivery (free)

POST /v1/delivery/telegram with an EIP-712 signature (domain `{name:'Isocast',version:'1',chainId:8453}`,
type `TelegramDelivery(address wallet,string chatId,string citySlug,string action,uint256 timestamp)`)
that recovers to `wallet`. Entitled signals for that wallet are then pushed to the chat.

## Links
- Docs: https://api.isocast.dev/llms.txt
- Full: https://api.isocast.dev/llms-full.txt
- Signal semantics: https://api.isocast.dev/llms/weather-signals
- OpenAPI: https://api.isocast.dev/openapi.json
- Discovery: https://api.isocast.dev/discovery
- Agent card: https://api.isocast.dev/.well-known/agent-card.json
