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.

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:
- Full bot script — polymarket_python_bot.py on GitHub
- Python SDK — Polymarket Python SDK
- Polymarket documentation — docs.polymarket.com
- API fundamentals — Polymarket API Python Tutorial
- Video walkthrough — Build a Polymarket Trading Bot with Python
Bot Architecture: Three Layers
The bot separates concerns into three layers, each with a single responsibility:
| Layer | Role | Functions |
|---|---|---|
| API Wrappers | Talk to Polymarket | get_markets, get_balance, get_price, get_positions, place_order |
| Strategy | Decide what to trade | find_markets, should_trade |
| Main Loop | Orchestrate execution | main |
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-dotenvpy-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_hereYour 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 byMAX_POSITIONSMARKETS_LIMIT = 100— the scan pulls up to 100 active markets per runMAX_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"]) / 1e6The 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 positionsA 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 respIn 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.
Trading crypto since 2011. Fully regulated. Never hacked. Get 15 USDT in BTC for free.![]()
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 candidatesEach 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 atgamma-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.05A 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:
- Position cap — once the bot holds
MAX_POSITIONStokens, it stops entirely. This is a hard limit on portfolio concentration. - 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.
- 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.
- 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.pyA 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.
The world's #1 prediction market
Sources
- Polymarket, "Polymarket Documentation", 2025
- Polymarket, "py-clob-client", GitHub
- Polymarket, "Gamma API", market data endpoint
- Polymarket, "CLOB API", order book and trading endpoint
- Polymarket, "Data API", user positions and activity endpoint
- Robot Traders, "Polymarket API Python Tutorial", RobotTraders.io
- Robot Traders, "polymarket_python_bot.py", GitHub
- Robot Traders, "Build a Polymarket Trading Bot with Python", YouTube
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.