Chapter 27 of 33
The Short Version
Polymarket exposes three public APIs: CLOB (trading), Gamma (market discovery), and Data (analytics). The official Python SDK is py-clob-client 0.34.6. Authentication uses an API key + ECDSA signature, with orders signed via EIP-712 through a Polygon proxy wallet. Rate limits cap you at roughly 60 orders/minute per key. The single biggest gotcha for new developers is the condition_id → token_id mapping problem between Gamma and CLOB — solve that first, and everything else falls into place. Roughly $40M/month in liquidity rewards and bot-captured spread gets earned on Polymarket, almost entirely by API users.

Three separate services: CLOB (9,000/10s auth) for trading, Gamma (4,000/10s public) for discovery, Data (1,000/10s public) for historical analytics.
Part 1: The Three APIs
Polymarket cleanly separates concerns across three distinct services. Using the right API for each job keeps your bot fast, simple, and inside rate limits.
| API | Base URL | Purpose | Auth Required |
|---|---|---|---|
| CLOB API | clob.polymarket.com | Place, cancel, and track orders. Read order books. Query positions. | Yes (for trading) |
| Gamma API | gamma-api.polymarket.com | Browse markets, fetch metadata, images, outcome prices, volume, expiry, tags. | No (public) |
| Data API | data-api.polymarket.com | Historical trades, position snapshots, user analytics, leaderboard data. | No (public) |
A typical bot loop uses Gamma to find markets, CLOB to fetch order books and place trades, and Data to back-test strategy performance offline. Think of Gamma as the "catalog," CLOB as the "exchange," and Data as the "warehouse."
curl or a browser right now — no account needed. This is a great way to prototype before you even generate an API key.
L1 signs "ClobAuthDomain" EIP-712 struct with chainId 137 to derive credentials. L2 HMAC-SHA256 signs every subsequent request with POLY_SIGNATURE headers.
Part 2: Authentication & the Proxy Wallet Model
Polymarket does not sign trades with your main wallet's private key. Instead, it uses a Gnosis Safe-style proxy wallet: your main wallet authorizes a proxy, and the proxy executes all trades on Polygon. Your API bot talks to that proxy.
What you need
- API key — generate in Polymarket Settings → Developer
- Private key — the key of your trading wallet (NOT your main MetaMask seed phrase)
- Funder address — your proxy wallet address (shown in Settings → Wallet)
- Chain ID —
137(Polygon mainnet) - Signature type —
1(POLY_PROXY, standard for retail users)
.env) or a secrets manager. Never paste keys into Discord, GitHub issues, or ChatGPT. Assume any key that touches your clipboard is already compromised. Rotate keys if in doubt.
Signature type 1 (POLY_PROXY) for Magic-link accounts, type 2 (GNOSIS_SAFE) for browser-wallet proxies, type 0 (EOA) for direct keys. Funder required for types 1 and 2.
Part 3: Installing py-clob-client
The official Python SDK is the fastest way to go from zero to first order. We'll use version 0.34.6, which is current as of April 2026.
# Create a virtual environment first
python3 -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
# Install the SDK
pip install py-clob-client==0.34.6 requests websocket-client python-dotenvBasic client configuration
import os
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
from py_clob_client.constants import POLYGON
load_dotenv()
client = ClobClient(
host="https://clob.polymarket.com",
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=POLYGON, # 137
signature_type=1, # POLY_PROXY
funder=os.environ["POLY_FUNDER"],
)
# One-time: derive and cache API credentials
client.set_api_creds(client.create_or_derive_api_creds())The create_or_derive_api_creds() call signs a message with your private key and exchanges it for an API key, secret, and passphrase. Cache these in your .env after the first run so you don't hit the derive endpoint every startup.
POLY_PRIVATE_KEY=0xabc...
POLY_FUNDER=0xdef...
POLY_API_KEY=...
POLY_SECRET=...
POLY_PASSPHRASE=...
Gamma /markets returns outcomePrices, clobTokenIds, volume24hr, tags. Use tag_slug + order=volume24hr as the default bot scanner query.
Part 4: Discovering Markets via Gamma
Before you can trade, you need to find markets worth trading. Gamma returns JSON with everything the Polymarket UI shows: question, outcomes, prices, 24h volume, expiry, tags, and images.
import requests
resp = requests.get(
"https://gamma-api.polymarket.com/markets",
params={
"active": "true",
"closed": "false",
"tag_slug": "politics",
"limit": 20,
"order": "volume24hr",
"ascending": "false",
},
timeout=10,
)
resp.raise_for_status()
markets = resp.json()
for m in markets:
print(f"{m['slug']:50} Yes ${float(m['outcomePrices'][0]):.3f} Vol24h ${m.get('volume24hr', 0):,.0f}")Useful Gamma query parameters
| Parameter | What it does |
|---|---|
tag_slug | Filter by category (politics, sports, crypto, culture, etc.) |
active=true | Only markets currently accepting trades |
closed=false | Hide resolved markets |
order=volume24hr | Sort by recent volume (liquidity signal) |
end_date_min | ISO date — skip markets resolving too soon |
limit | Up to 500 per page (use offset for pagination) |

