Keeping a local Pinnacle odds book over WebSocket in Python (snapshot + deltas)
Most odds feeds are built for polling: you ask for the whole board every few seconds and diff it yourself. A push feed works the other way round, and the client code gets simpler once you treat it as a tiny state machine. This post walks through that pattern for Pinnacle prices using PinnFeed, a WebSocket and REST API I work on (disclosure up front). The model: snapshot, then deltas After you connect and subscribe, the server sends three kinds of data messages: welcome, with your plan limits. A chunked snapshot of every event and market that matches your filters, split into parts of up to 400 events. The last part has "final": true. A stream of delta batches. Each change is either upsert (replace the object) or remove (the market was suspended or settled, or the event ended). There is also a heartbeat: the server sends ping every 20 seconds and closes connections that stay silent for 60 seconds, so you answer with pong. That means the whole client is: keep two dictionaries (events by id, markets by id), apply upserts and removes, and don't trust the book until the snapshot says final. A minimal local book in Python # pip install websockets import asyncio, json, os, websockets events, markets = {}, {} book_ready = False def apply_change(c): store = markets if c["kind"] == "market" else events obj = c.get("market") or c.get("event") if c["op"] == "upsert": store[obj["id"]] = obj elif c["op"] == "remove": store.pop(obj["id"] if obj else c.get("id"), None) async def main(): global book_ready async with websockets.connect( "wss://pinnfeed.io/v1/stream", additional_headers={"x-api-key": os.environ["PINNFEED_KEY"]}, compression="deflate", ) as ws: await ws.send(json.dumps({ "type": "subscribe", "sports": ["soccer"], "markets": ["moneyline", "total"], "live": True, })) async for raw in ws: msg = json.loads(raw) t = msg["type"] if t == "ping": await ws.send('{"type":"pong"}') elif t == "snapshot": for e in msg.get("events", []): events[e["id"]] = e for m in msg.get("markets", []): markets[m["id"]] = m book_ready = msg.get("final", False) elif t == "delta" and book_ready: for c in msg["changes"]: apply_change(c) asyncio.run(main()) A few notes from writing this: Wait for final: true. If you act on the first snapshot part you will think most markets are missing. Sending subscribe again replaces the subscription and triggers a fresh snapshot, so reset your dictionaries when you change filters. Ask for compression. The compression="deflate" option negotiates permessage-deflate, which matters when you subscribe to whole sports. Every outcome already carries a no-vig fair price next to the decimal and American prices, so you don't have to remove the margin yourself before comparing against another book. Polling is fine for prototypes If you only want to see which prices are moving, the REST side has a GET /v1/drops endpoint that returns the biggest shortening per outcome over a rolling window (1 to 60 minutes, with a minimum drop percentage): curl -H "x-api-key: $PINNFEED_KEY" \ "https://pinnfeed.io/v1/drops?sport=soccer&live=true&minutes=10&min_pct=5" On the free plan REST responses are delayed by 30 seconds, which is enough to build and test the logic before you need a live stream. Where to go next The full message reference (subscribe fields, snapshot and delta shapes, raw frames): PinnFeed WebSocket protocol docs A complete small project that turns drops into Telegram alerts: pinnfeed/dropping-odds-telegram on GitHub PinnFeed is not affiliated with Pinnacle. If you use odds data for betting, it is for adults (18+) only, and no data feed guarantees a profit.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to