
How I Built a Polymarket Trading Bot That Actually Makes Money
A full technical walkthrough of building a latency arbitrage bot for Polymarket prediction markets — from Binance WebSocket signals to CLOB order placement.
In this cluster
Quantitative Trading Systems: Building, testing, and operating automated trading bots on prediction markets and crypto exchanges.
I spent three months building a trading bot for Polymarket. Not a “signal subscriber” or a wrapper around someone else’s strategy — a system that finds mispricing, places orders, and manages exits autonomously with real money on the line.
Here is the full architecture: what works, what broke, and what the numbers look like.
TL;DR
- The edge: Polymarket’s 5-minute BTC up/down markets lag Binance price moves by 30-90 seconds
- The strategy: Detect a large BTC move on Binance, buy the corresponding YES/NO market on Polymarket before odds reprice
- The stack: Python, asyncio, Binance WebSocket, py_clob_client, VPS in Amsterdam
- The results: 69.6% win rate, +$11.51 net over 23 clean trades (Feb 13-16, 2025)
- The hardest part: The bugs were silent — wrong API calls that succeeded but destroyed your position
Why Prediction Markets Have Exploitable Lags
Polymarket runs binary options on events. The 5-minute BTC up/down markets ask: will BTC be higher or lower in 5 minutes than it is right now?
Market makers on Polymarket reprice these markets in response to Binance spot price moves. But they are not colocated at Binance. There is latency in their observation, decision, and order update cycle.
That window — typically 30 to 90 seconds on large moves — is the edge. When BTC moves 0.3% in 60 seconds, the Polymarket YES probability for “BTC up” often hasn’t fully moved from 0.55 to 0.80 yet. You buy YES at 0.60, the market closes at 0.80 (already resolved YES by then), and you capture the spread.
The math on a winning trade with maker entry:
Entry: 100 shares at $0.60 = $60
Payout: 100 shares × $1.00 = $100
Profit: $100 - $60 = $40 (66.7% return)
Maker fee: $0 (Polymarket pays makers) The math on a losing trade:
Entry: 100 shares at $0.60 = $60
Payout: $0 (market resolves other way)
Loss: -$60 This means the strategy needs roughly 60% win rate at p=0.60 entry price to break even — and historically the signal fires at higher-conviction entry points.
The Architecture
The system has four layers:
- Signal layer — Binance WebSocket detects >0.3% BTC move in 60 seconds
- Decision layer — checks market conditions, filters, existing positions
- Execution layer — places maker order on Polymarket CLOB
- Exit layer — holds to resolution or uses a stop-loss on adverse move
Signal Layer: Binance WebSocket
For a complete breakdown of this stage — including the full MomentumDetector, SignalGuard, and reconnect architecture — see Binance to Polymarket: Building a Real-Time Momentum Signal Pipeline.
import asyncio
import websockets
import json
from collections import deque
THRESHOLD_PCT = 0.003 # 0.3%
WINDOW_SECS = 60
price_window: deque = deque()
async def monitor_binance():
url = "wss://stream.binance.com:9443/ws/btcusdt@aggTrade"
async with websockets.connect(url) as ws:
async for msg in ws:
data = json.loads(msg)
price = float(data["p"])
ts = data["T"] / 1000
price_window.append((ts, price))
# Prune window
cutoff = ts - WINDOW_SECS
while price_window and price_window[0][0] < cutoff:
price_window.popleft()
if len(price_window) < 2:
continue
oldest_price = price_window[0][1]
pct_move = (price - oldest_price) / oldest_price
if abs(pct_move) >= THRESHOLD_PCT:
direction = "UP" if pct_move > 0 else "DOWN"
await on_signal(direction, pct_move, price) The key calibration: 30-second/0.15% threshold produced zero signals in testing. 60-second/0.3% produced 62% of BTC moves landing in-range. You need enough signal frequency to generate trades, but not so much that you’re trading noise.
Execution Layer: py_clob_client
The Polymarket Python SDK has some surprises. Here is the correct order placement pattern:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType
client = ClobClient(
host="https://clob.polymarket.com",
key=PRIVATE_KEY,
chain_id=137,
signature_type=1, # POLY_PROXY for MagicLink/email accounts
)
async def place_maker_order(token_id: str, price: float, size: float):
order_args = OrderArgs(
token_id=token_id,
price=price,
size=size,
side=BUY, # always BUY — YES and NO are separate tokens
)
# This creates AND submits the order in one call
result = client.create_and_post_order(order_args)
return result Two things that will burn you if you don’t know them:
Always use create_and_post_order, not create_order. The SDK has both. create_order only creates a local order object — it does not submit it. This is not obvious from the name.
Always use side=BUY. YES and NO are separate tokens on Polymarket. You never “sell NO” — you “buy YES.” When you want to exit a YES position, you place a SELL order, but entry is always BUY on the token you want to hold.
Signature Types
This tripped me up for a week. Polymarket has three signature types:
- Type 0 (EOA): Standard private key, EIP-712 signing. Works for wallets you control directly.
- Type 1 (POLY_PROXY): For MagicLink/email accounts. Gasless proxy contract signing. Different from type 0 in ways that matter for allowances.
- Type 2 (POLY_GNOSIS_SAFE): MetaMask browser wallet, Gnosis Safe. Not suitable for automated bots.
If you have a MagicLink account (most consumer Polymarket accounts), you need type 1. Using type 0 on a type 1 account will produce valid-looking signatures that the CLOB silently rejects.
The Allowance Bug That Cost Real Money
The CLOB tracks internal allowances separately from on-chain state. There are two API calls that sound similar but do opposite things:
# SAFE: reads current CLOB internal allowance
client.get_balance_allowance()
# DANGEROUS: reads on-chain, then OVERWRITES CLOB internal allowance
client.update_balance_allowance() I had code that called update_balance_allowance() before placing sell orders to “confirm I had enough allowance.” This overwrote the CLOB’s internal allowance with a freshly read on-chain value — which, after a recent fill, was zero. Every sell after that failed silently because the CLOB thought I had no allowance.
The fix: never call update_balance_allowance() after a fill. Only call get_balance_allowance() to read. The CLOB updates its internal state correctly on its own after fills.
Server Location Is Not Optional
The Polymarket CLOB is hosted in London (eu-west-2). Latency from different locations:
| Location | Latency |
|---|---|
| Amsterdam | 5-12ms |
| Frankfurt | 15-25ms |
| US East (NYC) | 130-150ms |
| US West | 180-200ms |
At 130ms from US East, you are behind every European competitor by 120ms. In a strategy where the edge window is 30-90 seconds, 120ms is not catastrophic — but it compounds with every other source of latency.
My bot runs on a QuantVPS instance in Amsterdam. The difference in fill quality between the Amsterdam server and testing from my Mac in San Francisco was measurable.
What the Numbers Look Like
Over 23 clean BTC trades (February 13-16, 2025, excluding deployment-disrupted trades):
- Win rate: 69.6% (16/23)
- Net P&L: +$11.51
- Average per trade: +$0.50
- BTC Down signal: 75% win rate (strongest direction)
- BTC Up signal: 65% win rate
“Clean” means I excluded trades during bot restarts, code deploys, and the 48-hour period after I changed the entry threshold. Deployment-disrupted trades are not strategy losses — they’re operational noise.
The win rate of 69.6% at average entry prices around p=0.62-0.68 puts this well above break-even. The position sizing is conservative ($5-15 per trade) because this is early validation, not full deployment — in the full system, an adaptive self-tuner scales that base bet based on rolling performance. For the expected value and Kelly sizing math behind why that win rate translates to positive P&L, see The Math Behind Directional Betting in Binary Markets.
The Exit Problem
Most of the complexity is not in signal generation or order placement — it is in exit management.
Options for exit:
- Hold to resolution: simplest, highest EV if your directional signal is right
- Stop-loss on adverse move: cut the position if odds move against you beyond a threshold
- Time-based exit: exit N minutes before market close
“Hold to resolution” sounds right but requires discipline. A position at 0.65 that moves to 0.40 before resolving YES will feel terrible even if the final outcome is correct. Most people override their bot at exactly the wrong moment.
I use hold-to-resolution as the default with a secondary check: if position moves against me AND the market has more than 3 minutes left AND I am down more than 30% on the position, I exit and redeploy elsewhere. This is not optimal from a pure EV perspective, but it prevents large single-trade losses from ending the session.
What I Would Do Differently
1. Build the exit manager before the entry logic. Entries are easy to tune. Exits determine whether a 69% win rate turns into positive P&L or not.
2. Validate the expiry check from day one. Markets close abruptly. If you place an order 1 second after a market closes, the fill happens but you can not exit. Add secs_remaining <= 0 checks before every order.
3. Log everything to a DB immediately. I lost the first two weeks of data because I was logging to a text file that got rotated. SQLite with trades, fills, and exits gives you something to analyze.
4. Colocation is table stakes, not optimization. I treated server location as something to optimize later. It is actually part of the core strategy definition.
Is This Worth Building?
The edge is real but narrow. 0.3% BTC moves in 60 seconds are moderately common — roughly 3-8 per day on active days. Position sizing is limited by Polymarket’s liquidity and the 5-minute window. This is not a strategy you can scale to $10,000 per trade.
For someone learning to build trading systems, Polymarket is excellent: real money, real order book, reasonable API, binary outcomes that are easy to evaluate. The feedback loop is short enough (5 minutes per trade) that you accumulate signal fast.
For pure P&L maximization at scale, you would need to find higher-liquidity markets or stack multiple edges.
FAQ
Is Polymarket trading profitable?
It can be with a genuine edge. Latency arbitrage — exploiting the lag between Binance price moves and Polymarket odds repricing — produced a 69.6% win rate across 23 clean trades. Without an edge, you'll lose to fees and informed market makers.
What programming language is best for Polymarket bots?
Python with py_clob_client is the natural choice since Polymarket provides an official Python SDK. The async event loop handles WebSocket feeds and order placement cleanly.
How do Polymarket signature types work?
Type 0 is EOA (standard private key signing). Type 1 is POLY_PROXY, used by MagicLink/email accounts — it's gasless and works differently from standard EIP-712. Type 2 is GNOSIS_SAFE for MetaMask browser wallets.
What server location should I use for a Polymarket bot?
Amsterdam (eu-west-2 region) gives 5-12ms to Polymarket's CLOB in London. US East gives 130-150ms. That 120ms disadvantage is significant in a latency-sensitive strategy.
What is the minimum order size on Polymarket?
5 USDC shares minimum. Position sizing below this threshold will be rejected by the CLOB.
Sources & Further Reading
Sources
- Polymarket py_clob_client The official Python SDK for Polymarket CLOB order placement.
- Polymarket Developer Documentation API reference and authentication documentation.
- Binance WebSocket API The market data stream used for the momentum signal.
Further Reading
- Self-Tuner: Building an Adaptive Position Sizing System in Python How to build a self-tuning position sizing system that adjusts bet size based on recent performance — without overfitting or over-reacting to variance.
- Binance to Polymarket: Building a Real-Time Momentum Signal Pipeline How to wire Binance WebSocket price feeds into a Polymarket trading bot — signal detection, filtering, deduplication, and latency-optimized order flow.
- I Let an AI Agent Run My Blog for 30 Days. Here's What It Actually Did. How I configured OpenClaw to study my writing voice, handle SEO/AEO/GEO, and publish blog posts autonomously with a single Telegram approval.
Discussion
Comments powered by GitHub Discussions coming soon.