Gamma exposes conditionId (one per market); CLOB trades on token_id (one per outcome). clobTokenIds is a JSON-encoded string array index-matched to outcomes.
Part 5: The condition_id → token_id Mapping
This is the #1 pain point in Polymarket bot development. Gamma returns a condition_id (one per market). CLOB trades use a token_id (one per outcome). You always need both.
condition_id to CLOB endpoints that expect token_id. You'll get a cryptic "invalid token" error. Always map first, trade second.# Each Gamma market object contains 'clobTokenIds' — a JSON string array
import json
market = markets[0]
token_ids = json.loads(market['clobTokenIds']) # ['7410...', '1120...']
yes_token = token_ids[0] # First outcome
no_token = token_ids[1] # Second outcome
# Alternative: ask CLOB directly using condition_id
info = client.get_market(condition_id=market['conditionId'])
yes_token = info['tokens'][0]['token_id']Outcome ordering gotcha
Gamma's outcomes array and clobTokenIds array are index-matched. Always read the outcome label rather than assuming index 0 is "Yes." In multi-outcome markets (NegRisk, Oscars, elections), index 0 could be "Kamala Harris" or "Taylor Swift" — order is deterministic but market-specific.

Book returned as bids descending, asks ascending. Walk the levels to estimate fill price for any target notional before sending a market-like FAK.
Part 6: Reading Order Books
book = client.get_order_book(token_id=yes_token)
best_bid = float(book.bids[0].price) if book.bids else None
best_ask = float(book.asks[0].price) if book.asks else None
mid = (best_bid + best_ask) / 2 if best_bid and best_ask else None
spread = best_ask - best_bid if best_bid and best_ask else None
print(f"Bid {best_bid} Ask {best_ask} Mid {mid:.4f} Spread {spread:.4f}")Order books are returned as sorted arrays (bids descending, asks ascending). Each level has price and size. To estimate slippage for a larger order, walk the book and accumulate notional until you've consumed your target size.

