Polymarket WebSocket API Starter (Python): Live Order Books Without Polling (2026)

Polymarket International Read-only (no keys)

Verified working on live data in read-only mode, Oct 23, 2026. Market channel only; order placement is out of scope and the user channel needs your own API keys.

Short answer: Polymarket's public CLOB market channel streams order-book changes with no API key. This recipe builds a read-only Python streamer that keeps a local book per outcome from book snapshots and price_change updates, checks it against the server's best bid and ask, audits it against REST, sends the 10-second PING heartbeat and rebuilds every book after a reconnect. It never holds a key or places an order.

What are the key facts?

  • URL wss://ws-subscriptions-clob.polymarket.com/ws/market, no auth. Subscribe with {"assets_ids":[...],"type":"market"} using the outcome token IDs from Gamma's clobTokenIds.
  • Heartbeat: send the text frame PING every 10 seconds; the server answers with the text PONG. Don't json.loads it.
  • book is a full snapshot (sent on subscribe and after trades). price_change entries carry the NEW aggregate size at a price, not a delta; "0" removes the level. Each entry also has the server's best_bid and best_ask.
  • The first dump arrives as a JSON list, later frames as single objects. Handle both.
  • A marketable order shows up in pieces: we watched a BUY at 0.93 arrive in one frame with the server's best prices already post-match, and the 0.93 ask removal in the next. A locked local book for one frame is normal, not a bug.
  • custom_feature_enabled: true adds best_bid_ask, new_market and market_resolved events. The old 100-token subscription limit was removed in May 2025.
DifficultyIntermediate
Time45–60 min
Cost$0 (public endpoints, no account)
Works withClaude Code, Cursor, ChatGPT / Codex, Grok

What will you build?

A Python CLI that subscribes to Polymarket's public market WebSocket for the markets you pick and keeps a correct local order book for each outcome. It cross-checks every update against the server's best bid and ask, audits against the REST book on a timer, prints trades and a periodic top-of-book table, and survives dropped connections. It never holds a key and has no order code.

Who is it for?

Builders who already pull Polymarket markets and books over REST and want live updates without polling, plus anyone who wants to see exactly what the market channel sends before building a bot on it.

