Polymarket exposes four WebSocket channels that give you real-time access to orderbook updates, order lifecycle events, live sports scores, and crypto prices. If you’re building a trading bot or agent, WebSockets are how you avoid hammering the REST API and get data the instant it changes.

This guide covers every channel, shows working connection code in Python and TypeScript, walks through orderbook reconstruction from scratch, and covers the production patterns you need for a reliable streaming pipeline.

For a general overview of the Polymarket API, see the Polymarket API Guide. For rate limit details on the REST endpoints you’ll use alongside WebSockets, see the Polymarket Rate Limits Guide.


TL;DR

Polymarket offers four WebSocket channels:

  1. Market – public orderbook and price data for any token
  2. User – authenticated order fills, cancellations, and status updates
  3. Sports – live game scores, periods, and match status
  4. RTDS – crypto prices (Binance and Chainlink feeds) and platform comments

All four use standard WebSocket connections with JSON messages. Market and user channels require a PING every 10 seconds. Sports and RTDS channels require a PING (or pong response) every 5 seconds. This guide covers connection, subscription, orderbook reconstruction, and production patterns for each.


Channel Reference

ChannelEndpointAuthDataHeartbeat
Marketwss://ws-subscriptions-clob.polymarket.com/ws/marketNoOrderbook, prices, trades, custom eventsClient sends PING every 10s
Userwss://ws-subscriptions-clob.polymarket.com/ws/userYes (apiKey, secret, passphrase)Order fills, cancellations, statusClient sends PING every 10s
Sportswss://sports-api.polymarket.com/wsNoLive game scores, periods, statusRespond to server ping within 10s
RTDSwss://ws-live-data.polymarket.comOptional (gamma_auth)Crypto prices, commentsClient sends PING every 5s

Market Channel

The market channel is the primary feed for trading bots. It streams orderbook updates, price changes, and trade data for any token you subscribe to – no authentication required.

Endpoint: wss://ws-subscriptions-clob.polymarket.com/ws/market

Subscription Format

After connecting, send a JSON subscription message with the token IDs (asset IDs) you want to track:

{
  "assets_ids": ["71321045679252212594626385532706912750332728571942532289631379312455583992563"],
  "type": "market",
  "custom_feature_enabled": true
}

Setting custom_feature_enabled to true enables the additional message types best_bid_ask, new_market, and market_resolved. You almost always want this on.

Message Types

Message TypeDescription
bookIncremental orderbook update with changed bid/ask levels
price_changeMid-price or last-trade price has changed
tick_size_changeMarket tick size has been updated
last_trade_pricePrice of the most recent fill
best_bid_askCurrent best bid and ask prices (requires custom_feature_enabled)
new_marketA new market has been listed (requires custom_feature_enabled)
market_resolvedA market has resolved to an outcome (requires custom_feature_enabled)

Working Example (Python)

import asyncio
import websockets
import json

async def market_stream(token_ids):
    uri = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
    async with websockets.connect(uri) as ws:
        # Subscribe
        await ws.send(json.dumps({
            "assets_ids": token_ids,
            "type": "market",
            "custom_feature_enabled": True
        }))

        # Heartbeat task
        async def heartbeat():
            while True:
                await asyncio.sleep(10)
                await ws.send("PING")

        asyncio.create_task(heartbeat())

        # Process messages
        async for message in ws:
            if message == "PONG":
                continue
            data = json.loads(message)
            print(f"[{data.get('event_type')}] {data}")

asyncio.run(market_stream(["<token-id-yes>", "<token-id-no>"]))

Replace <token-id-yes> and <token-id-no> with the actual token IDs for the Yes and No outcomes of a market. You can find token IDs via the Gamma API or the CLOB GET /markets endpoint.

Working Example (TypeScript)

import WebSocket from "ws";

