Back to Blog
Prediction Markets

Build a Polymarket Trading Bot with Python: From Market Scanning to Automated Execution

Complete guide to building a Polymarket trading bot in Python: three APIs, crypto market scanning, spread filtering, and automated trade execution.

Robot TradersMarch 8, 20267 min read
Polymarket logo, Python logo, and gear icon connected by arrows in a cycle over a blurred Polymarket interface background.

Polymarket lists hundreds of active prediction markets at any given time. A handful of them have YES outcomes priced between 15 and 40 cents, where the crowd considers the event unlikely. Checking each one manually, evaluating spread quality, filtering by volume: this is repetitive work that a Python script handles in seconds. And unlike a human, the script places the trade the moment conditions are met.

This tutorial builds a complete Polymarket trading bot in under 200 lines of Python. The bot connects to Polymarket's three API endpoints, scans for crypto markets matching a contrarian strategy, filters out illiquid or wide-spread opportunities, and places trades while enforcing position limits. One command, fully logged output, no manual intervention.

The Polymarket API tutorial covers the fundamentals: endpoints, authentication, order books, and manual order placement. This article picks up from there and turns those building blocks into a self-contained automated trading system.

All resources for this tutorial:

Bot Architecture: Three Layers

The bot separates concerns into three layers, each with a single responsibility:

LayerRoleFunctions
API WrappersTalk to Polymarketget_markets, get_balance, get_price, get_positions, place_order
StrategyDecide what to tradefind_markets, should_trade
Main LoopOrchestrate executionmain

Each layer only calls the one below it. The strategy layer uses API wrappers but never constructs HTTP requests. The main loop calls strategy functions but never parses raw API responses. This separation makes it straightforward to swap the strategy (different price ranges, different market categories) without touching the API or execution code.

Setup and Configuration

Dependencies

Three packages power the bot:

pip install py-clob-client requests python-dotenv

py-clob-client is the open-source Python wrapper for Polymarket's CLOB API, handling order signing and the underlying cryptographic layer. requests handles direct HTTP calls to the Gamma and Data APIs, and python-dotenv loads credentials from a .env file so they stay out of the source code.

Credentials

The bot needs two values from your Polymarket account: your wallet address and your private key. Store them in a .env file in the same directory as the script:

POLYMARKET_PRIVATE_KEY=your_private_key_here POLYMARKET_FUNDER_ADDRESS=your_wallet_address_here

Your private key is exported from Polymarket's account settings (the authentication section of the API tutorial walks through the process). This key grants full access to your funds; keep it out of version control and never share it.

Configuration and Client Initialisation

The script loads credentials, sets trading parameters, and creates the authenticated client:

import os import json import requests from datetime import datetime from dotenv import load_dotenv from py_clob_client.client import ClobClient from py_clob_client.clob_types import ( OrderArgs, MarketOrderArgs, OrderType, BalanceAllowanceParams, AssetType, ) from py_clob_client.order_builder.constants import BUY, SELL load_dotenv() PRIVATE_KEY = os.getenv("POLYMARKET_PRIVATE_KEY") FUNDER_ADDRESS = os.getenv("POLYMARKET_FUNDER_ADDRESS") SIGNATURE_TYPE = 1 # 0 = MetaMask/hardware, 1 = email, 2 = browser proxy TRADE_SIZE = 0.5 # Fraction of balance per trade MARKETS_LIMIT = 100 # Max markets to fetch per scan MAX_POSITIONS = 3 # Cap on simultaneous positions GAMMA_API = "https://gamma-api.polymarket.com" DATA_API = "https://data-api.polymarket.com" CLOB_API = "https://clob.polymarket.com" client = ClobClient( CLOB_API, key=PRIVATE_KEY, chain_id=137, # Polygon signature_type=SIGNATURE_TYPE, funder=FUNDER_ADDRESS, ) creds = client.derive_api_key() client.set_api_creds(creds)

Three constants control how the bot manages risk:

  • TRADE_SIZE = 0.5 — each trade uses 50% of the available balance, but total exposure is capped by MAX_POSITIONS
  • MARKETS_LIMIT = 100 — the scan pulls up to 100 active markets per run
  • MAX_POSITIONS = 3 — the bot stops placing trades once it holds three open positions

API Wrappers: The Bot's Interface to Polymarket

Five functions handle all communication between the bot and Polymarket's endpoints.

