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.

What you'll learn: how the three APIs fit together, how to install and configure py-clob-client, how to authenticate with your proxy wallet, how to fetch markets and order books, how to place and cancel orders, how to stream real-time price updates over WebSocket, the exact rate limits and how to back off cleanly, and a production-ready bot architecture you can extend.
Prerequisites: a funded Polymarket account with at least one manual trade completed, Python 3.8+ (or Node.js), and basic familiarity with HTTP, JSON, and async code. If you haven't traded manually yet, start with First Trade before wiring up a bot.
Architecture diagram: CLOB, Gamma, and Data APIs

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.

APIBase URLPurposeAuth Required
CLOB APIclob.polymarket.comPlace, cancel, and track orders. Read order books. Query positions.Yes (for trading)
Gamma APIgamma-api.polymarket.comBrowse markets, fetch metadata, images, outcome prices, volume, expiry, tags.No (public)
Data APIdata-api.polymarket.comHistorical 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."

Pro tip: Gamma and Data require no authentication. You can explore them with curl or a browser right now — no account needed. This is a great way to prototype before you even generate an API key.
L1 EIP-712 plus L2 HMAC-SHA256 authentication flow

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 ID137 (Polygon mainnet)
  • Signature type1 (POLY_PROXY, standard for retail users)
Security non-negotiables: never commit your private key to git. Use environment variables (.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.
py-clob-client 0.34.6 installation and ClobClient configuration

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-dotenv

Basic 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.

Worked example — minimal .env:
POLY_PRIVATE_KEY=0xabc...
POLY_FUNDER=0xdef...
POLY_API_KEY=...
POLY_SECRET=...
POLY_PASSPHRASE=...
Gamma API market query parameters and response shape

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

ParameterWhat it does
tag_slugFilter by category (politics, sports, crypto, culture, etc.)
active=trueOnly markets currently accepting trades
closed=falseHide resolved markets
order=volume24hrSort by recent volume (liquidity signal)
end_date_minISO date — skip markets resolving too soon
limitUp to 500 per page (use offset for pagination)
condition_id to token_id mapping between Gamma and CLOB

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.

The mistake: passing 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.

CLOB order book with bid/ask levels and slippage walk

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.

Comparison of GTC, GTD, FOK, FAK order types

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

TypeCodeBehaviourWhen to use
Good Till CancelledGTCRests on the book until filled or you cancelDefault. Most market making and limit strategies.
Good Till DateGTDAuto-cancels at a specified timestampEvent-driven: "cancel 5 min before the Fed release"
Fill or KillFOKMust fill the entire size immediately or cancel fullyArbitrage legs where partial fills ruin the trade
Fill and KillFAKFills whatever it can at limit price, cancels the restAggressive 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.

Heartbeats and reconnects: send a ping every 20 seconds. If you miss two pongs, reconnect. On reconnect, always re-fetch the order book via REST first, then resubscribe — otherwise your local book drifts from reality.

Part 9: Rate Limits & Backoff

Endpoint classLimitBurst
Order placement (CLOB)~60 / minute per API key~10 / second
Order cancellation~120 / minute~20 / second
Market data reads (CLOB book)~300 / minutehigher, varies
Gamma APIGenerous; respect 429s
WebSocket messagesNo 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.

ComponentResponsibilityAPIs used
ScannerScheduled job: pull markets matching your criteria (tags, volume, days to expiry)Gamma
Price engineMaintain real-time local order books via WebSocketCLOB WS
Signal generatorPure function: book state + metadata → target position— (in-memory)
Order managerDiff current orders vs target, place/cancel minimallyCLOB REST
Risk managerEnforce per-market caps, daily loss limits, circuit breakers— (in-memory + DB)
Logger & ledgerPersist every decision, fill, cancel. Feeds tax reports and debugging.SQLite / Postgres
Reliability first: before optimising for PnL, make sure your bot can restart cleanly at 3 AM on a Sunday without a human. That means idempotent order placement (use client-side order IDs), persistent state, and automated alerts (Telegram, Discord, PagerDuty) for any unhandled exception.

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.closed before 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.
Testnet limitation: Polymarket does not operate a public testnet in 2026. "Paper trading" means placing tiny real orders ($1–$5) on low-liquidity markets. Budget a few dollars for your first week of debugging — it will save you hundreds later.

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:

  1. Read order book for target market
  2. Compute a fair midpoint (e.g., VWAP of top 3 levels each side)
  3. Post a bid at mid − spread_target/2 and an ask at mid + spread_target/2
  4. On every WebSocket update, reprice if your quote drifts more than a tick from target
  5. 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

Twelve production habits from live bot operators.
  1. Cache API credentials after the first derive callcreate_or_derive_api_creds() is rate-limited and slow. Store apiKey/secret/passphrase in .env and load on startup.
  2. 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."
  3. Set funder to 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.
  4. Index outcomes by label, never by positionclobTokenIds[outcomes.index("Yes")] not clobTokenIds[0]. NegRisk and Oscar markets have arbitrary ordering.
  5. 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.
  6. 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.
  7. 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.
  8. Use cancel_market_orders(market=conditionId) at shutdown not cancel_all(). Market-scoped cancel is idempotent and safer if the bot crashes mid-loop on one market only.
  9. Track heartbeatMs per 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.
  10. 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.
  11. 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.
  12. 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

SituationActionWhy
401 "invalid api key" on first callCheck signature_type matches wallet origin and funder is the proxy addressType 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 locallyCLOB reserves collateral the instant you post; two concurrent orders can double-book
429 throttling on /order endpointBack off with jitter: 2^attempt + random() capped at 30sCloudflare throttles rather than rejects; naive retry amplifies backlog
WebSocket disconnected mid-tradeSnapshot book via REST, reconcile local state, then resubscribeDeltas during the gap are lost; snapshot re-synchronizes price ladders
Order placed but no fill confirmationQuery /data/order/{id} within 5s; if pending, wait; if not found, replaceRare but recoverable; default to "check state, then act"
Market resolved during active quoteCancel all open orders on that conditionId on resolution eventPost-resolution orders can linger as zombie fills if adapter quirks trigger
Running a market-making botQuote within 2 cents of midpoint with 100+ share sizeReward formula weights tightness + size + time-on-book; tight + size + persistent wins
Running an arbitrage bot on multi-outcomeUse FOK for each leg, not GTCPartial fills on leg A with a full leg B = unhedged exposure and instant loss
First time building a botBuild scanner first, then price engine, then signal — never signal firstSignals without a clean book state are correlation traps; get pipes working first
Production bot crashed at 3amHave systemd auto-restart + Telegram alert + persistent stateAny unattended bot will crash; only question is whether it restarts cleanly
Worked example: minimal market-maker loop for liquidity rewards.

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