function marketStream(tokenIds: string[]) {
  const ws = new WebSocket(
    "wss://ws-subscriptions-clob.polymarket.com/ws/market"
  );

  ws.on("open", () => {
    // Subscribe
    ws.send(
      JSON.stringify({
        assets_ids: tokenIds,
        type: "market",
        custom_feature_enabled: true,
      })
    );

    // Heartbeat every 10 seconds
    setInterval(() => {
      ws.send("PING");
    }, 10_000);
  });

  ws.on("message", (raw: Buffer) => {
    const message = raw.toString();
    if (message === "PONG") return;

    const data = JSON.parse(message);
    console.log(`[${data.event_type}]`, data);
  });

  ws.on("close", (code, reason) => {
    console.log(`Disconnected: ${code} ${reason}`);
  });

  ws.on("error", (err) => {
    console.error("WebSocket error:", err);
  });
}

marketStream(["<token-id-yes>", "<token-id-no>"]);

Dynamic Subscriptions

Both the market and user channels support adding and removing subscriptions on a live connection. You do not need to disconnect and reconnect when you want to track new markets.

Subscribe to additional markets:

{
  "assets_ids": ["<new-token-id>"],
  "operation": "subscribe"
}

Unsubscribe from a market:

{
  "assets_ids": ["<token-id>"],
  "operation": "unsubscribe"
}

This is useful for agents that dynamically discover new markets (for example, when a new_market event fires) and want to start tracking them immediately.


User Channel

The user channel delivers your personal order lifecycle events – fills, cancellations, placement confirmations. It requires API key authentication and subscribes by condition ID (not asset ID).

Endpoint: wss://ws-subscriptions-clob.polymarket.com/ws/user

Authentication

Include your API credentials in the subscription message:

{
  "auth": {
    "apiKey": "your-api-key",
    "secret": "your-api-secret",
    "passphrase": "your-passphrase"
  },
  "markets": ["0x1234...condition_id"],
  "type": "user"
}

Important: The markets field takes condition IDs, not token/asset IDs. A condition ID identifies the market as a whole, while asset IDs identify individual Yes/No outcomes. You can find condition IDs from the Gamma API or GET /markets on the CLOB.

If your authentication fails, see Polymarket Auth Troubleshooting for common fixes.

Message Types

Message TypeDescription
tradeAn order has been matched and is progressing through settlement. Lifecycle: MATCHED then CONFIRMED.
orderOrder placement acknowledgments, updates, and cancellations.

The trade message fires twice per fill – once when the match engine pairs your order (MATCHED), and once when the trade settles on-chain (CONFIRMED). Your agent should track both states to know when funds are actually transferred.

Working Example (Python)

import asyncio
import websockets
import json

async def user_stream(api_key, secret, passphrase, condition_ids):
    uri = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
    async with websockets.connect(uri) as ws:
        # Authenticate and subscribe
        await ws.send(json.dumps({
            "auth": {
                "apiKey": api_key,
                "secret": secret,
                "passphrase": passphrase
            },
            "markets": condition_ids,
            "type": "user"
        }))

        # Heartbeat
        async def heartbeat():
            while True:
                await asyncio.sleep(10)
                await ws.send("PING")

        asyncio.create_task(heartbeat())

        # Process order events
        async for message in ws:
            if message == "PONG":
                continue
            data = json.loads(message)
            event_type = data.get("event_type")

            if event_type == "trade":
                status = data.get("status")
                print(f"Trade {data.get('id')}: {status}")
            elif event_type == "order":
                print(f"Order update: {data}")

asyncio.run(user_stream(
    "your-api-key",
    "your-api-secret",
    "your-passphrase",
    ["0x1234...condition_id"]
))

Dynamic subscribe/unsubscribe works on the user channel too. Use "operation": "subscribe" or "operation": "unsubscribe" with a markets array of condition IDs to add or remove markets on a live connection.


Sports Channel

The sports channel streams live game data for all active sports events on Polymarket. Unlike the market and user channels, no explicit subscription is needed – you receive updates for every active game as soon as you connect.

Endpoint: wss://sports-api.polymarket.com/ws

Heartbeat: The server sends a ping frame every 5 seconds. Your client must respond with a pong within 10 seconds or the connection will be dropped. Most WebSocket libraries handle this automatically at the protocol level.

Message type: sport_result – contains live scores, game periods, and match status (in-progress, final, etc.).

import asyncio
import websockets
import json

