Polymarket Bot Tutorial · Chapter 15 of 32
Sports microstructure bots on Polymarket: in-game edge, scoreline-driven mispricing, the NBA tag (745) and Tennis tag (864), live data sources, and execution patterns for high-frequency sports markets.
What this chapter covers
Sports markets are the most consistently active non-political segment on Polymarket. The bots that work fall into two clean buckets: pre-game line-catchers that trade once the line is set, and in-game microstructure bots that react to order-book movement during play. This chapter covers both with the specific tag IDs, data sources, and latency budgets that apply to each.
Sports markets are the busiest non-political segment on Polymarket. The execution pattern that works combines a live-score feed (ESPN, PandaScore) with order-book microstructure signals. This chapter covers what works for NFL, NBA, soccer, and tennis specifically, and where esports differs.
- Why sports markets are tradeable
- Pre-game vs in-game (different bots)
- Verified tag IDs (745 NBA, 864 Tennis)
- Data sources: ESPN, official APIs, on-screen
- Latency budget for in-game
- The 0.99 / 0.01 trap
- Code: subscribe to a games book and react
Why sports markets are tradeable
Sports markets clear in defined timeframes (hours to days), have public live data, and attract continuous order flow during games. All three are necessary for a tradeable market - political markets miss "defined timeframe," weather markets miss "continuous flow," obscure tournaments miss "public live data."
The trader population on sports markets is also more diverse than on, say, election markets. Casual sports bettors price emotionally; informed traders correct toward fair value over the course of a game. The gap between the two is the bot edge.
Volume distribution is uneven: an NFL Sunday will rotate hundreds of millions of dollars across Polymarket sports markets; a Tuesday-night Saudi Pro League fixture might do under $50k. Size your strategy to where the action actually is.
Pre-game vs in-game (different bots)
Two fundamentally different bot designs.
Pre-game line-catcher: scan markets that just opened, identify mis-priced lines against your model or against a sharper venue's number, place a FOK buy. Hold to in-play, sometimes to resolution. Speed: minutes-not-seconds. Edge: model + line-shopping.
In-game microstructure: subscribe to a live game's order book WebSocket, react to imbalance signals + score events within seconds. Speed: seconds-not-minutes. Edge: latency + reading order flow.
The two share almost no code. They have different risk profiles, different data sources, different exit strategies. A bot trying to do both ends up doing neither well; pick one.
Verified tag IDs (745 NBA, 864 Tennis)
Production tag IDs verified May 2026 for major sports categories. Use these to filter /events calls efficiently.
| Sport / League | Tag ID | Tag slug | Notes |
|---|---|---|---|
| NBA | 745 | nba | highest volume Oct-Jun |
| NFL | 450 | nfl | peak Sun/Mon Sep-Feb |
| Tennis (all) | 864 | tennis | year-round, tournament cadence |
| Soccer (general) | 1059 | soccer | combine with sub-tags below |
| EPL | 739 | epl | |
| UCL | 2186 | uefa-champions-league | |
| Esports (all) | 702 | esports | LoL+CS2+Valorant+Dota |
| MLB | 1245 | mlb | peak Apr-Oct |
| NHL | 823 | nhl | peak Oct-Jun |
Tag IDs are stable across years. New tags are added (Saudi Pro League, IPL) but old tags don't get renumbered.
Data sources: ESPN, official APIs, on-screen
For traditional sports the free ESPN scoreboard API covers everything you need: scores, period/clock, win-probability, sometimes shot location. No key required; rate-limited only at the IP level. Endpoint pattern: https://site.api.espn.com/apis/site/v2/sports/<sport>/<league>/scoreboard.
For esports, ESPN has no coverage. Options: PandaScore ($30-60/mo, the industry standard), HLTV (CS2-only, scrapeable, no API), Liquipedia (community-maintained, scrapeable, slower update cadence).
On-screen feeds (paying for a TV stream and OCR-reading the scorebug) work but are operationally heavy. Recommended only if you have a strategy that requires sub-3-second updates on a sport that no API covers in real time.
Latency budget for in-game
The end-to-end latency budget for an in-game reactive bot.
- Score event happens: t=0
- Source feed reflects: t+3-15s (ESPN: ~10s; PandaScore: ~3s)
- Your bot reads the feed: t+10-16s
- Bot decides action: +50ms
- FOK order placed: +200-500ms
- Matched at CLOB: +300-1000ms (network + matching)
Total: 11-17 seconds. The fastest professional firms achieve 3-5 seconds end-to-end with paid premium feeds and co-located VPS. Retail bots running on standard hosts and free ESPN are at the slower end.
Strategies that need sub-5s are not viable for retail. Strategies that work in 10-17s window are: line-catching after a score, fading overreactions, late-game certainty plays.
The 0.99 / 0.01 trap
The most common in-play sports bot failure: buying the heavy favorite at 0.99 with one minute left, expecting easy +1¢. Three reasons it fails.
First, the 1% implied probability of the underdog isn't zero - late comebacks happen with non-trivial frequency. A 99.5% certain win, played 200 times, produces one loss for full position size.
Second, the spread at 0.99/0.01 means you pay 99c per share, win 1c on success, lose 99c on the rare reversal. Risk-reward is brutal.
Third, the bot using GTC sell at 0.999 will rarely fill - there are no buyers at that price. The position rides to resolution. If it wins, you got 1c. If the reversal happens, you lose 99c.
The trap is real money lost by builders who didn't run the math. Stay out of 0.95+ priced markets unless your strategy is specifically built for the redemption-arbitrage profile.
Code: subscribe to a games book and react
Reference: subscribe to a specific NBA game's WebSocket, log book updates, fire FOK on imbalance signal.
import websocket, json
THRESHOLD = 0.5 # imbalance level to trigger
def on_message(ws, message):
msg = json.loads(message)
if msg.get("event_type") != "book": return
bids = msg.get("bids", [])
asks = msg.get("asks", [])
bid_depth = sum(float(b["price"]) * float(b["size"]) for b in bids[:5])
ask_depth = sum(float(a["price"]) * float(a["size"]) for a in asks[:5])
total = bid_depth + ask_depth
if total < 100: return # too illiquid
imb = (bid_depth - ask_depth) / total
if abs(imb) > THRESHOLD:
print(f"signal imb={imb:.2f} bid={bid_depth:.0f} ask={ask_depth:.0f}")
# fire FOK here
ws = websocket.WebSocketApp(
"wss://ws-subscriptions-clob.polymarket.com/ws/market",
on_open=lambda ws: ws.send(json.dumps({"type":"Market","markets":["<CONDITION_ID>"]})),
on_message=on_message
)
ws.run_forever()
Production additions: cooldown between fires, per-token inventory cap, kill on stale book (no message in 30s).





