The Short Version
Polymarket gives you three public APIs (an API is a way for programs to read or place trades automatically): CLOB (trading), Gamma (market discovery), and Data (analytics). The official Python SDK is py-clob-client 0.34.6. Auth uses an API key + ECDSA signature, and orders are signed via EIP-712 through a Polygon proxy wallet. Rate limits cap you at roughly 60 orders/minute per key. The 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 all of it by API users.
Part 1: The Three APIs
Polymarket splits the work cleanly across three separate 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."
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 runs 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)
Part 3: Installing py-clob-client
The official Python SDK is the fastest way to go from zero to your first order. We'll use version 0.34.6 - the current release on PyPI (Feb 2026) and what nearly every live bot runs.
# 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. It swaps that for an API key, secret, and passphrase. Cache these in your .env after the first run, so you don't hit the derive endpoint on every startup.
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: the 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) |
Part 5: The condition_id → token_id Mapping
This is the #1 pain point in Polymarket bot building. Gamma returns a condition_id (one per market). CLOB trades use a token_id (one per outcome). You always need both.
# 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. Don't assume index 0 is "Yes." In multi-outcome markets (NegRisk, Oscars, elections), index 0 could be "Kamala Harris" or "Taylor Swift." The order is fixed but specific to each market.
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 come back as sorted arrays (bids descending, asks ascending). Each level has a price and a size. To estimate slippage on a larger order, walk the book and add up notional until you've filled your target size.
Part 6b: The CLOB v2 REST Endpoints (raw, no SDK)
The SDK wraps these, but knowing the raw endpoints lets you debug, use another language, or build a thin client. Base URL: https://clob.polymarket.com. Every read below is public - no auth needed. These were verified live in June 2026.
| Endpoint | Method | What it returns |
|---|---|---|
/markets | GET | All markets (paginated via next_cursor). Includes condition_id, tokens[], minimum_tick_size, neg_risk. |
/sampling-markets | GET | Only markets with a live order book - the fastest way to find tradeable token_ids. |
/book?token_id= | GET | Full order book: bids[] and asks[] with price + size. |
/price?token_id=&side=buy | GET | Best price for a side. side is case-insensitive (buy/BUY). Returns {"price":"0.14"}. |
/midpoint?token_id= | GET | {"mid":"0.21"} - halfway between best bid and ask. |
/spread?token_id= | GET | {"spread":"0.14"} - best ask minus best bid. |
/tick-size?token_id= | GET | {"minimum_tick_size":0.01} - the smallest legal price step for that token. |
/prices-history?market=&interval= | GET | Historical price points. interval = 1m,1h,6h,1d,1w,max. |
/trades | GET | Recent trades (auth for your own; public for market). |
/order | POST | Place a signed order (auth required). |
/order | DELETE | Cancel one order by id (auth). |
/orders | GET | Your resting open orders (auth). |
/balance-allowance?asset_type= | GET | Your USDC balance and on-chain allowance (auth). Check before every order. |
Verified responses, straight from the live API:
$ curl "https://clob.polymarket.com/price?token_id=7347...&side=buy"
{"price":"0.14"}
$ curl "https://clob.polymarket.com/midpoint?token_id=7347..."
{"mid":"0.21"}
$ curl "https://clob.polymarket.com/spread?token_id=7347..."
{"spread":"0.14"}
$ curl "https://clob.polymarket.com/tick-size?token_id=7347..."
{"minimum_tick_size":0.01}L2 auth headers (for raw REST without the SDK)
Read endpoints are public. To place or cancel orders over raw REST you sign each request with your API credentials. The SDK does this for you; here is what it builds under the hood:
| Header | What it carries |
|---|---|
POLY_ADDRESS | Your signing wallet address |
POLY_API_KEY | The API key from create_or_derive_api_creds() |
POLY_PASSPHRASE | The passphrase from the same derive call |
POLY_TIMESTAMP | Current UNIX seconds (must match server clock - see clock-sync tip) |
POLY_NONCE | Per-request nonce |
POLY_SIGNATURE | HMAC-SHA256 of timestamp + method + path + body, keyed with your API secret, base64-url encoded |
Part 7: Placing Orders - Buy and Sell
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. Then post_order submits it to CLOB. You never send raw private keys over the wire - only signed orders.
Align your price to the tick first
Every order price must be an exact multiple of the market's minimum_tick_size (0.01 on most markets, 0.001 on tight ones). An off-tick price is rejected. Fetch the tick once and round to it:
from py_clob_client.clob_types import OrderArgs, OrderType
tick = float(client.get_tick_size(token_id=yes_token)) # e.g. 0.01
def to_tick(p, tick): return round(round(p / tick) * tick, 4)
price = to_tick(0.453, tick) # -> 0.45 on a 0.01 marketBuy
side="BUY", size is in shares (not dollars). 100 shares at $0.45 costs at most $45, and pays $100 if the outcome wins. Minimum order value is about $1.
buy = OrderArgs(token_id=yes_token, price=to_tick(0.45, tick), size=100, side="BUY")
resp = client.post_order(client.create_order(buy), OrderType.GTC)
print(resp) # {'success': True, 'orderID': '0x...', 'status': 'live', ...}Sell
Selling is the same call with side="SELL". You can only sell shares you already hold - selling more than your position is rejected with an "insufficient balance" error. To close a position, sell the same token_id you bought.
sell = OrderArgs(token_id=yes_token, price=to_tick(0.62, tick), size=100, side="SELL")
resp = client.post_order(client.create_order(sell), OrderType.GTC)Order parameters at a glance
| Field | Meaning | Notes |
|---|---|---|
token_id | The outcome you're trading | Not condition_id - see Part 5 |
side | BUY or SELL | BUY needs USDC; SELL needs shares |
price | 0.001-0.999 | Must be a multiple of the tick size |
size | Number of shares | Min ~$1 order value; cost = price x size |
| order type | GTC / GTD / FOK / FAK | Passed to post_order(...) |
Putting it together: your first API trade (one runnable script)
This is the whole flow end to end - connect, find a liquid market, read the book, align to the tick, place a small real order, then cancel it. Fill in your two secrets and run it. Start with a tiny size (a few dollars) on your very first run.
import os, json, requests
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
from py_clob_client.constants import POLYGON
from py_clob_client.clob_types import OrderArgs, OrderType
load_dotenv()
# 1) Connect (signing key in your EOA, funds in your proxy/funder)
client = ClobClient(
"https://clob.polymarket.com",
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=POLYGON, # 137
signature_type=1, # 1 = email/Magic proxy, 2 = browser-wallet proxy
funder=os.environ["POLY_FUNDER"],
)
client.set_api_creds(client.create_or_derive_api_creds()) # cache these after first run
# 2) Find the most-traded open market (Gamma, no auth)
m = requests.get(
"https://gamma-api.polymarket.com/markets",
params={"active": "true", "closed": "false", "order": "volume24hr",
"ascending": "false", "limit": 1}, timeout=10,
).json()[0]
token_id = json.loads(m["clobTokenIds"])[0] # index 0 = first outcome (read the label!)
print("Trading:", m["question"])
# 3) Read the book + the tick size
tick = float(client.get_tick_size(token_id))
book = client.get_order_book(token_id)
best_ask = float(book.asks[0].price)
print("best ask", best_ask, "| tick", tick)
# 4) Place a small BUY at the ask (tiny size to start)
price = round(round(best_ask / tick) * tick, 4) # snap to tick
order = OrderArgs(token_id=token_id, price=price, size=5, side="BUY") # 5 shares
resp = client.post_order(client.create_order(order), OrderType.GTC)
print(resp) # {'success': True, 'orderID': '0x...', ...}
# 5) Cancel it (clean up)
# client.cancel(order_id=resp["orderID"])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 at 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 carries the public order book and trades. The /user feed carries your own order and fill events (authenticated). Production bots connect to both, reconnect automatically on disconnect, and treat the WebSocket as the source of truth for the current book.
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. Use 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 reliable Polymarket bot has the same six components. Build each as its own module, and 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
- Off-tick price rejection - the price must be an exact multiple of the market's
minimum_tick_size. Fetch it via/tick-size?token_id=and round before you sign, or the order bounces. - 404 "No orderbook exists" - you queried
/book,/priceor/midpointon a closed/resolved token. Use/sampling-marketsto find tokens with a live book. - 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). Most of it flows to API-driven market makers. They keep tight two-sided quotes across thousands of markets.
The reward formula favors orders near the midpoint, with size and time-on-book. Here's 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
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 |
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











