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 · 3 min read · reviewed 2026-10-14

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

A WebSocket can deliver market updates without repeatedly requesting each snapshot. It does not guarantee that an application has received every change or maintains a valid book. The first useful prototype should have a finite duration, bounded buffers and an explicit interpretation of what it records.

Use the documented surface

Polymarket's current documentation describes unified SDK subscriptions and a raw market stream. The raw subscription uses outcome asset IDs, which must come from verified market metadata. A market title or condition identifier is not an interchangeable asset ID. Its application heartbeat uses text PING and PONG; that differs from WebSocket protocol control frames.

The illustrative raw observer below uses the documented market endpoint. Supply one actual verified asset ID; a placeholder is rejected. It accepts dictionary or list JSON frames, retains raw event dictionaries, and stops at a frame count or deadline. It does not normalize prices, rebuild a book, place orders or reconnect automatically.

import asyncio
import contextlib
import json
import websockets

async def observe_raw_market(asset_id, frame_limit=10, seconds=25):
    if not isinstance(asset_id, str) or not asset_id.isascii() or not asset_id.isdigit():
        raise ValueError("verified numeric outcome asset ID required")
    if type(frame_limit) is not int or not 1 <= frame_limit <= 50:
        raise ValueError("frame limit must be 1..50")
    if type(seconds) not in (int, float) or not 0 < seconds <= 30:
        raise ValueError("deadline must be within 30 seconds")
    async def collect():
        url = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
        async with websockets.connect(url, open_timeout=10, close_timeout=2,
                                      max_size=65536, max_queue=8,
                                      ping_interval=None) as ws:
            await ws.send(json.dumps({"assets_ids": [asset_id], "type": "market"}))
            async def heartbeat():
                while True:
                    await ws.send("PING")
                    await asyncio.sleep(10)
            task = asyncio.create_task(heartbeat())
            events = []
            try:
                for _ in range(frame_limit):
                    frame = await ws.recv()
                    if frame in ("PONG", b"PONG"):
                        continue
                    value = json.loads(frame)
                    rows = value if isinstance(value, list) else [value]
                    if not all(isinstance(row, dict) for row in rows):
                        raise ValueError("unexpected market frame")
                    events.extend(rows)
                return events
            finally:
                task.cancel()
                with contextlib.suppress(asyncio.CancelledError):
                    await task
    return await asyncio.wait_for(collect(), timeout=seconds)

Call this from an existing async program, or use asyncio.run with an actual asset ID. The deadline includes collection and connection setup; connection closing adds bounded cleanup time. A timeout or malformed message is an incomplete observation, not an empty successful book. Validate the installed websockets version against its API before integrating it into another application.

Authenticate Kalshi connections correctly

Kalshi's documented WebSocket handshake requires signed authentication, including for channels carrying public market data. Select the intended demo or production endpoint and follow the current key-type and timestamp requirements. This observer does not generate keys or supply a Kalshi signing implementation.

After authentication, use the current channel contract and verified ticker filters. A ticker summary differs from snapshot/delta depth and user-specific fills. Preserve subscription IDs and the documented scope of sequence numbers. Do not treat a sequence value from one subscription as a global offset for unrelated streams.

Rebuild state before trusting deltas

A book consumer needs an initial valid snapshot, ordered applicable updates and a gap policy. Connection recovery should invalidate uncertain state until it can be rebuilt through the supported protocol. Heartbeat responses establish a transport observation, not the freshness of every subscribed instrument.

Distinguish snapshots, deltas, trades and lifecycle messages. Normalize their timestamp units and outcome identifiers separately. A terminal-market message is not proof that a cash settlement has arrived. Keep overflow, parsing and disconnect evidence with bounded payloads rather than silently resuming a supposedly continuous history.

Relate the stream to paper research

Banger's public adapters deliver normalized ticks through on_market_tick(market, tick, ctx). Kalshi also has a public REST fallback when credentials are absent. The existence of either adapter does not prove a complete, gap-free depth reconstruction or exact execution simulation.

Validate raw parsing, heartbeat cleanup and timeout behavior with offline fixtures before a supervised finite observation. Keep the result's data scope explicit. This example demonstrates transport handling; a strategy or reconstructed-book service needs its own identity, state, risk and shutdown validation.

Sources

Keep reading

Run your first strategy free

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

Start free