async def sports_stream():
    uri = "wss://sports-api.polymarket.com/ws"
    async with websockets.connect(uri) as ws:
        async for message in ws:
            data = json.loads(message)
            if data.get("event_type") == "sport_result":
                payload = data.get("payload", {})
                print(f"{payload.get('home_team')} vs {payload.get('away_team')}: "
                      f"{payload.get('home_score')}-{payload.get('away_score')} "
                      f"({payload.get('status')})")

asyncio.run(sports_stream())

The websockets library in Python automatically responds to protocol-level ping frames, so you do not need to implement the pong response manually.


RTDS (Real-Time Data Socket)

RTDS streams crypto prices and platform comments. It is a separate system from the market/user channels, with its own endpoint, subscription format, and heartbeat interval.

Endpoint: wss://ws-live-data.polymarket.com

Auth: Optional. Pass a gamma_auth token for user-specific streams (like personalized comment notifications). Public crypto price feeds do not require authentication.

Heartbeat: Client sends PING every 5 seconds (faster than the market/user channels).

Crypto Prices

RTDS offers two crypto price sources:

TopicSourceSymbols
crypto_pricesBinancebtcusdt, ethusdt, solusdt, xrpusdt
crypto_prices_chainlinkChainlinkbtc/usd, eth/usd, sol/usd, xrp/usd

Subscription format:

{
  "action": "subscribe",
  "subscriptions": [
    {
      "topic": "crypto_prices",
      "type": "update",
      "filters": "btcusdt,ethusdt"
    }
  ]
}

The filters field is a comma-separated list of symbols. Omit it to receive all symbols for that topic.

Comments Stream

Subscribe to the comments topic to receive real-time platform comments. This is primarily useful for sentiment analysis or social monitoring agents.

{
  "action": "subscribe",
  "subscriptions": [
    {
      "topic": "comments",
      "type": "update"
    }
  ]
}

Working Example

import asyncio
import websockets
import json

async def rtds_crypto_stream():
    uri = "wss://ws-live-data.polymarket.com"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "action": "subscribe",
            "subscriptions": [{
                "topic": "crypto_prices",
                "type": "update",
                "filters": "btcusdt,ethusdt"
            }]
        }))

        async def heartbeat():
            while True:
                await asyncio.sleep(5)
                await ws.send("PING")

        asyncio.create_task(heartbeat())

        async for message in ws:
            if message == "PONG":
                continue
            data = json.loads(message)
            payload = data.get("payload", {})
            print(f"{payload.get('symbol')}: ${payload.get('value')}")

asyncio.run(rtds_crypto_stream())

This is especially useful for agents trading crypto-correlated prediction markets (e.g., “Will BTC be above $X by date Y?”) and need a real-time price reference without setting up a separate Binance WebSocket connection.


Building a Local Orderbook

The most common use case for the market WebSocket channel is maintaining a local orderbook. The pattern is: fetch a REST snapshot for the initial state, then apply incremental WebSocket updates to keep it current.

Step 1: Fetch REST Snapshot

Use the CLOB REST API to get the current orderbook state:

import requests

def get_orderbook_snapshot(token_id):
    resp = requests.get(
        "https://clob.polymarket.com/book",
        params={"token_id": token_id}
    )
    return resp.json()

The response includes bids and asks arrays, each containing objects with price and size fields.

Step 2: Apply WebSocket Updates

Once connected to the market WebSocket, incoming book messages contain incremental updates – only the price levels that have changed. The algorithm:

  1. For each bid in the update: if size is 0, remove that price level from your local bids. Otherwise, set (or overwrite) that price level with the new size.
  2. Apply the same logic for asks.
  3. This keeps your local orderbook in sync without needing to re-fetch the full snapshot.

Step 3: Handle Reconnections

If the WebSocket connection drops:

  1. Re-fetch the REST snapshot to get a clean state (you may have missed updates during the disconnect).
  2. Reconnect to the WebSocket and resubscribe.
  3. Replace your local orderbook with the fresh snapshot.

There is no sequence number or gap detection mechanism in the market channel, so a full snapshot is the only safe recovery path after a disconnect.

Complete Orderbook Class (Python)