The Gamma API serves market metadata: questions, outcome prices, volumes, and token identifiers. This wrapper accepts keyword filters passed directly as query parameters:

def get_markets(**filters): params = {"limit": MARKETS_LIMIT, "active": True, "closed": False} params.update(filters) response = requests.get(f"{GAMMA_API}/markets", params=params) return response.json()

Polymarket stores USDC balances as raw integers with 6 decimal places. The balance function converts to a readable dollar amount:

def get_balance(): balance = client.get_balance_allowance( BalanceAllowanceParams(asset_type=AssetType.COLLATERAL) ) return int(balance["balance"]) / 1e6

The pricing function returns four metrics in one call: midpoint (average of best bid and best ask), best ask, best bid, and spread:

def get_price(token_id): return { "midpoint": float(client.get_midpoint(token_id)["mid"]), "best_ask": float(client.get_price(token_id, side="BUY")["price"]), "best_bid": float(client.get_price(token_id, side="SELL")["price"]), "spread": float(client.get_spread(token_id)["spread"]), }

The Data API exposes any wallet's open positions, since this is public blockchain data. Without an address argument, the function defaults to the bot's own wallet:

def get_positions(address=None): addr = address or FUNDER_ADDRESS response = requests.get(f"{DATA_API}/positions", params={"user": addr}) positions = response.json() print(f"{datetime.now().strftime('%H:%M:%S')} - {len(positions)} open positions") return positions

A single function handles both market and limit orders. Without a price argument, it sends a Fill-or-Kill market order (spend a dollar amount at the best available price). With a price, it sends a Good-Till-Cancelled limit order (buy a number of shares at a target price):

def place_order(token_id, side, amount, price=None): if price is None: order = MarketOrderArgs( token_id=token_id, amount=amount, side=side, order_type=OrderType.FOK ) signed = client.create_market_order(order) resp = client.post_order(signed, OrderType.FOK) else: order = OrderArgs( token_id=token_id, price=price, size=amount, side=side ) signed = client.create_order(order) resp = client.post_order(signed, OrderType.GTC) return resp

In both cases, order placement follows two steps: create_market_order (or create_order) signs the order with your credentials, then post_order submits it to the exchange.

Sign Up

Strategy: Finding Underpriced Prediction Markets

The strategy layer defines what the bot considers worth trading. This implementation targets crypto prediction markets where the YES outcome trades between 15 and 40 cents.

A YES price of $0.25 means the crowd assigns a 25% probability to the event. The contrarian thesis: within this range, some outcomes are systematically underpriced, and buying YES on enough of them yields positive expected value over time. Not every individual bet wins; prediction markets are probabilistic by nature. But if the pricing inefficiency is real, a diversified set of positions across multiple markets should be net positive over a large enough sample.

The find_markets function scans Polymarket's crypto category and filters the results:

def find_markets(): min_price = 0.15 max_price = 0.40 min_volume = 10000 markets = get_markets(tag_id=21) # Crypto markets candidates = [] for m in markets: prices = json.loads(m.get("outcomePrices", "[]")) volume = float(m.get("volume24hr", 0)) if len(prices) >= 2: yes_price = float(prices[0]) if min_price <= yes_price <= max_price and volume >= min_volume: token_ids = json.loads(m["clobTokenIds"]) m["yes_token_id"] = token_ids[0] m["no_token_id"] = token_ids[1] candidates.append(m) candidates.sort(key=lambda m: float(m.get("volume24hr", 0)), reverse=True) print(f"{datetime.now().strftime('%H:%M:%S')} - Found {len(candidates)} markets matching strategy") return candidates

Each filter serves a specific purpose:

  • Price range (15–40 cents) — below 15 cents, even correct bets rarely pay enough to justify the risk. Above 40 cents, the upside diminishes. This window targets the strongest risk-to-reward ratio for a bullish contrarian approach.
  • 24-hour volume above $10,000 — markets with low volume have wide spreads and poor fills. The threshold ensures enough liquidity to enter and exit without excessive slippage.
  • Category: crypto (tag_id=21) — restricts the scan to crypto-related markets. Other categories include politics (tag_id=2) and finance (tag_id=120); the full list is at gamma-api.polymarket.com/tags.

Candidates are sorted by descending volume, so the most liquid markets get evaluated first.

Spread Guard

Before placing any trade, the bot checks one final condition: is the spread (gap between best bid and best ask) acceptable?