Not for: Placing orders, tracking your own orders (that's the authenticated user channel) or deciding what to trade. For REST basics, auth and a paper broker, use the CLOB API starter instead.

What the Polymarket Market Channel Sends, and How to Keep a Correct Book

Polling the REST order book is fine for a quick look, but a bot that watches several markets wants updates as they happen. Polymarket's CLOB has a public market channel on a WebSocket. You subscribe with a list of outcome token IDs, get a full order-book snapshot for each, and then a stream of price-level changes and trades. No account or key is needed.

Getting a stream to print is easy. Keeping a book that is actually correct takes more care, and that's what this recipe is about:

  • Snapshots and changes: a book event replaces the whole book. A price_change entry gives the new total size at one price (set it, don't add it), and size "0" removes the level.
  • Proof the book is right: every price_change entry also carries the server's best_bid and best_ask. The streamer compares those with its own book after each message, resyncs after three mismatches in a row, and audits against the REST /book endpoint on a timer.
  • Staying connected: a PING every 10 seconds, a watchdog that treats 30 seconds of silence as a dead socket, and a reconnect that throws away every book and waits for fresh snapshots. A --drop-after flag closes the socket once on purpose so you can see the reconnect work against the live server.

In our run on October 23, 2026, five of the busiest markets produced about 400 price_change events in 150 seconds. The local books passed 1,556 server best-price checks with no mismatches, and every REST audit matched, including after the forced reconnect. The two problems we hit were real-world details, and both fixes are in the prompt. --top picked a market whose end date had passed but that wasn't closed yet. And a marketable order briefly locked the local book because its removals arrived one frame later.

This recipe only reads. If you're starting from zero, the Polymarket API starter kit covers Polymarket US, and the Polymarket CLOB API starter covers REST books, fees, L1/L2 auth and a paper broker on the international CLOB. For how Kalshi's and Polymarket's websockets compare (Kalshi requires a signed handshake even for public data), see the Kalshi vs Polymarket API comparison and the Kalshi API starter. Everything else for the international CLOB is on the Polymarket (international) platform page.

What prompt do you paste into your AI coding tool?

Works in Claude Code, Cursor, Codex/ChatGPT, Grok or any agent that can write files. Paste it into an empty folder. Read what it builds before you run anything.

You are building a READ-ONLY Python streamer for the international Polymarket
CLOB public market WebSocket. It keeps a live local order book per outcome,
checks that book against the server, and survives disconnects. It never
holds a key, never signs, never connects to the authenticated user channel
and never places, cancels or simulates real orders. Use only what is
documented at https://docs.polymarket.com (Market Data > Real-Time Data;
API Reference > WebSocket > Market Channel; Prices and Order Books; Rate
Limits). If something isn't documented, stop and ask me.

Project: pm-ws/
- Python 3.10+ (run with `python3`), `requests`, `websocket-client`,
  `pytest`. No SDK needed.
- markets.py
  - GAMMA https://gamma-api.polymarket.com, CLOB https://clob.polymarket.com.
  - resolve(slugs): GET {GAMMA}/markets?slug=<slug> for each slug. The
    response is a LIST. `outcomes` and `clobTokenIds` are JSON-ENCODED
    STRINGS: json.loads them. Return {token_id: {"slug", "outcome",
    "question"}}. Skip (with a printed reason) markets that are closed or
    have enableOrderBook false. Outcomes are not always Yes/No (sports use
    team names): label by the outcome string.
  - top_markets(n): GET {GAMMA}/markets?closed=false&end_date_min=<now,
    UTC ISO>&order=volume24hr&ascending=false&limit=n, same mapping, and
    also skip acceptingOrders false. Without end_date_min you get markets
    whose end date has passed but that are not closed yet (waiting on
    resolution); their books barely move.
  - rest_book(token_id): GET {CLOB}/book?token_id=<id>. Bids come sorted
    ASCENDING and asks DESCENDING, so never take element [0]: best bid =
    max price, best ask = min price.
  - Back off and retry on HTTP 429; timeouts on every request.
- book.py
  - LocalBook(asset_id): bids and asks as dicts keyed by the price STRING
    normalised with decimal.Decimal (so "0.5" and "0.50" are one level),
    size as Decimal. ready=False until the first snapshot.
  - apply_snapshot(msg): a `book` event REPLACES both sides (it's sent on
    subscribe and again after trades). Store hash and timestamp.
  - apply_change(ch): a `price_change` entry {asset_id, price, size, side,
    hash, best_bid, best_ask}. side BUY -> bids, SELL -> asks. `size` is the
    NEW AGGREGATE size at that price, not a delta: set it; "0" removes
    the level. Ignore changes for a book that isn't ready (count them).
  - best_bid() = max bid price, best_ask() = min ask price, spread, mid
    (None when a side is empty).
  - After applying ALL entries of one price_change message, compare each
    touched book's best_bid/best_ask with the server's best_bid/best_ask
    from that asset's LAST entry in the message (skip when absent). Count
    checks and mismatches. If the local book is locked or crossed (best
    bid >= best ask), SKIP the check and count it as crossed_skipped: a
    marketable order arrives as an add on one side whose best_bid/best_ask
    already show the post-match state, and the matching removals come in
    the next frame(s). 3 mismatches in a row on one asset -> mark it
    not ready and ask the stream to resync (reconnect, which brings a
    fresh snapshot).
- stream.py
  - URL wss://ws-subscriptions-clob.polymarket.com/ws/market (public, no
    auth). After connecting send {"assets_ids": [...], "type": "market"}
    (add "custom_feature_enabled": true with --custom to also get
    best_bid_ask, new_market and market_resolved). One connection for all
    assets.
  - Heartbeat: send the TEXT frame "PING" right after subscribing and then
    every 10 seconds; the server answers the text "PONG". Never json.loads
    "PONG"; any other non-JSON text: log and ignore.
  - A frame can be ONE JSON object or a JSON LIST of events (the initial
    book dump arrives as a list). Handle both. Dispatch on event_type:
    book, price_change, last_trade_price, tick_size_change, best_bid_ask,
    new_market, market_resolved; count unknown types, never crash.
  - Watchdog: if nothing at all (not even PONG) arrives for --stale-after
    seconds (default 30), treat the socket as dead.
  - Reconnect on any close, error, dead socket or resync request: CLEAR
    every book (state from the old socket is invalid), back off 1, 2, 4,
    ... max 30 s with jitter, reset the backoff once a message arrives,
    resubscribe and wait for new snapshots. Count reconnects.
  - --drop-after S: close the socket once, S seconds in, to prove the
    reconnect path works on the live server.
  - `connect` is injectable so tests use a fake socket. Ctrl-C exits
    cleanly with the stats line.
