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.
URLwss://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.
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?
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.
Install and run the tests (they pass offline, using a fake socket and a fake clock):
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.
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.
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.
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.
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.
Key handling: API keys and private-key files live in environment variables or a secrets manager. Never in code, prompts, chat logs, screenshots or git. .env and *.pem are in .gitignore, and your AI tool is blocked from reading them (see the Safety Pack).
Position limits: hard caps in config (max contracts per order, max total exposure, max daily loss). The bot refuses any order above them, and the caps are checked in code, not just in the prompt.
Kill switch: one command or one file (touch KILL) stops new orders immediately, and a second command cancels every resting order through the venue's cancel-all endpoint.
Paper first: paper or demo mode is the default. Live trading needs an explicit --live flag and a typed confirmation, and you only flip it after days of clean paper logs.
No key, no wallet, no signing library, no order code. A test fails if any app module mentions private_key, eth_account, ws/user, post_order, create_order, /order or api_secret.
Market data is not advice. The output is a top-of-book table and a list of trades; it doesn't rank, score or suggest anything.
Be gentle with the API: one WebSocket connection for all markets, reconnects back off from 1 to 30 seconds with jitter, and REST audits run only on a timer. The documented REST limits are generous (1,500 requests per 10 seconds for /book), but a reconnect loop without backoff is how people get rate-limited.
If you later add the user channel to track your own orders, its API credentials go in .env, never in a prompt or in git. Follow the Wallet-Key Safety Pack.
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.
Get new recipes by email
When a new bot recipe passes its model tests, we'll send it to you. Free. No picks, no signals, ever.
An AI prompt that builds a Python starter for the international Polymarket CLOB API: Gamma market data, correct order books, per-market fees, L1/L2 auth explained, and a paper broker. No orders, no keys.
A copy-paste AI prompt and step-by-step guide to the Polymarket US API: public market data with no key, then authenticated reads. Includes the international Gamma API, read-only, with a geoblock check.
Get started with the Kalshi API using an AI coding tool: public market data with no key, then RSA-PSS or Ed25519 signed requests against the Kalshi demo environment. Endpoints checked against the official docs.
An AI prompt that builds four small wallet-safety tools for crypto and prediction-market bots: a secret scanner pre-commit hook, an env-file guard, a kill switch and a read-only wallet audit that flags balances and unlimited approvals.