Prediction Markets Streaming Guide
Prediction Markets Streaming Guide (Non-Sport Order Book)
What you'll learn: How to connect to OpticOdds' Server-Sent Events (SSE) stream for
non-sport prediction markets, read the full real-time order book for a category, handle the
connected / ping / snapshot events, and build robust consumers in Python and Node.js.
What Is This Stream?
Instead of repeatedly polling REST endpoints, OpticOdds pushes the complete order book for
non-sport prediction markets over Server-Sent Events (SSE). You open one long-lived GET request
for a category (e.g. tech, politics) and receive a full order-book snapshot — top-of-book
prices plus full bid/ask depth — for every market in that category across every supported
platform (Kalshi, Polymarket), updated as the books move.
SSE is a standard HTTP protocol. You open a long-lived GET request, and the server sends events
down the connection as they happen. It's simpler than WebSockets (no handshake, no bidirectional
messaging) and works through proxies, load balancers, and firewalls without special configuration.
This is the streaming complement to the REST prediction-market endpoints
(/prediction-markets/categories, /prediction-markets/canonical-events). Use REST to discover
events and canonical matches; use this stream to watch the books.
Snapshot-only: Unlike /stream/odds/{sport}, this endpoint does not send incremental
deltas. Every message is a complete order book for a single market. To keep local state,
replace your cached book for that market_id on each message — never patch.
One streaming endpoint:
| Endpoint | What it streams |
|---|---|
/stream/prediction-markets | Full real-time order book (bids/asks) for every non-sport market in a category |
Stream Architecture
+--------------+ +--------------+
| Your App | GET (long-lived) -------------->| OpticOdds |
| (Consumer) |< events pushed to you ----------| SSE Server |
+--------------+ +--------------+
How it works:
- You open an HTTP GET request with
stream=True(Python) orEventSource(Node.js/browser). - The server sends a
connectedevent confirming the connection is live. - The server pushes a
snapshotevent — a complete order book for one market — every time any
book in the category changes. - Periodic
pingevents keep the connection alive. - If the connection drops, you reconnect; because every message is a full snapshot, your state
simply re-hydrates (no gap recovery needed).
Streaming Prediction Markets — /stream/prediction-markets
This endpoint delivers the full non-sport order book for a category as it changes.
URL
GET https://api.opticodds.com/api/v3/stream/prediction-markets
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes* | Your API key (alternative to X-Api-Key header). |
category | string | Yes | Category of markets to stream — one per connection. Open multiple connections for multiple categories. |
Valid category values come from GET /prediction-markets/categories (always pull the live
list rather than hardcoding): politics, economics, finance, crypto, tech, culture,
climate, health, geopolitics, companies, other. An omitted or unrecognized category
closes the connection.
Best Practice: Run one connection per category. Each connection is a firehose — a busy
category can emit thousands of snapshots per second — so consume on a dedicated thread/process
and hand messages off to an async worker or queue.
Event Types
connected — Connection Confirmed
Sent immediately when your stream opens successfully.
event: connected
retry: 5000
data: ok go
The retry: 5000 tells SSE clients to wait 5 seconds before auto-reconnecting if the connection drops.
ping — Keepalive
Sent about every 5 seconds to keep the connection alive and confirm the server is still streaming.
event: ping
retry: 5000
data: 2026-08-06T17:59:53Z
The timestamp tells you the server's current time — useful for detecting stale connections. If no
ping or snapshot arrives for ~15 seconds, drop the connection and reconnect.
snapshot — Full Order Book
Fired every time a market's book changes. The payload is the complete current order book for
one market — it replaces any prior state you hold for that market_id.
event: snapshot
retry: 5000
data: {"type":"snapshot","entry_id":"","data":{ ... }}
The message envelope:
| Field | Description |
|---|---|
type | Always snapshot. |
entry_id | Sequence identifier. Currently empty ("") — this stream has no replay/resume (see Reconnecting). |
data | The market order book (below). |
The data object:
{
"type": "snapshot",
"entry_id": "",
"data": {
"market_id": "kalshi:KXA100MAX-26DEC31-1.390",
"platform": "kalshi",
"source_market_id": "KXA100MAX-26DEC31-1.390",
"source_event_id": "KXA100MAX-26DEC31",
"outcomes": {
"yes": {
"token_id": "yes",
"best_bid": 0.63,
"best_ask": 0.68,
"spread": 0.05,
"last_trade_price": 0,
"tick_size": 0,
"bids": [
{ "price": 0.63, "size": 2 },
{ "price": 0.25, "size": 6.15 },
{ "price": 0.01, "size": 24000 }
],
"asks": [
{ "price": 0.68, "size": 4 },
{ "price": 0.99, "size": 26939.49 }
]
},
"no": {
"token_id": "no",
"best_bid": 0.32,
"best_ask": 0.37,
"spread": 0.05,
"last_trade_price": 0,
"tick_size": 0,
"bids": [ { "price": 0.32, "size": 4 } ],
"asks": [ { "price": 0.37, "size": 2 } ]
}
},
"timestamp_ns": 1786037945925000000,
"category": "tech",
"canonical_id": ""
}
}Key fields on each snapshot's data:
| Field | Description |
|---|---|
market_id | OpticOdds market id, formatted <platform>:<source_market_id>. Use as your cache key. |
platform | Source platform: kalshi or polymarket. |
source_market_id | The platform's native market id. |
source_event_id | The platform's native event id; groups related markets under one event. |
outcomes | Map of outcome token → per-outcome book. Binary markets expose yes and no. |
timestamp_ns | Time the book was captured, as a Unix timestamp in nanoseconds. |
category | The category this market belongs to (matches your category filter). |
canonical_id | OpticOdds canonical event id for cross-platform matching. Empty ("") when unmatched. |
Each entry in outcomes (e.g. outcomes.yes):
| Field | Description |
|---|---|
token_id | Outcome/token identifier (echoes the outcome key, e.g. yes / no). |
best_bid | Highest bid price, 0–1 (dollars per contract ≈ implied probability). 0 when no bids. |
best_ask | Lowest ask price, 0–1. 0 when no asks. |
spread | best_ask − best_bid. |
last_trade_price | Price of the most recent trade; 0 if none available. |
tick_size | Minimum price increment; 0 when not provided by the source. |
bids | Bid levels, ordered best (highest) first. |
asks | Ask levels, ordered best (lowest) first. |
Each level in bids / asks:
| Field | Description |
|---|---|
price | Price for the level, typically 0.01–0.99. |
size | Quantity available at that price (contracts/shares); may be fractional. |
Note: Field order within objects is not guaranteed — the same fields can appear in different
positions from message to message. Always parse by key, never by position.
Note: Contracts are priced from 0 to 1, so a price doubles as an implied probability. For
binary markets the yes and no books mirror each other — a yes bid at 0.63 corresponds to
a no ask at 0.37.
Reconnecting
Heartbeats. A ping arrives about every 5 seconds carrying the server's ISO-8601 UTC time.
Treat it as a liveness signal — reconnect if ping/snapshot traffic stalls for ~15 seconds.
Reconnect delay. Every event carries retry: 5000. Standard SSE/EventSource clients
reconnect automatically after 5 seconds; a hand-rolled loop should wait ~5s before retrying.
Important: This stream has no replay. entry_id is empty and there is no last_entry_id
parameter (unlike /stream/odds/{sport}). Because every message is a full snapshot,
reconnecting re-hydrates your state as fresh snapshots arrive — there are no missed deltas to
recover.
Code Examples
Python (sseclient-py)
sseclient-py)import json
import time
import requests
import sseclient # pip install sseclient-py
API_KEY = "YOUR_API_KEY"
CATEGORY = "tech"
URL = "https://api.opticodds.com/api/v3/stream/prediction-markets"
books = {} # market_id -> latest order book (full snapshot replaces prior state)
def stream():
resp = requests.get(
URL,
params={"category": CATEGORY, "key": API_KEY},
stream=True,
headers={"Accept": "text/event-stream"},
)
resp.raise_for_status()
for event in sseclient.SSEClient(resp).events():
if event.event == "connected":
print("connected")
elif event.event == "ping":
continue # heartbeat (~5s); event.data is an ISO-8601 timestamp
elif event.event == "snapshot":
book = json.loads(event.data)["data"]
books[book["market_id"]] = book
yes = book["outcomes"].get("yes", {})
print(book["market_id"], yes.get("best_bid"), yes.get("best_ask"))
if __name__ == "__main__":
while True:
try:
stream()
except Exception as e:
print("stream dropped, reconnecting:", e)
time.sleep(5) # honor the server's `retry: 5000`Node.js (eventsource)
eventsource)import EventSource from "eventsource"; // npm i eventsource
const API_KEY = "YOUR_API_KEY";
const CATEGORY = "tech";
const url =
`https://api.opticodds.com/api/v3/stream/prediction-markets` +
`?category=${CATEGORY}&key=${API_KEY}`;
const books = new Map(); // market_id -> latest order book
const es = new EventSource(url);
es.addEventListener("connected", () => console.log("connected"));
es.addEventListener("ping", () => { /* heartbeat ~every 5s */ });
es.addEventListener("snapshot", (e) => {
const { data: book } = JSON.parse(e.data);
books.set(book.market_id, book); // full snapshot replaces prior state
const yes = book.outcomes.yes ?? {};
console.log(book.market_id, yes.best_bid, yes.best_ask);
});
// EventSource auto-reconnects using the server's `retry: 5000` hint.
es.onerror = (err) => console.error("stream error, will auto-reconnect", err);Best Practices
- Replace, don't patch. Every
snapshotis authoritative and complete for itsmarket_id.
Overwrite your cached book; there are no deltas to merge. - One category per connection. Run a connection per category you need; each is independent.
- Parse by key. Field order within objects is not guaranteed.
- Watch the heartbeat. Reconnect if
ping/snapshottraffic stalls for ~15s. - Plan for volume. Decouple ingestion from processing; don't block the read loop.
- Join across platforms with
canonical_id. When populated, it links the same event on
Kalshi and Polymarket — pair it with the REST/prediction-markets/canonical-eventsendpoint
for cross-platform strategies.
Updated about 5 hours ago