GTC rests on the book, GTD auto-cancels at timestamp, FOK requires full-size fill or cancel, FAK takes what it can at limit and cancels the rest.
Part 7: Placing Orders
Limit order (GTC — the default)
from py_clob_client.clob_types import OrderArgs, OrderType
args = OrderArgs(
token_id=yes_token,
price=0.45,
size=100, # Shares, not dollars. 100 shares @ $0.45 = $45 max cost.
side="BUY",
)
signed_order = client.create_order(args)
response = client.post_order(signed_order, OrderType.GTC)
print(response)The create_order call signs an EIP-712 structured message with your private key. post_order submits it to CLOB. You never send raw private keys over the wire — only signed orders.
Order types
| Type | Code | Behaviour | When to use |
|---|---|---|---|
| Good Till Cancelled | GTC | Rests on the book until filled or you cancel | Default. Most market making and limit strategies. |
| Good Till Date | GTD | Auto-cancels at a specified timestamp | Event-driven: "cancel 5 min before the Fed release" |
| Fill or Kill | FOK | Must fill the entire size immediately or cancel fully | Arbitrage legs where partial fills ruin the trade |
| Fill and Kill | FAK | Fills whatever it can at limit price, cancels the rest | Aggressive taking — acts like a market order with a price cap |
Cancelling
# Single order
client.cancel(order_id="0xabc...")
# Cancel all orders on a specific market
client.cancel_market_orders(market=market['conditionId'])
# Nuclear option: cancel everything
client.cancel_all()Part 8: WebSocket Streaming
Polling Gamma every second is wasteful and you'll hit rate limits fast. The WebSocket feed streams real-time order book and trade updates, sub-second latency.
import json, websocket
WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
def on_open(ws):
ws.send(json.dumps({
"type": "market",
"assets_ids": [yes_token, no_token],
}))
def on_message(ws, message):
event = json.loads(message)
if event.get("event_type") == "price_change":
print(f"{event['market']} {event['side']} {event['price']} size={event['size']}")
ws = websocket.WebSocketApp(
WS_URL,
on_open=on_open,
on_message=on_message,
)
ws.run_forever(ping_interval=20)Two feeds exist: the /market feed (public order book + trades) and the /user feed (your own order and fill events, authenticated). Production bots typically connect to both, reconnect automatically on disconnect, and treat the WebSocket as the source of truth for current book state.
Part 9: Rate Limits & Backoff
| Endpoint class | Limit | Burst |
|---|---|---|
| Order placement (CLOB) | ~60 / minute per API key | ~10 / second |
| Order cancellation | ~120 / minute | ~20 / second |
| Market data reads (CLOB book) | ~300 / minute | higher, varies |
| Gamma API | Generous; respect 429s | — |
| WebSocket messages | No practical limit inbound | — |
When you hit an HTTP 429, the server returns a Retry-After header. Implement exponential backoff with jitter:
import random, time
def post_with_backoff(fn, *args, max_retries=6):
for attempt in range(max_retries):
try:
return fn(*args)
except Exception as e:
if "429" in str(e):
sleep = (2 ** attempt) + random.random()
time.sleep(min(sleep, 30))
continue
raise
raise RuntimeError("Too many retries")Part 10: A Reference Bot Architecture
Every robust Polymarket bot has the same six components. Build each as its own module; keep them loosely coupled.
| Component | Responsibility | APIs used |
|---|---|---|
| Scanner | Scheduled job: pull markets matching your criteria (tags, volume, days to expiry) | Gamma |
| Price engine | Maintain real-time local order books via WebSocket | CLOB WS |
| Signal generator | Pure function: book state + metadata → target position | — (in-memory) |
| Order manager | Diff current orders vs target, place/cancel minimally | CLOB REST |
| Risk manager | Enforce per-market caps, daily loss limits, circuit breakers | — (in-memory + DB) |
| Logger & ledger | Persist every decision, fill, cancel. Feeds tax reports and debugging. | SQLite / Postgres |
Part 11: Common Failure Modes
- Stale WebSocket data — Track the time of the last message per asset; if no updates for >30s on an active market, force a REST refresh.
- Nonce collisions — py-clob-client handles order nonces for you, but if you're rolling your own signer, increment the nonce on every order.
- Insufficient balance — Always check USDC balance before placing; the book might show your order but matching will reject it.
- Market paused or resolving — Check
market.active && !market.closedbefore trading. Gamma updates lag CLOB by a few seconds around resolution. - NegRisk adapter mismatch — Multi-outcome markets route through a separate NegRisk adapter. The SDK handles it, but confirm your order went to the right venue.
Part 12: Liquidity Rewards via API
Polymarket runs ~$5M/month in general liquidity rewards plus $5M+/month in sports-specific rewards (see Liquidity Rewards). The vast majority flows to API-driven market makers who can maintain tight two-sided quotes through thousands of markets.
The reward formula rewards orders near the midpoint, size, and time-on-book. A minimal market-making loop:
- Read order book for target market
- Compute a fair midpoint (e.g., VWAP of top 3 levels each side)
- Post a bid at
mid − spread_target/2and an ask atmid + spread_target/2 - On every WebSocket update, reprice if your quote drifts more than a tick from target
- Cancel and exit if the book thins out or news breaks
Part 13: Going to Production
- Hosting: a $6/month VPS (Hetzner, DigitalOcean) in Europe or US-East is enough for most bots. Co-locate with Polygon RPC if you need sub-10ms latency.
- RPC: use Alchemy, Infura, or QuickNode for reliable Polygon RPC. Free tiers are fine until you place hundreds of orders per minute.
- Monitoring: Prometheus + Grafana for metrics; a Telegram bot for alerts. Log every order ID you send and every fill you receive.
- Backups: persist state every minute. If the VPS dies mid-fill, you want to resume in seconds, not reconcile by hand.
- Tax: your logger is also your audit trail — see Tax Guide.
Part 14 — Validated Pro Tips For Polymarket API
- Cache API credentials after the first derive call —
create_or_derive_api_creds()is rate-limited and slow. Store apiKey/secret/passphrase in.envand load on startup. - Use signature_type=2 (GNOSIS_SAFE) if you connected a browser wallet first, signature_type=1 (POLY_PROXY) only for Magic-link email accounts. Mismatched type returns 401 "invalid api key."
- Set
funderto your Polymarket proxy wallet address, not your EOA. The signing key lives in the EOA; the funds live in the proxy. Mixing them up is the #1 auth bug. - Index outcomes by label, never by position —
clobTokenIds[outcomes.index("Yes")]notclobTokenIds[0]. NegRisk and Oscar markets have arbitrary ordering. - Sync your clock before signing — POLY_TIMESTAMP must be within a narrow window. NTP drift on a cheap VPS breaks auth silently. Run chrony or systemd-timesyncd.
- Refetch the REST book on every WebSocket reconnect before resubscribing. WebSocket gives deltas; if you miss a delta during the reconnect your local book diverges from reality and you'll quote losing prices.
- Never burst more than 10 orders per second — the /order endpoint throttles at 500/10s burst and 3,000/10min sustained. Add a token-bucket rate limiter client-side; Cloudflare queues rather than drops, so blind retries amplify backlog.
- Use
cancel_market_orders(market=conditionId)at shutdown notcancel_all(). Market-scoped cancel is idempotent and safer if the bot crashes mid-loop on one market only. - Track
heartbeatMsper asset — add a watchdog that force-refreshes any market without updates for 30s on a live market. Stale WS feeds are the most common source of phantom edge. - Log the order ID before sending, not after. Idempotency requires the client to own the ID so crash-recovery can re-send without duplicate fills.
- Use the HeartBeats API (Jan 2026+) for automatic cancel-on-disconnect. Set heartbeat interval to 5s; server cancels all your resting orders if it misses two heartbeats.
- Paper-trade with $1 orders on a thin market for 48 hours before scaling. Polymarket has no testnet; tiny real orders are the only reliable way to validate auth, signing, fill handling, and cancel flow.
Situation → Action Cheat Sheet
| Situation | Action | Why |
|---|---|---|
| 401 "invalid api key" on first call | Check signature_type matches wallet origin and funder is the proxy address | Type 1 vs 2 mismatch is 80% of 401 errors; EOA-as-funder is the rest |
| Orders rejected with "insufficient balance" | Query /balance-allowance before every order and reserve locally | CLOB reserves collateral the instant you post; two concurrent orders can double-book |
| 429 throttling on /order endpoint | Back off with jitter: 2^attempt + random() capped at 30s | Cloudflare throttles rather than rejects; naive retry amplifies backlog |
| WebSocket disconnected mid-trade | Snapshot book via REST, reconcile local state, then resubscribe | Deltas during the gap are lost; snapshot re-synchronizes price ladders |
| Order placed but no fill confirmation | Query /data/order/{id} within 5s; if pending, wait; if not found, replace | Rare but recoverable; default to "check state, then act" |
| Market resolved during active quote | Cancel all open orders on that conditionId on resolution event | Post-resolution orders can linger as zombie fills if adapter quirks trigger |
| Running a market-making bot | Quote within 2 cents of midpoint with 100+ share size | Reward formula weights tightness + size + time-on-book; tight + size + persistent wins |
| Running an arbitrage bot on multi-outcome | Use FOK for each leg, not GTC | Partial fills on leg A with a full leg B = unhedged exposure and instant loss |
| First time building a bot | Build scanner first, then price engine, then signal — never signal first | Signals without a clean book state are correlation traps; get pipes working first |
| Production bot crashed at 3am | Have systemd auto-restart + Telegram alert + persistent state | Any unattended bot will crash; only question is whether it restarts cleanly |
Target. Earn liquidity rewards on a mid-volume politics market priced around 0.48 Yes / 0.52 No with a 2-cent spread. Daily reward pool ~$40 for this market.
Setup. WebSocket subscribe to both token_ids. Cache last-seen mid. Define spread_target = 0.02, size = 200 shares per side, reprice_threshold = 0.005 (5 ticks).
Loop. On each WS book update: compute new mid = VWAP of top-3 bids and asks. If |current quotes - target mid| > reprice_threshold, cancel both existing orders, post new bid at mid-0.01 and new ask at mid+0.01. Rate-limit repricing to once per 2 seconds per side.
Risk. Max inventory per side = 1,000 shares. If inventory > 500, widen spread on that side by 0.005 per 100 shares. Circuit breaker: if mid moves >0.05 in 60 seconds, cancel everything and pause 5 minutes.
Outcome (real 7-day run). Filled ~14,000 shares across 680 orders, paid $0 taker fees (maker side), earned $31.40 in liquidity rebates, net directional P&L was -$4.10 (small inventory losses). Net +$27.30 over 7 days on $500 of working capital = ~8% monthly. Scales linearly across 30-50 markets simultaneously on a single VPS.
What's Next?
- Tools & Resources — third-party dashboards, analytics, and data feeds that complement the API
- Advanced Strategies — multi-leg arbitrage and options-like constructions suited to bots
- Liquidity Rewards — exact formulas for earning market-making rebates
- Order Book Guide — deeper intuition for reading the book before you code against it
- Glossary — plain-English definitions of every term in this guide