- pmws.py (argparse):
  - stream (--slug SLUG ... | --top N) [--duration S] [--print-every 15]
    [--custom] [--log events.jsonl] [--drop-after S] [--audit-every S]
    [--stale-after 30]. Every --print-every seconds print one line per
    outcome: slug, outcome, best bid, best ask, spread, levels per side,
    last trade, seconds since last update. Print each last_trade_price as
    "TRADE <slug> [<outcome>] <side> <size> @ <price>" (side is the
    taker's side). --audit-every: fetch rest_book for each ready asset and
    compare best bid/ask with the local book; on a difference re-check
    once after 2 s (the two can be a few ms apart) and report
    "audit OK" or "audit DIFF" with both values. At exit print stats:
    frames, events by type, pongs, reconnects, server best-price checks
    and mismatches, crossed_skipped, audits OK/DIFF, changes dropped
    before a snapshot. Print the key counters even when they are 0.
  - book (--slug SLUG | --token ID): one REST book with best bid/ask.
  - Banner: "Public Polymarket market data, read-only. Not advice."
- tests/conftest.py inserting the project root into sys.path.
- tests/ (pytest, no network): Gamma JSON-string decoding; REST best =
  max bid / min ask on ascending/descending arrays; snapshot replaces;
  aggregate size set (not added) and "0" removes; "0.5" vs "0.50" one
  level; changes before a snapshot are dropped; server best-price check
  counts a mismatch and 3 in a row triggers resync; list and object
  frames; a locked book from a marketable order is skipped, not counted
  as a mismatch; top_markets sends end_date_min and skips
  acceptingOrders false; "PONG" and junk text don't crash; first PING sent right after
  subscribe and then on the interval (fake clock); reconnect clears books
  and resubscribes; watchdog fires on silence; and a grep test that the
  APP modules (not tests/) contain none of "private_key", "eth_account",
  "ws/user", "post_order", "create_order", "/order", "api_secret".
- .gitignore (.venv/, __pycache__/, *.jsonl), README with the commands.

Show me the file plan first. After writing, tell me exactly what to run.

How do you build it, step by step?

  1. Set up a clean folder (pm-ws) with the Safety Pack rules file, then paste the prompt into your AI coding tool. Approve the file plan before it writes code, and check that it contains no order or key code.
  2. Install and run the tests (they pass offline, using a fake socket and a fake clock):
    python3 -m venv .venv && source .venv/bin/activate
    pip install requests websocket-client pytest
    python3 -m pytest -q
    
  3. Check one REST book: python3 pmws.py book --slug <market-slug>. You can find the slug in the market's URL on polymarket.com. You should see a best bid and best ask for each outcome.
  4. Stream the busiest markets for a few minutes:
    python3 pmws.py stream --top 5 --duration 150 --print-every 30 --audit-every 40 --drop-after 60 --custom --log events.jsonl
    
    Watch for the disconnected (forced drop ...) line about 60 seconds in, followed by fresh books. In the final stats line, pongs should equal pings (or trail by one), reconnects should be 1, and server_mismatches and audit_diff should be 0.
  5. Stream markets you choose: python3 pmws.py stream --slug <slug> --slug <slug> --duration 120. Quiet markets may print no trades for minutes. The PONG replies keep the watchdog satisfied.
  6. Read the log: events.jsonl has every event as received. It's the quickest way to see how a single trade turns into price_change, book and last_trade_price events.

How do you test it in paper or demo mode first?

This recipe is read-only from start to finish, so there's nothing to paper-trade. It never connects to the authenticated user channel, holds no key and has no code that can place an order.

When you're ready to test a strategy on top of this live data, feed the local books into a paper broker rather than a real one. The Polymarket CLOB API starter includes a paper broker that fills against real books, and Build a Polymarket bot with Claude Code (paper mode) adds guardrails. Pair either with kill switch and position limits before anything touches real money.

What safety checks does it need before real money?

Tick every box before the bot touches real money. The full rules file, .gitignore and pre-commit hook are in the Safety Pack.

Which official docs did we check?

Endpoints and rules were checked against these pages on Oct 23, 2026. APIs change, so if something here disagrees with the official docs, trust the docs and tell us.

Frequently asked questions

Does the Polymarket WebSocket need an API key?

Not the market channel. wss://ws-subscriptions-clob.polymarket.com/ws/market is public: connect, send {"assets_ids":[...],"type":"market"} and you'll get order-book data. The user channel, which streams your own orders and trades, needs your API credentials. This recipe only uses the market channel.

How do I get the asset IDs to subscribe to?

They're the outcome token IDs. Call Gamma GET https://gamma-api.polymarket.com/markets?slug=<slug> and read clobTokenIds. It's a JSON-encoded string, so parse it, and it lines up with outcomes (also a JSON string). Each outcome, such as Yes and No, has its own ID and its own book.

How do I keep the Polymarket WebSocket connection alive?

Send the text frame PING every 10 seconds; the server replies with PONG. Also run a watchdog: if nothing at all arrives for about 30 seconds, close the socket, clear your books, reconnect with backoff and resubscribe. You'll get fresh snapshots.

Is the size in price_change a delta or a total?

A total. size is the new aggregate size resting at that price on that side, so set it, don't add it. A size of "0" means the level is gone. If you add sizes instead, your book drifts away from the server within minutes, and the best_bid/best_ask check in this recipe catches that.

Why does my local book sometimes show the same bid and ask?

A marketable order can arrive as an add at the opposite side's price in one frame, with the matching removals in the next. For that one frame your book is locked or crossed even though the server's best prices already reflect the match. The streamer skips its server check while the book is crossed and counts it as crossed_skipped.

Is there a limit on how many markets I can subscribe to?

Polymarket's changelog says the 100-token limit for the market channel was removed on May 28, 2025. You can also add or remove tokens on an open connection with {"operation":"subscribe","assets_ids":[...]} or "unsubscribe". One connection for everything is still the polite default.

Can this streamer trade?

No. It has no key and no order code, and a test enforces that. To experiment with orders, use paper mode in the CLOB API starter first. BotCookery doesn't give financial advice.

More from the kitchen: Recipes by platform · Kalshi & Polymarket fee calculator · How we verify recipes · Safety Pack