← Back to blog

Polymarket API Python Tutorial: Connect, Fetch Orderbooks, and Place Trades

updated 2026-10-08 polymarket python api tutorial trading

By ZenHodl. Dataset documentation, research and model evaluations are linked in the article. A separate filtered ledger of bot-attributed trades, including losses and its admission rules, is public at /results.

Polymarket runs a Central Limit Order Book (CLOB) — the same architecture as crypto exchanges. If you want to build a trading bot, you need to talk to this API directly. The full API is documented at docs.polymarket.com, and if you're weighing Polymarket against Kalshi, ESPN, or odds aggregators, our prediction market API guide compares all four. Here's how to use it from Python.

Source check: October 8, 2026. Polymarket recommends the unified polymarket-client SDK for new projects. The token-based CLOB examples below target py-clob-client-v2 1.2.0. That package also distinguishes position_id orders, which use a different signing route; do not substitute identifiers or apply one Exchange version to every market. Public GET response shapes were checked separately; authenticated orders, cancellation and redemption were not executed for this article.

Setup

Public market listings and books require no account or signing key. The order examples additionally require an eligible account, matching wallet/signature path, CLOB credentials and pUSD collateral. Deposit wallets are the current default account wallet; legacy proxy and Safe accounts differ. This tutorial retains the CLOB-specific client for its existing token-based examples:

pip install py-clob-client-v2==1.2.0 requests

CLOB V2 became production on April 28, 2026. The archived py-clob-client package’s legacy order-signing flow is unsupported in production, even though the hostname remains clob.polymarket.com.

Connecting the Client

For public reads only:

from py_clob_client_v2 import ClobClient

HOST = "https://clob.polymarket.com"
client = ClobClient(host=HOST, chain_id=137)

The following replaces that public client with an authenticated deposit-wallet client. It reads your credentials; it was not executed in this source review.

import os
from py_clob_client_v2 import ApiCreds, ClobClient, SignatureTypeV2

HOST = "https://clob.polymarket.com"

api_creds = ApiCreds(
    api_key=os.environ["POLYMARKET_API_KEY"],
    api_secret=os.environ["POLYMARKET_API_SECRET"],
    api_passphrase=os.environ["POLYMARKET_API_PASSPHRASE"],
)

client = ClobClient(
    host=HOST,
    key=os.environ["POLYMARKET_PRIVATE_KEY"],
    chain_id=137,  # Polygon mainnet
    creds=api_creds,
    signature_type=SignatureTypeV2.POLY_1271,
    funder=os.environ["POLYMARKET_DEPOSIT_WALLET"],
)

Always use environment variables or a secret manager for credentials. Never hardcode keys. The example uses signature type 3 (POLY_1271), the deposit-wallet path recommended for new API users. Existing POLY_PROXY and Gnosis Safe users should use their existing funder with signature type 1 or 2; standalone EOAs use type 0. See Polymarket's current authentication guide before choosing.

If you have not created CLOB API credentials, derive them with the same wallet key before constructing the fully authenticated client:

bootstrap = ClobClient(
    host=HOST,
    chain_id=137,
    key=os.environ["POLYMARKET_PRIVATE_KEY"],
)
api_creds = bootstrap.create_or_derive_api_key()

Finding Sports Markets

For the CTF token-based flow shown here, a market has a condition identifier and outcome token identifiers. Newer position-backed markets have a separate identifier system; inspect the market version before selecting an SDK order field. This discovery example reads one NBA page, not the entire league catalog:

import json
import requests

# Resolve a league slug to the numeric tag ID required by market listings.
tag_response = requests.get(
    "https://gamma-api.polymarket.com/tags/slug/nba",
    timeout=10,
)
tag_response.raise_for_status()
tag = tag_response.json()
page_response = requests.get(
    "https://gamma-api.polymarket.com/markets/keyset",
    params={"tag_id": tag["id"], "closed": "false", "limit": 50},
    timeout=10,
)
page_response.raise_for_status()
page = page_response.json()

for market in page["markets"]:
    if market.get("version") != "v1":
        continue  # this example trades CTF tokens, not newer position IDs
    condition_id = market.get("conditionId")
    encoded_token_ids = market.get("clobTokenIds")
    if not condition_id or not encoded_token_ids:
        continue  # not yet listed on the CLOB
    token_ids = json.loads(encoded_token_ids) if isinstance(encoded_token_ids, str) else encoded_token_ids
    print(market["question"], condition_id, token_ids)

Change nba to the desired tag and follow next_cursor through after_cursor to collect more pages. The checked raw Gamma response uses conditionId and JSON-encoded clobTokenIds; these raw HTTP dictionaries are not the unified SDK's typed models. An identifier may exist before an executable book does.

Reading the Orderbook

