A Polymarket trading bot needs timely market data to make informed execution decisions. But connecting to a WebSocket and receiving messages is only the beginning. Connections can drop, messages can be delayed, and a bot can accidentally keep using an old order-book snapshot after reconnecting.
A reliable monitor must do three things well: consume market updates, detect when its data is no longer trustworthy, and rebuild its state after a connection failure.
This tutorial builds a small Python monitor around Polymarket's public market WebSocket. It focuses on top-of-book data, connection recovery, and stale-data protection rather than order execution.
- Connect to the Polymarket market WebSocket
Polymarket provides a public market channel for real-time order-book and trade updates. The documented endpoint is:
"wss://ws-subscriptions-clob.polymarket.com/ws/market"
The market channel subscribes using asset IDs, also called token IDs. Each outcome has its own token ID, so you need the appropriate ID for the outcome you want to monitor.
The official "Polymarket WebSocket documentation" (https://github.com/Polymarket/agent-skills/blob/main/websocket.md) describes the subscription format and event types.
Install the Python WebSocket library:
pip install websockets
The example below uses the raw WebSocket protocol so the connection lifecycle is visible rather than hidden inside a larger framework.
- Understand the messages you receive
Three message types are particularly useful for a basic monitor.
"book" provides an order-book snapshot with bid and ask levels. Use it to initialize or rebuild local market state.
"price_change" reports changes to price levels. Its "price_changes" array can contain updates for different assets and levels.
"last_trade_price" reports a trade that has occurred. A last-traded price is not necessarily the price at which your next order can execute.
For monitoring the best bid and ask, the "price_change" payload includes "best_bid" and "best_ask" fields. The code should still distinguish these updates from a complete order-book snapshot.
That distinction matters after reconnecting: receiving one price update does not necessarily mean your local view of the entire book has been rebuilt.
- Build a monitor with stale-data protection
The monitor below tracks one outcome token. It starts in an unready state, subscribes to the market, sends the required application-level heartbeat, and marks the data ready only after receiving a fresh book snapshot.
If the connection drops, the monitor retries with exponential backoff. Every new connection begins unready, preventing the application from treating an old snapshot as current.
Replace "YOUR_OUTCOME_TOKEN_ID" with a valid token ID.
import asyncio
import json
import time
import websockets
WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
ASSET_ID = "YOUR_OUTCOME_TOKEN_ID"
class MarketMonitor:
def init(self):
self.best_bid = None
self.best_ask = None
self.book_ready = False
self.last_message_at = None
def reset(self):
self.best_bid = None
self.best_ask = None
self.book_ready = False
self.last_message_at = None
def handle(self, message):
event_type = message.get("event_type")
self.last_message_at = time.monotonic()
if event_type == "book":
if message.get("asset_id") != ASSET_ID:
return
bids = [
float(level["price"])
for level in message.get("bids", [])
if float(level["size"]) > 0
]
asks = [
float(level["price"])
for level in message.get("asks", [])
if float(level["size"]) > 0
]
self.best_bid = max(bids) if bids else None
self.best_ask = min(asks) if asks else None
self.book_ready = True
print("Fresh book snapshot received.")
elif event_type == "price_change":
for change in message.get("price_changes", []):
if change.get("asset_id") != ASSET_ID:
continue
# Do not trust incremental updates until a fresh
# snapshot has initialized this connection's state.
if not self.book_ready:
continue
if "best_bid" in change:
self.best_bid = float(change["best_bid"])
if "best_ask" in change:
self.best_ask = float(change["best_ask"])
if self.book_ready:
print(
f"Bid: {self.best_bid} | "
f"Ask: {self.best_ask} | "
f"Spread: {self.spread()}"
)
def spread(self):
if self.best_bid is None or self.best_ask is None:
return None
return round(self.best_ask - self.best_bid, 6)
async def heartbeat(ws):
while True:
await asyncio.sleep(10)
await ws.send("PING")
async def run_monitor():
monitor = MarketMonitor()
backoff = 1
while True:
monitor.reset()
try:
async with websockets.connect(
WS_URL,
open_timeout=10,
close_timeout=5,
ping_interval=None,
) as ws:
await ws.send(json.dumps({
"type": "market",
"assets_ids": [ASSET_ID],
"custom_feature_enabled": True,
}))
print("Connected and subscribed.")
backoff = 1
heartbeat_task = asyncio.create_task(heartbeat(ws))
try:
async for raw in ws:
if raw == "PONG":
continue
message = json.loads(raw)
if isinstance(message, dict):
monitor.handle(message)
finally:
heartbeat_task.cancel()
await asyncio.gather(
heartbeat_task,
return_exceptions=True,
)
except (
OSError,
asyncio.TimeoutError,
websockets.exceptions.WebSocketException,
json.JSONDecodeError,
) as error:
print(f"Connection or message error: {error}")
monitor.reset()
print(f"Reconnecting in {backoff} seconds...")
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30)
if name == "main":
asyncio.run(run_monitor())
This is a teaching example, not a complete trading data service. It uses the best-bid and best-ask fields for top-of-book monitoring; it does not maintain a complete local order book after every incremental change. A production implementation should apply price-level updates correctly, validate messages, track data timestamps, and test the current protocol against the official documentation.
- Why the monitor resets its state
Imagine the connection drops while the best ask is $0.48. During the outage, the market moves to $0.53.
If the bot reconnects but keeps using its old $0.48 value, a strategy might believe an attractive entry is still available when it is not.
Resetting the state avoids presenting that old value as current. The monitor waits for a new book snapshot before declaring its top-of-book view ready.
The same principle should apply to a trading bot's decision-making layer:
- Do not submit new trades when market data is stale.
- Do not assume a successful reconnect means the local book is synchronized.
- Rebuild state from a fresh snapshot before trusting incremental updates.
- Reconcile open orders and positions separately from public market data.
A market-data connection does not tell you whether your own order was accepted or filled. Account and order state require their own reliable tracking process.
- Detect stale data explicitly
A connected socket does not guarantee that the data your strategy needs is fresh.
A production monitor should track the time of the last valid market update and define a maximum acceptable age. If no qualifying update arrives within that period, it should mark the feed stale and prevent dependent trading decisions.
The threshold should reflect the application. A dashboard may tolerate a longer delay than a strategy attempting to react to small price changes.
It is also important to distinguish a quiet market from a broken connection. A lack of price changes does not necessarily mean the feed is unhealthy. Heartbeat responses, connection state, server timestamps, and application-level freshness checks should be considered together.
Polymarket's documented market and user channels require sending "PING" every ten seconds, with the server responding "PONG". The example disables the WebSocket library's own ping frames because it sends the protocol's application-level heartbeat instead.
- What to improve before using this in a trading bot
A reliable production implementation needs additional safeguards.
Full order-book maintenance. A "book" event supplies a snapshot. Subsequent "price_change" events can add, update, or remove price levels. A local book must apply these changes correctly, including updates where size becomes zero.
Snapshot and update consistency. If the application loses messages or reconnects, it should rebuild the book rather than assuming its previous state remains valid.
Structured logging. Record connection attempts, reconnects, stale-feed incidents, malformed messages, and snapshot recovery. Avoid logging private credentials.
Backpressure. If updates arrive faster than the application can process them, an unbounded queue can increase memory usage and delay decisions. Measure processing lag and choose a queue policy appropriate to the strategy.
Execution safeguards. Before placing an order, validate the age of the market data, price limits, available balance, and portfolio exposure. A fresh feed alone does not make a trade safe.
Testing. Simulate disconnects, malformed messages, empty books, missing fields, and delayed updates. Verify that the bot refuses to act while its market state is unready or stale.
Conclusion
A Polymarket WebSocket monitor is more than a connection that prints prices. It is a stateful component that must know when its data is valid, when it has become stale, and when it needs to rebuild its view of the market.
Starting with snapshot recovery, heartbeat handling, freshness checks, and bounded reconnect delays gives developers a stronger foundation for market-data systems.
Dexoryn Labs maintains a public "Polymarket copy-trading bot repository" (https://github.com/dexorynlabs/polymarket-copy-trading-bot) for developers evaluating existing automation. Teams with more specific data-processing, monitoring, or execution requirements may need a custom implementation.
Reliable market data does not guarantee profitable trading. It helps prevent a bot from making decisions based on information it should no longer trust.
This article is for educational purposes only. Automated trading involves financial risk. Test in a non-trading environment before connecting market-data logic to live order execution.
Top comments (0)