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:

EndpointWhat it streams
/stream/prediction-marketsFull 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) or EventSource (Node.js/browser).
  • The server sends a connected event confirming the connection is live.
  • The server pushes a snapshot event — a complete order book for one market — every time any
    book in the category changes.
  • Periodic ping events 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

ParameterTypeRequiredDescription
keystringYes*Your API key (alternative to X-Api-Key header).
categorystringYesCategory 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:

FieldDescription
typeAlways snapshot.
entry_idSequence identifier. Currently empty ("") — this stream has no replay/resume (see Reconnecting).
dataThe 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:

FieldDescription
market_idOpticOdds market id, formatted <platform>:<source_market_id>. Use as your cache key.
platformSource platform: kalshi or polymarket.
source_market_idThe platform's native market id.
source_event_idThe platform's native event id; groups related markets under one event.
outcomesMap of outcome token → per-outcome book. Binary markets expose yes and no.
timestamp_nsTime the book was captured, as a Unix timestamp in nanoseconds.
categoryThe category this market belongs to (matches your category filter).
canonical_idOpticOdds canonical event id for cross-platform matching. Empty ("") when unmatched.

Each entry in outcomes (e.g. outcomes.yes):

FieldDescription
token_idOutcome/token identifier (echoes the outcome key, e.g. yes / no).
best_bidHighest bid price, 01 (dollars per contract ≈ implied probability). 0 when no bids.
best_askLowest ask price, 01. 0 when no asks.
spreadbest_ask − best_bid.
last_trade_pricePrice of the most recent trade; 0 if none available.
tick_sizeMinimum price increment; 0 when not provided by the source.
bidsBid levels, ordered best (highest) first.
asksAsk levels, ordered best (lowest) first.

Each level in bids / asks:

FieldDescription
pricePrice for the level, typically 0.010.99.
sizeQuantity 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)

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)

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 snapshot is authoritative and complete for its market_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/snapshot traffic 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-events endpoint
    for cross-platform strategies.

Did this page help you?