A book response is a timestamped observation of displayed liquidity. It does not reserve that liquidity or guarantee a fill:

token_id = "YOUR_TOKEN_ID_HERE"
book = client.get_order_book(token_id)

bids = book.get("bids", [])
asks = book.get("asks", [])
best_bid = max((float(level["price"]) for level in bids), default=None)
best_ask = min((float(level["price"]) for level in asks), default=None)
if best_bid is None or best_ask is None:
    raise ValueError("Two-sided book unavailable; do not invent a spread")
midpoint = (best_bid + best_ask) / 2
spread = best_ask - best_bid

print(f"Bid: {best_bid}  Ask: {best_ask}  Mid: {midpoint:.4f}  Spread: {spread:.2f}")

Compute the extrema rather than assuming the first array element is best; that keeps the code correct if the venue's level ordering changes.

Crossing both sides of an unchanged 3-cent spread costs about 3 cents per share before platform fees and price impact. A maker fill, changing book or settlement hold has a different cost path. This is why execution quality matters so much.

Placing a Limit Order

This is an authenticated token-based order example, not a dry run. Its price and size are illustrative; validate current tick, minimum size, balance and market status. The review did not submit it.

from py_clob_client_v2 import (
    OrderArgs,
    OrderType,
    PartialCreateOrderOptions,
    Side,
)

response = client.create_and_post_order(
    order_args=OrderArgs(
        token_id=token_id,
        price=0.63,  # pUSD per share
        size=50,     # shares
        side=Side.BUY,
    ),
    options=PartialCreateOrderOptions(
        tick_size=str(book["tick_size"]),
        neg_risk=bool(book["neg_risk"]),
    ),
    order_type=OrderType.GTC,
)
print("Order response (check acceptance, status and fills):", response)

Orders sit on the book until filled, canceled, expired, or otherwise removed. CLOB V2 determines platform fees per market at match time: makers have a zero platform fee, while takers pay the selected market's price-dependent fee when fees are enabled. Query the market configuration instead of hardcoding “2%.”

Managing Orders

from py_clob_client_v2 import OrderPayload

# Check open orders
for order in client.get_open_orders():
    print(f"{order['side']} {order['original_size']} @ {order['price']}  "
          f"filled: {order['size_matched']}")

# Cancel a single order
client.cancel_order(OrderPayload(orderID="YOUR_ORDER_ID"))

# Cancel everything
client.cancel_all()

Polling Loop for a Trading Bot

The loop below is a signal sketch: your_model and game_state are application placeholders, and its thresholds are example inputs, not validated trading rules.

import time

while True:
    book = client.get_order_book(token_id)
    bids = book.get("bids", [])
    asks = book.get("asks", [])
    best_bid = max((float(level["price"]) for level in bids), default=None)
    best_ask = min((float(level["price"]) for level in asks), default=None)
    if best_bid is None or best_ask is None:
        time.sleep(5)
        continue
    spread = best_ask - best_bid

    model_fair = your_model.predict(game_state)
    edge = model_fair - best_ask

    if edge > 0.08 and spread < 0.04:
        # Signal found — place buy order
        pass

    time.sleep(5)

Five seconds is only this sketch’s polling interval. Select your cadence from endpoint budgets and the strategy’s freshness requirement; the market WebSocket supplies updates but still needs reconnect and reconciliation. Polymarket publishes endpoint-specific REST limits and throttles excess traffic; do not treat one global requests-per-second number as a contract.

Common Pitfalls

Rate limits: Space your calls. Use exponential backoff on 429 responses.

V1 package still installed: If an error trace imports py_clob_client, remove the legacy package and verify that the service interpreter imports py_clob_client_v2.

Wrong wallet mode: The signer, signature type, and funder must describe the same wallet path. New deposit wallets use type 3; old Safe examples that use type 2 are not interchangeable.

Wrong collateral: CLOB V2 uses pUSD. USDC.e sitting in another address does not fund the order.

Stale orderbooks: During score changes, market makers pull quotes. Always check quote freshness before trading.

Token ID confusion: Each YES/NO outcome has a separate token ID. Buying the wrong side is an expensive mistake.

From API to Bot

Connecting to the API is step one. A bot also needs evaluated probability estimates, a cost-aware signal filter and execution/reconciliation rules. Those components do not establish profitability. Our course teaches the workflow in six notebooks.


Part of the ZenHodl blog. We write about sports analytics, prediction markets, and building trading bots with Python.

Related reading

Get ZenHodl Weekly

Dataset releases, research notes, and public results, including corrections.

Research and dataset updates from ZenHodl.

Want to build this yourself?

Work through ESPN data collection, Elo, probability modeling, calibration and backtesting in six Jupyter notebooks. Inspect the free first module and course requirements before buying.