Free trial
Start hereThe real feed, REST and WebSocket, on your own bot. Pay only if it earns its place.
No card. Sign up, then we switch it on.
Real-time odds feed for trading bots
Every Pinnacle price change, streamed over WebSocket the moment it happens. Sub-second delivery, every market, 13 sports.
| Event | Home | Draw | Away | Handicap | Total |
|---|---|---|---|---|---|
| 1.46 | 4.45 | 8.10 | −1 2.06 +1 1.80 | o1.5 1.94 u1.5 1.91 | |
| 2.31 | 3.08 | 3.62 | −0.25 2.02 +0.25 1.84 | o2.25 1.96 u2.25 1.89 | |
| 1.19 | No draw | 5.15 | −4.5 1.92 +4.5 1.93 | o21.5 1.88 u21.5 1.97 | |
| 1.42 | No draw | 2.98 | −6.5 1.93 +6.5 1.92 | o224.5 1.91 u224.5 1.94 | |
| 1.61 | No draw | 2.42 | −1.5 2.64 +1.5 1.51 | o5.5 1.95 u5.5 1.90 | |
| 1.86 | No draw | 1.98 | −1.5 1.92 +1.5 1.93 | o161.5 1.90 u161.5 1.95 |
Pinnacle → your bot · Europe
32ms
median 29 ms · 1.4 msg/s
Arsenal 1 to 0 Chelsea
Arsenal to win · last 60 s
The latest odds message for a moneyline, as the feed sends it. Prices are American odds. Demo data.
{"type": "odds", "data": {"type": "live", "sport_id": 1, "topic": "matchups/reg/sp/29/live/ld", "op": "upd", "rec": {"id": 1638780974, "markets": [{"key": "s;0;m", "version": 3724875515, "status": "open", "prices": [ { "designation": "home", "price": -217 }, { "designation": "draw", "price": 345 }, { "designation": "away", "price": 710 } ]}]}, "ts": 1792512311307}}
{"type": "odds", "data": {"type": "live", "sport_id": 1, "topic": "matchups/reg/sp/29/live/ld", "op": "upd", "rec": {"id": 1638780974, "markets": [{"key": "s;0;m", "version": 3724875515, "status": "open", "prices": [ { "designation": "home", "price": -217 }, { "designation": "draw", "price": 345 }, { "designation": "away", "price": 710 } ]}]}, "ts": 1792512311307}}
import json, websocketsURL = "wss://feed.oddspush.com/v1/stream" async def stream(token): async with websockets.connect(URL, compression=None) as ws: send = lambda m: ws.send(json.dumps(m)) await send({"type": "login", "token": token}) await ws.recv() # welcome await send({"type": "subscribe", "sports": [1]}) async for raw in ws: on_message(json.loads(raw)) # odds, snapshot, keepalive
import WebSocket from "ws";const URL = "wss://feed.oddspush.com/v1/stream"; const ws = new WebSocket(URL, { perMessageDeflate: false });const send = (m) => ws.send(JSON.stringify(m));ws.on("open", () => send({ type: "login", token: TOKEN }));ws.on("message", (raw) => { const { type, data } = JSON.parse(raw); if (type === "welcome") send({ type: "subscribe", sports: [1] }); if (type === "odds") onOdds(data);});
How it works
The moment Pinnacle changes a price, our relay picks it up and pushes it to you over WebSocket, exactly as published. No polling, no waiting for the next refresh.
Source
Pinnacle
Arsenal v Chelsea
Moneyline · Arsenal 2.10
Relay
OddsPush relay
1 in to 3 out, same bytes
Dublin +27 ms
Arsenal ML 2.10
London +30 ms
Arsenal ML 2.10
Frankfurt +33 ms
Arsenal ML 2.10
0ms Pinnacle publishes the price Every change, from a connection we keep alive for you.
Fan-out Received once, sent to every client Byte for byte as published: nothing rounded, nothing reordered.
~30 ms On your servers, in Europe Your own queue: a slow client never holds back the others.
Integration
Our docs come in a version written for AI coding agents. Download one file, hand it to Claude Code, Codex or Cursor, and it builds the client for you: login, subscription, a live price book and reconnects.
The link is our guide for coding agents: the whole protocol, the rules for a correct price book and two reference clients, in one Markdown file.
Read https://oddspush.com/oddspush-agent-guide.md and build a Python service that streams soccer and basketball odds into a live price book. My key is in ODDSPUSH_KEY.
Claude Code, Codex, Cursor or any agent that can open a link. Without web access, download the guide (76 KB) into your project. Also at /llms.txt.
The agent asks what the guide leaves to you, writes the client and runs the guide's checklist. Set
ODDSPUSH_KEY on the server whose IP is in your account, and start it.
Read https://oddspush.com/oddspush-agent-guide.md and build a Python service that streams soccer and basketball odds into a live price book. My key is in ODDSPUSH_KEY.
Read the guide
What should happen with each price?
Print the moneyline when it moves.
Wrote client.py and its tests
Logged in, snapshot of 1,255 events
Checklist passed, reconnect in 0.6 s
Running, about 11 updates a second:
# pip install websocketsimport asyncio, json, osimport websocketsURL = "wss://feed.oddspush.com/v1/stream"KEY = os.environ["ODDSPUSH_KEY"] # opk_…book = {} # event id -> {market key: market}def merge(rec): markets = book.setdefault(rec["id"], {}) for m in rec.get("markets") or []: k = m.get("key") old = markets.get(k, {}).get("version", -1) if k is None or m.get("version", 0) <= old: continue # stale or reordered: skip if m.get("status") == "open": markets[k] = m else: markets.pop(k, None) # closed: remove itasync def main(): # compression=None: deflate only adds latency async with websockets.connect(URL, compression=None, max_size=2**24) as ws: send = lambda msg: ws.send(json.dumps(msg)) await send({"type": "login", "token": KEY}) welcome = json.loads(await ws.recv()) if welcome["type"] == "error": raise SystemExit(welcome["code"]) await send({"type": "subscribe", "sports": [1], "channels": ["ld", "both"]}) async for raw in ws: msg = json.loads(raw) kind = msg["type"] if kind == "odds": data = msg["data"] rec = data.get("rec") or {} channel = str(data.get("topic")).rsplit("/")[-1] if data.get("op") == "del": book.pop(rec.get("id"), None) elif "id" in rec and channel in ("ld", "both"): merge(rec) # other channels are not prices elif kind == "snapshot": for rec in msg["events"]: merge(rec) elif kind == "missed" or msg.get("status") == "up": await send({"type": "snapshot"})asyncio.run(main())
// npm install wsimport WebSocket from "ws";const KEY = process.env.ODDSPUSH_KEY; // opk_…const book = new Map(); // event id -> Map(market key -> market)const ws = new WebSocket("wss://feed.oddspush.com/v1/stream", { perMessageDeflate: false, // deflate only adds latency});const send = (msg) => ws.send(JSON.stringify(msg));function merge(rec) { if (!book.has(rec.id)) book.set(rec.id, new Map()); const markets = book.get(rec.id); for (const m of rec.markets ?? []) { const old = markets.get(m.key)?.version ?? -1; // stale or reordered: skip if (m.key === undefined || (m.version ?? 0) <= old) continue; if (m.status === "open") markets.set(m.key, m); else markets.delete(m.key); // closed: remove it }}ws.on("open", () => send({ type: "login", token: KEY }));ws.on("message", (raw) => { const msg = JSON.parse(raw); if (msg.type === "welcome") { send({ type: "subscribe", sports: [1], channels: ["ld", "both"], }); } else if (msg.type === "error") { console.error(msg.code); } else if (msg.type === "odds") { const { op, rec = {}, topic = "" } = msg.data; if (op === "del") book.delete(rec.id); // other channels are not prices else if (rec.id && /\/(ld|both)$/.test(topic)) merge(rec); } else if (msg.type === "snapshot") { msg.events.forEach(merge); } else if (msg.type === "missed" || msg.status === "up") { send({ type: "snapshot" }); }});
# Log in within 15 s, from your registered IP. npm install -g wscat wscat -c wss://feed.oddspush.com/v1/streamConnected (press CTRL+C to quit) {"type":"login","token":"opk_…"} {"type":"welcome","feed":{"alive":true,"ready":true}, …} {"type":"subscribe","sports":[1],"channels":["ld","both"]} {"type":"subscription","filter":{"sports":[1], …}} {"type":"odds","data":{"type":"live","sport_id":1, …}} {"type":"snapshot","part":0,"final":false,"events":[ …
{"type": "odds", "data": { "type": "live", "sport_id": 1, "topic": "matchups/reg/sp/29/live/ld", "op": "upd", "rec": { "id": 1638780974, "type": "matchup", "startTime": "2026-10-20T16:00:00Z", "participants": [ {"name": "Lazio", "alignment": "home"}, {"name": "Roma", "alignment": "away"} ], "state": {"state": 1, "minutes": 31}, "markets": [ { "key": "s;0;m", "type": "moneyline", "period": 0, "version": 3772905262, "prices": [ {"designation": "home", "price": 145}, {"designation": "away", "price": 215}, {"designation": "draw", "price": 230} ], "isAlternate": false, "status": "open" }, … 41 more markets ] }, "ts": 1792512282138 }}
type names the message. data is the frame as published.
other.
events. On "op": "del" the event is gone: drop it.
0 is the full match, m the moneyline.
key. Equal to or lower than yours: stale, skip it.
230 is +230, or 3.30 decimal.
open means the market is closed: remove it.
Pricing
Pull the book over REST, or have every change pushed to you. All 13 sports on both. Run it on your own bot free for 7 days before you pay anything.
The real feed, REST and WebSocket, on your own bot. Pay only if it earns its place.
No card. Sign up, then we switch it on.
Pull the whole book whenever your model asks for it.
On request as fresh as you poll
$69 / month
$62
/ month
instead of
$69
$186 for 3 months, paid once Save 10%
$55.17
/ month
instead of
$69
$331 for 6 months, paid once Save 20%
Every price change pushed to you, milliseconds after Pinnacle.
~30 ms pushed to your bot
$138 / month
$124.33
/ month
instead of
$138
$373 for 3 months, paid once Save 10%
$110.33
/ month
instead of
$138
$662 for 6 months, paid once Save 20%
The same feed
Every plan and the trial get the same prices at the same moment. Nothing sampled.
Crypto, prepaid
USDC, USDT, BTC and more. No card, no auto-renewal: you renew when you choose.
Switch any time
Move between paid plans whenever you like: the days you have left carry over, pro rata.
One key per account, bound to one static IP. Every limit, plan by plan
FAQ
Still deciding?
Run it on your own bot for 7 days. If it does not earn its place, let it end.
Try it free for 7 daysOr ask us: support@oddspush.com
Sign up with your email and open the link we send: your key is on that page. Then email support@oddspush.com from that address and we switch the trial on. The 7 days count from that moment.
You get REST + WebSocket, all 13 sports and real prices, on your own bot. No card, nothing to cancel: when it ends, pick a plan or let it go. One trial per person.
REST ($69 a month): your bot asks for the book when it needs it. Enough for models that price before kick-off or check every few seconds.
REST + WebSocket ($138 a month): every change is pushed to you about ~30 ms after Pinnacle publishes it, and REST comes with it. If you trade on price moves, this is the one.
~30 ms from Pinnacle publishing a price to a bot in Europe. Our relay runs next to the source; from further away, add the network time between your server and Western Europe.
If every millisecond matters, host your bot in Europe.
Pinnacle's odds for 13 sports: moneyline, spreads, totals and the rest of each live market, plus the pre-match prices that travel on the live feed. Each message carries the original record byte for byte: nothing rounded, renamed or held back.
On connect you get a snapshot of the current book, then every change. Formats and examples are in the docs.
Each key works from one static IP, so a leaked key is useless to anyone else. Run your bot on a VPS or a cloud server with a fixed address; a home connection that changes IP will lock you out.
Set or change it in your account, up to 3 times a day. Your key stays the same.
In crypto, from your account: USDC, USDT, BTC or another coin, no card. You prepay 1, 3 or 6 months, and the longer terms cost less.
Nothing renews on its own, so there is nothing to cancel: you renew when you choose. Move between paid plans and the days you have left carry over.