def should_trade(price_data): return price_data["spread"] < 0.05

A 5-cent spread on a 25-cent token means losing 20% on an immediate round trip. Markets where the spread exceeds 5 cents get skipped; the execution cost would eat the theoretical edge.

The Main Loop: Scan, Filter, Execute

The main function ties everything together. It scans for candidates, checks the bot's current state, and walks through each market with four guards before committing capital:

def main(): ts = lambda: datetime.now().strftime("%H:%M:%S") print(f"{ts()} - Scanning markets...") markets = find_markets() if not markets: print(f"{ts()} - No markets match strategy criteria") print(f"{ts()} - Done.") return balance = get_balance() print(f"{ts()} - Balance: ${balance:.2f}") positions = get_positions() held_tokens = {p["asset"] for p in positions} amount = round(balance * TRADE_SIZE, 2) for market in markets: print(f"\n{ts()} - --- {market['question']} ---") if len(held_tokens) >= MAX_POSITIONS: print(f"{ts()} - Max positions reached, stopping") break if market["yes_token_id"] in held_tokens or market["no_token_id"] in held_tokens: print(f"{ts()} - Already in this market, skipping") continue price_data = get_price(market["yes_token_id"]) print(f"{ts()} - YES price: {price_data['best_ask']:.2f} | Spread: {price_data['spread']:.2f}") if not should_trade(price_data): print(f"{ts()} - Spread too wide, skipping") continue print(f"{ts()} - Placing order...") place_order(market["yes_token_id"], BUY, amount=amount) held_tokens.add(market["yes_token_id"]) print(f"{ts()} - Trade executed") print(f"\n{ts()} - Done.") if __name__ == "__main__": main()

Each candidate passes through four sequential checks:

  1. Position cap — once the bot holds MAX_POSITIONS tokens, it stops entirely. This is a hard limit on portfolio concentration.
  2. Duplicate filter — if the bot already holds the YES or NO token for this market, it skips to avoid doubling down on the same bet.
  3. Live price refresh — the strategy filtered on snapshot prices from the Gamma API, but the actual best ask and spread may have shifted. The bot re-checks live pricing from the CLOB API before trading.
  4. Spread guard — markets above the 5-cent spread threshold get skipped to protect execution quality.

Every decision is logged with a timestamp. After a run, you can trace exactly which markets the bot evaluated, which it skipped, and why.

Running the Bot

Save the script as polymarket_python_bot.py alongside your .env file and run it with a single command:

python polymarket_python_bot.py

A typical run produces output like this:

14:32:01 - Scanning markets... 14:32:02 - Found 4 markets matching strategy 14:32:02 - Balance: $18.00 14:32:02 - 1 open positions 14:32:03 - --- Will BTC hit $150K by June 2026? --- 14:32:03 - YES price: 0.32 | Spread: 0.02 14:32:04 - Placing order... 14:32:05 - Trade executed 14:32:05 - --- Will ETH flip BTC by market cap in 2026? --- 14:32:06 - YES price: 0.18 | Spread: 0.08 14:32:06 - Spread too wide, skipping 14:32:06 - Done.

The script runs once and exits. For continuous operation, schedule it with cron (Linux/macOS) or Task Scheduler (Windows) at whatever interval fits your strategy.

The parameters (price range, volume floor, spread cap, position limit) define one specific strategy. Adapting them takes minutes. Change tag_id to 2 for politics or 120 for finance. Lower min_price to 0.05 for deeper contrarian bets with higher variance. Raise min_volume to $50,000 for only the most liquid markets. The layered architecture keeps these changes confined to the strategy layer; the API wrappers and main loop stay untouched.

This bot is a starting point, not a production system. Natural extensions include exit logic to sell positions that have moved against you, performance tracking across runs with a local database, a notification layer that alerts you before each trade, or a built-in scheduler using time.sleep. The complete code is on GitHub; fork it and build from there.

Sign Up

Sources

This article contains affiliate links. If you sign up through these links, we may earn a commission at no additional cost to you.

Not investment advice. Crypto trading involves risk of loss. Payward Europe Solutions Limited t/a Kraken is regulated by the Central Bank of Ireland. FOR MORE INFORMATION AND APPLICABLE CONDITIONS OR LIMITATIONS, PLEASE CONSULT https://www.kraken.com/legal/disclosures.

This article contains affiliate links. If you sign up through these links, we may earn a commission at no additional cost to you.