BANGER
Product status: Banger’s public beta is paper-only. References to live execution are future-looking. Venue rules and legal claims can change; verify current primary sources for your jurisdiction.
2026-08-06 · 6 min read · facts as of 2026-08-06

Streaming Live Prediction-Market Data with WebSockets: Polymarket and Kalshi

Polling a REST order-book endpoint on a one-second loop is the wrong tool for a live strategy. It burns your rate budget and still lags the book. Both Polymarket and Kalshi push real-time updates over WebSocket instead. This post covers how to subscribe on each venue, how to keep a correct local book across reconnects, and how to hand clean events to a strategy loop. Code is Python.

Polymarket: the market channel

Polymarket's CLOB exposes WebSocket channels split by purpose. The 'market' channel is public and carries level-2 data. The 'user' channel tracks your own orders and trades and needs auth.

The public market channel serves orderbook snapshots, price changes, and trade executions with no auth required. You connect and subscribe with asset IDs.

The subscription gotcha that trips up most people: you subscribe with asset IDs, also called token IDs or CLOB token IDs, not a market ID or slug. These come from the clobTokenIds field in market metadata, and a binary YES/NO market usually has two of them.

# Polymarket market channel
import asyncio, json, websockets

WS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"

async def stream(token_ids):
    async with websockets.connect(WS) as ws:
        await ws.send(json.dumps({
            "assets_ids": token_ids,   # note: assets_ids, per the docs
            "type": "market",
            "custom_feature_enabled": True,
        }))
        # keepalive: send PING every 10s, ignore PONG replies
        async def ping():
            while True:
                await asyncio.sleep(10)
                await ws.send("PING")
        asyncio.create_task(ping())
        async for raw in ws:
            if raw == "PONG":
                continue
            msg = json.loads(raw)
            handle(msg["event_type"], msg)

Two subscription details matter. The payload key is literally assets_ids, which does not match the asset_ids many people search for. And setting custom_feature_enabled to true unlocks the best_bid_ask, new_market, and market_resolved events on top of the defaults.

Each message carries an event_type field that tells you what it is. A price_change event means a level in the book changed, often from an order being placed or cancelled, and does not by itself mean a trade happened. A last_trade_price event is emitted when a maker and taker match. Route on event_type and you have a clean event stream.

On keepalive, the guidance is to send PING roughly every 10 seconds. The server also pings, and if you fail to pong within its window the connection closes, so handle both directions.

One version note worth building against: Polymarket cut production over to CLOB V2 on April 28, 2026. Legacy V1 SDKs and V1-signed orders stopped working on that date, and every open order was wiped at the cutover. Verify you are on the V2 client and endpoints before deploying any code.

Kalshi: orderbook_delta and snapshots

Kalshi's model is snapshot-plus-delta. On subscribe to orderbook_delta, the server first sends a full orderbook_snapshot, then streams incremental orderbook_delta messages that you apply to a local copy. That is the whole point: keep a book in memory rather than re-fetching it.

Auth is required on every Kalshi WebSocket connection, including public data channels. All channels require the same three headers on the handshake: KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, and KALSHI-ACCESS-SIGNATURE. The signature is formed from the timestamp, the method GET, and the exact path /trade-api/ws/v2. The distinction between public and private channels on Kalshi is about per-user data enrichment, not about whether auth is required. orderbook_delta is classified as a private channel because responses include your own client_order_id, but the underlying market data is public.

The subscribe command takes a channels array and market tickers. For orderbook_delta only market_ticker or market_tickers filters are supported, not market_id.

# Kalshi orderbook_delta subscribe (auth headers set on the handshake)
subscribe = {
    "id": 1,
    "cmd": "subscribe",
    "params": {
        "channels": ["orderbook_delta"],
        "market_tickers": ["KXNFLGAME-25OCT13-SF-KC"],
    },
}
await ws.send(json.dumps(subscribe))

# first message: orderbook_snapshot -> load full book
# then: orderbook_delta -> apply [price, delta] to yes/no side
# delta > 0 means size added, delta < 0 means size removed

Two operational limits to design around. Kalshi caps concurrent WebSocket connections per user, with a default of 200 that scales by tier, so multiplex many tickers onto one connection with the market_tickers array instead of one socket per market. And instead of opening new connections to change your subscription, use the update_subscription command to add or remove tickers on a live session.

For heartbeats, the Kalshi server sends a Ping control frame roughly every 10 seconds with the body 'heartbeat', and your client must respond with a Pong. Python's websockets library answers Pongs automatically, so in Python you mostly get this for free. Other stacks need it wired explicitly.

Reconnects and sequence gaps

This is where naive clients silently corrupt themselves. Kalshi's orderbook_delta messages carry a monotonic seq. If you see a seq greater than last plus one, you missed a delta, and a missed delta means your local book is wrong. Duplicates with seq at or below the last seen should be ignored.

Deltas are not replayable, so do not try to guess what changed. On a gap or after any disconnect, the correct move is: stop acting on the local book, clear it, and rebuild from a fresh snapshot before processing deltas again.

Kalshi added a cleaner path for this. As of April 2026, orderbook_delta supports a get_snapshot action on update_subscription, so after a reconnect you can request a fresh snapshot and resume without dropping the existing subscription.

A concrete failure mode to watch for: at least one community SDK had a bug where a single sequence gap cleared the local book but never requested a resnapshot, leaving the book permanently empty while the iterator stayed open. The lesson generalizes. Whatever client you use, make a gap loud. Surface it to strategy code so it can halt rather than trade against a dead book.

A minimal reconnect policy:

Feeding the strategy loop

Keep the socket dumb and the strategy clean. The recv loop should do three things: normalize each venue's events into one internal shape, maintain the local book, and push typed events onto a queue. Your strategy consumes that queue. It should never touch raw frames or reconnect logic.

A single normalized event, whatever the venue, is usually enough: a timestamp, a market identifier, best bid and ask, and an event kind (book update, trade, or resync-required). That last kind matters. When the feed signals a gap or reconnect, the strategy should pause new entries until the book is rebuilt rather than sizing off stale prices.

This separation is exactly what a runtime buys you. Banger (bangertrades.com) is a strategy-automation runtime for Polymarket and Kalshi: you write a banger.Strategy, paper-trade it against the live order book, then run it under a declarative risk envelope with a per-trade cap, daily loss stop, max open positions, and a kill switch. It never custodies funds. You bring your own venue keys. The feed-normalization and resync handling described above are the plumbing it standardizes so your strategy sees clean events. Install with pip install bangertrades, then banger run strategy.py --paper to dry-run against live data before risking capital.

Whichever venue you target, the shape of a correct client is the same: subscribe with the right identifiers, answer heartbeats, treat every reconnect as a resync, and never let a sequence gap reach your strategy as a valid book. Get those four right and the streaming layer stops being a source of silent losses.

Sources

Keep reading

Run your first strategy free

Paper-trade on Polymarket and Kalshi market data without venue keys.

Start free