class LocalOrderbook:
    def __init__(self, token_id):
        self.token_id = token_id
        self.bids = {}  # price -> size
        self.asks = {}  # price -> size

    def load_snapshot(self, snapshot):
        self.bids = {
            level["price"]: float(level["size"])
            for level in snapshot.get("bids", [])
        }
        self.asks = {
            level["price"]: float(level["size"])
            for level in snapshot.get("asks", [])
        }

    def apply_update(self, message):
        for bid in message.get("bids", []):
            price, size = bid["price"], float(bid["size"])
            if size == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = size
        for ask in message.get("asks", []):
            price, size = ask["price"], float(ask["size"])
            if size == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = size

    @property
    def best_bid(self):
        return max(self.bids.keys()) if self.bids else None

    @property
    def best_ask(self):
        return min(self.asks.keys()) if self.asks else None

    @property
    def spread(self):
        if self.best_bid and self.best_ask:
            return float(self.best_ask) - float(self.best_bid)
        return None

Usage with the WebSocket stream:

import asyncio
import websockets
import json
import requests

async def run_orderbook(token_id):
    book = LocalOrderbook(token_id)

    # Step 1: Load REST snapshot
    snapshot = requests.get(
        "https://clob.polymarket.com/book",
        params={"token_id": token_id}
    ).json()
    book.load_snapshot(snapshot)
    print(f"Snapshot loaded. Spread: {book.spread}")

    # Step 2: Stream updates
    uri = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "assets_ids": [token_id],
            "type": "market"
        }))

        async def heartbeat():
            while True:
                await asyncio.sleep(10)
                await ws.send("PING")

        asyncio.create_task(heartbeat())

        async for message in ws:
            if message == "PONG":
                continue
            data = json.loads(message)
            if data.get("event_type") == "book":
                book.apply_update(data)
                print(f"Bid: {book.best_bid} | Ask: {book.best_ask} | Spread: {book.spread}")

asyncio.run(run_orderbook("<token-id>"))

Production Patterns

Reconnection with Exponential Backoff

In production, WebSocket connections will drop. Network blips, server restarts, and load balancer rotations all cause disconnects. Your bot needs to reconnect automatically with increasing delays to avoid overwhelming the server.

import asyncio
import websockets
import json

async def resilient_connection(token_ids, on_message):
    backoff = 1
    while True:
        try:
            async with websockets.connect(
                "wss://ws-subscriptions-clob.polymarket.com/ws/market"
            ) as ws:
                backoff = 1  # Reset on successful connection
                await ws.send(json.dumps({
                    "assets_ids": token_ids,
                    "type": "market"
                }))

                # Heartbeat
                async def heartbeat():
                    while True:
                        await asyncio.sleep(10)
                        await ws.send("PING")

                hb_task = asyncio.create_task(heartbeat())

                try:
                    async for message in ws:
                        if message != "PONG":
                            await on_message(json.loads(message))
                finally:
                    hb_task.cancel()

        except (websockets.ConnectionClosed, ConnectionError, OSError) as e:
            print(f"Disconnected: {e}. Reconnecting in {backoff}s...")
            await asyncio.sleep(backoff)
            backoff = min(backoff * 2, 60)

Key details:

  • Reset backoff on success. Once you establish a connection and start receiving messages, reset the delay to 1 second.
  • Cap the maximum delay. 60 seconds is a reasonable ceiling – long enough to let transient issues resolve, short enough that you don’t miss too much data.
  • Cancel the heartbeat task when the connection closes to avoid sending on a dead socket.
  • Re-fetch the REST snapshot after reconnecting if you’re maintaining a local orderbook.

Heartbeat Management

Each channel has its own heartbeat interval:

ChannelIntervalDirection
Market10 secondsClient sends PING
User10 secondsClient sends PING
Sports5 secondsServer sends ping; client responds with pong
RTDS5 secondsClient sends PING

If you’re connected to multiple channels simultaneously, run a separate heartbeat task for each connection. Do not share a single timer across channels with different intervals.

Multiple Market Subscriptions

You can subscribe to multiple token IDs in a single subscription message:

