Free playbooks in your inbox
tutorial · Polymarket Profits

How to Build a Polymarket Trading Bot That Finds Mispriced Markets

Get a Python bot reading live Polymarket prices on your laptop tonight and flagging every market that sits 7 points off its estimate after the fee. It runs on public data from any country, paper-trades to a CSV file, and its first run shows you exactly why version one flags almost everything.

From the youcanbuildthings catalog ▸ Build-tested

Hello builders,

Tonight you can have a bot reading live Polymarket prices on your own laptop, reading the order books of 31 busy markets in 7 seconds on my last run. The screenshots that sell Polymarket bots never show the code, and a first bot’s bugs are common ones: it buys the same market twice, or it reads the wrong side of the order book. The Polymarket trading bot Python file below is the one from my book, cut down to a single scan, and it needs no key, no wallet and no account.

Public market data needs no credentials, so it runs from any country, including the US. You need Python 3.11 or newer and one library:

mkdir polybot && cd polybot
python3 -m venv .venv && source .venv/bin/activate
pip install requests

The four jobs in one file

I think of the bot as four people in a room. The Data Watcher pulls prices. The Analyst asks one question per market: is the price far enough from our estimate to pay after the fee? The Trader picks a size, 2% of the paper balance. The Logger writes every decision to trades.csv, because when something goes wrong at 3 AM the log is the only thing that will tell you what happened.

Save this as polybot.py:

import csv, json, os, time
import requests

GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
EDGE_THRESHOLD = 0.07   # 7 points, after the fee
BALANCE = 100.00        # paper money
TRADED_FILE = "traded_markets.json"

def get_markets(limit=50):
    params = {"closed": "false", "limit": limit,
              "order": "volume24hr", "ascending": "false"}
    return requests.get(f"{GAMMA}/markets", params=params,
                        timeout=30).json()

def get_market_prices(token_id):
    book = requests.get(f"{CLOB}/book", params={"token_id": token_id},
                        timeout=30).json()
    bids = [float(b["price"]) for b in book.get("bids", [])]
    asks = [float(a["price"]) for a in book.get("asks", [])]
    if not bids or not asks:
        return None
    bid, ask = max(bids), min(asks)   # never bids[0] / asks[0]
    return (bid + ask) / 2

def taker_fee(rate, p):
    return rate * p * (1 - p)

def analyze_market(m, price, baseline=0.50):
    if price < 0.05 or price > 0.95:
        return None                   # extreme prices are usually right
    rate = float((m.get("feeSchedule") or {}).get("rate") or 0)
    edge = baseline - price
    net = abs(edge) - taker_fee(rate, price)
    if net < EDGE_THRESHOLD:
        return None
    return ("YES" if edge > 0 else "NO"), net

traded = set(json.load(open(TRADED_FILE))) if os.path.exists(TRADED_FILE) else set()
print(f"Mode: PAPER | Balance: ${BALANCE:.2f} | "
      f"Edge threshold: {EDGE_THRESHOLD:.0%} | Max position: $5.00")
signals = scanned = 0
with open("trades.csv", "a", newline="") as f:
    log = csv.writer(f)
    for m in get_markets():
        if json.loads(m["outcomes"])[:1] != ["Yes"]:
            continue
        scanned += 1
        price = get_market_prices(json.loads(m["clobTokenIds"])[0])
        signal = price and analyze_market(m, price)
        if not signal or m["conditionId"] in traded:
            continue
        signals += 1
        side, net = signal
        size = min(5.00, BALANCE * 0.02)
        print(f"[PAPER] {side} ${size:.2f} {m['question'][:28]}.. edge {net:.1%}")
        log.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), m["question"],
                      side, round(price, 4), size, round(net, 4), True])
        traded.add(m["conditionId"])
json.dump(sorted(traded), open(TRADED_FILE, "w"))
print(f"Scanned {scanned} markets, signals found: {signals}")

Run it with python3 polybot.py. The first line it prints is the setup, then one [PAPER] row per trade it would have made, then a count.

Why max and min matter

That comment in get_market_prices is the bug I see most. When I wrote this I pulled the book for the busiest market, “Will Kylian Mbappé win the 2026 Ballon d’Or?”, straight from Polymarket’s public CLOB. It came back with 55 bids and 104 asks, sorted worst price first:

bids[0] = 0.001     asks[0] = 0.999     spread 0.998
max bid = 0.205     min ask = 0.209     spread 0.004

So the real spread was 0.209 minus 0.205, or 0.4 cents, and the first item in each list would have told you 99.8 cents. Every filter after that point works on the wrong number. Do this math, every time.

The fee is the other number we read per market. Gamma returned a feeSchedule rate of 0.03 on the Ballon d’Or markets and 0 on “Will there be no change in Fed interest rates”, which is why the code reads the rate from each market and never hardcodes it. The taker fee is rate × p × (1 − p), and the edge is measured after it.

Read your first scan

Polymarket trading bot Python first paper scan: Mode PAPER, no key, no account, no money moves; Balance $100.00; Edge threshold 7%; Max position $5.00; Scan #1 scanned 50 markets; PAPER NO $2.00 on the Fed no-change market at edge 41.5%, 2% of the $100 paper balance; Signals found 49, and 49 of 50 is the coin-flip baseline; pipeline Data Watcher, Analyst, Trader, Logger, trades.csv

That’s Scan #1 from the book’s test run, which uses the fuller bot from the companion repo (it scans every 30 seconds, so it prints a scan number, and the one-scan file above prints a setup line and a count in its place). Mode is PAPER, so no money moves. The balance is $100.00, the edge threshold is 7% and the cap is $5.00, so 2% of the balance puts every paper trade at $2.00. The first row is NO $2.00 on the Fed no-change market at an edge of 41.5%.

That run scanned 50 markets and found 49 signals. Why would a bot flag 49 out of 50? Our baseline is 0.50 for every market, and the busiest markets sit far from 50 cents (that Fed market was at $0.915), so against a coin flip almost all of them look mispriced. The bot is reporting its own estimate back to you.

When I ran the file above, it scanned the 31 Yes/No markets in the top 50 and logged 14 paper trades at edges from 13.5% to 43.5%. Your counts will differ, because the markets move every hour. The price filter in analyze_market cut 6 more that sat under $0.05 or over $0.95, because a market at $0.01 asking whether the sun explodes tomorrow is priced right. A second run logged 0, since every market it flagged was already in traded_markets.json. That file is what stops a restart from buying the same market twice.

What we have now is the plumbing: real prices, edge after the fee, a size and a log. The estimate is still one line, baseline=0.50, and the book’s next chapter replaces it with baselines from markets you’ve watched resolve. Your bot is dumber than you, but faster.

Now go build something this weekend!

John Cook

Why trust this? Every youcanbuildthings guide is pulled from a build-tested book: code that ran in production before it was written down.