{
  "assets_ids": ["<token-id-1>", "<token-id-2>", "<token-id-3>"],
  "type": "market",
  "custom_feature_enabled": true
}

For agents that monitor many markets, use dynamic subscribe/unsubscribe operations to add and remove markets without reconnecting. This is more efficient than maintaining separate WebSocket connections per market.


Agent Integration

An autonomous trading agent typically combines multiple WebSocket feeds in its decision loop:

  1. Market channel provides real-time price signals and orderbook depth. The agent watches for price movements, spread changes, or liquidity shifts that trigger trading logic.

  2. User channel confirms order execution. After placing an order via the REST API, the agent listens for trade messages (MATCHED then CONFIRMED) to verify the fill and update its position tracking.

  3. RTDS supplies correlated asset prices. For crypto-linked markets, the agent compares the RTDS crypto price feed against the prediction market price to detect mispricings.

  4. Sports channel delivers live game state for sports-related markets. An agent can react to score changes or game status updates faster than waiting for the market price to adjust.

A typical agent architecture:

REST API (orders, snapshots)
    |
    v
+---------------------------+
|      Agent Core           |
|  - Strategy logic         |
|  - Position tracking      |
|  - Risk management        |
+---------------------------+
    ^         ^         ^
    |         |         |
Market WS  User WS  RTDS WS
(prices)   (fills)   (crypto)

Each WebSocket feed runs in its own asyncio task, pushing events into a shared queue or callback system that the agent core processes sequentially. This keeps the decision logic single-threaded while the I/O runs concurrently.


FAQ

How do I connect to the Polymarket WebSocket?

Connect to wss://ws-subscriptions-clob.polymarket.com/ws/market for public market data or /ws/user for authenticated order updates. Send a subscription message with assets_ids (market channel) or condition IDs (user channel) immediately after connecting. Send PING every 10 seconds to maintain the connection. The server responds with PONG to confirm the connection is alive. If you miss heartbeats, the server will close the connection and you will need to reconnect.

What is the difference between the market and user WebSocket channels?

The market channel is public (no authentication required) and provides orderbook updates, price changes, last trade prices, and custom events like best_bid_ask and market_resolved for specified token IDs. The user channel requires API key authentication (apiKey, secret, passphrase) and provides your personal order lifecycle events – trade fills (MATCHED then CONFIRMED) and order status updates (placements, cancellations) – for specified condition IDs. The key distinction: market channel uses asset IDs (individual token outcomes), while the user channel uses condition IDs (the market as a whole).

How do I reconstruct the Polymarket orderbook from WebSocket data?

First fetch a REST snapshot via GET /book with the token ID to get the current state of all bid and ask levels. Then connect to the market WebSocket channel and subscribe to that token. Apply incoming book messages as incremental updates to your local orderbook – update price level quantities when they change, and remove levels when the quantity drops to zero. If the WebSocket disconnects, re-fetch the REST snapshot before resubscribing, since you may have missed updates during the outage.

Do WebSocket connections count against Polymarket rate limits?

No. WebSocket connections do not count against REST API rate limits. The REST rate limits (documented in the Rate Limits Guide) only apply to HTTP requests. Using WebSockets instead of polling endpoints like GET /price or GET /book is the single best way to reduce your REST request count and avoid 429 throttling errors. The only REST call you need alongside a WebSocket is the initial GET /book snapshot for orderbook reconstruction.

What is the Polymarket RTDS channel?

RTDS (Real-Time Data Socket) at wss://ws-live-data.polymarket.com is a separate WebSocket service that streams crypto prices and platform comments. It pulls crypto prices from two sources: Binance (symbols: btcusdt, ethusdt, solusdt, xrpusdt) and Chainlink (symbols: btc/usd, eth/usd, sol/usd, xrp/usd). It requires a PING every 5 seconds (faster than the 10-second interval on market/user channels) and uses a topic-based subscription model with optional symbol filters. Authentication is optional – pass a gamma_auth token only if you need user-specific streams.


See Also


This guide is maintained by AgentBets.ai. Found an error or API change we missed? Let us know on Twitter.

Not financial advice. Built for builders.