Polymarket Bot Tutorial · Chapter 32 of 32
Real Polymarket bot mistakes and postmortems: phantom fills, sticky-fail dedup, lol-ctg-ccg whipsaw, NegRisk flag bug, premature go-live - with the commits and dates that fixed each.
What this chapter covers
What follows is our own production diary of the bugs that cost us real money. The pattern matters more than the specifics, because the same classes of bug recur across every bot, and the cure is usually a missing watchdog rather than a better strategy. This chapter is here to save you the tuition we already paid.
- Phantom fills (commits e68a087, 8bb7761)
- NegRisk flag bug (commit 06deaef)
- Sticky-fail dedup (commit 4c0bef1)
- Whipsaw incident: lol-ctg-ccg
- Premature go-live: 2025 wipe
- Sleep-through-bug: kill switch worked
- Lessons that generalize
Phantom fills (commits e68a087, 8bb7761)
The first major phantom-fill incident on our trader, May 2025. Bot placed 22 FOK buys, all matched at the CLOB. The bot immediately attempted to post 22 GTC sells. 8 of them rejected with "balance: 0 / sum of active orders: 0 / order amount: 10000000."
Root cause: settlement lag (chapter 12). CLOB matched in 100ms, the bot posted the sell in 200ms, but the Polygon ERC-1155 transfer took ~2 seconds. The CLOB rejected the sell because the chain still showed zero balance.
Fix: insert a 5-second blocking wait between any successful buy and any GTC follow-up on the same token. Commits e68a087 and 8bb7761. Zero phantom-fill incidents since.
Lesson: API time and chain time are different timelines. Code that assumes they're synchronous will hit this exact failure mode.
NegRisk flag bug (commit 06deaef)
A NegRisk multi-outcome event with 8 candidates had a momentary arb of 1.8c (sum of YES asks = 0.982). Our arber fired all 8 FOK buys. 6 of them filled; 2 settled into the wrong exchange contract.
Root cause: the bot was calling createAndPostOrder without setting negRisk: true in the flags object. Two of the markets had a different historical creation date and required the flag; six did not need it because their underlying contract was already routing through NegRisk by default.
Fix: read market.negRisk from Gamma for every market, pass through to every order call. Commit 06deaef. We re-ran the arb with the flag set; the remaining 2 settled correctly.
Lesson: never default a market property. Read it explicitly from the source of truth every time.
Sticky-fail dedup (commit 4c0bef1)
The bot retried a failed buy 5 times in 12 seconds. The first attempt actually succeeded (network timeout caused the bot to not see the response); the next 4 retries created 4 additional positions. Total: 5 positions on the same market when we wanted 1.
Root cause: no idempotent client-order-id. The bot's retry logic was "if it failed, try again with a new salt." The CLOB had no way to recognize the retries as duplicates.
Fix: generate a deterministic UUID per intended order before the first attempt. All retries use the same client-order-id, allowing the CLOB to dedup. Commit 4c0bef1.
Lesson: retries without idempotence are duplicates. Every order needs a stable client-side identifier.
Whipsaw incident: lol-ctg-ccg
An esports match (CTG vs CCG) had the bot enter a buy at 0.45 when imbalance flipped positive. Within 30 seconds, the imbalance flipped negative and our GTC sell at 0.50 was hit by someone else's order. PnL: +5c × 10 shares = +$0.50.
10 minutes later, the same market's imbalance flipped positive again. Bot entered again at 0.42. This time the imbalance never recovered; mid drifted to 0.18 and the position rode to resolution at 0.
Root cause: the strategy treated imbalance as a directional signal but didn't track that the imbalance was bouncing - both signals were noise, not information. The bot was whipsawed across two failed signals on the same market within 20 minutes.
Fix: cooldown per market - after a fill, no new entries on the same market for 30 minutes. Allowed multiple entries across different markets, but not back-to-back on the same one.
Lesson: a signal that bounces is not a signal. Filter for persistence before acting.
Premature go-live: 2025 wipe
A new market-making strategy passed 12 paper trades. The builder didn't wait for 30, decided "looking good," deployed live with $500 of capital. Within 18 hours the wallet was at $200.
Root cause: 12 trades isn't enough sample to distinguish 60% WR from 35% WR. The strategy was actually 35% WR; the 12-trade paper window happened to have an unrepresentative streak.
The 30-trade gate exists for a reason. The variance on a 12-trade sample makes it indistinguishable from "the strategy doesn't work."
Lesson: discipline beats conviction. The 30-trade gate is not negotiable.
Sleep-through-bug: kill switch worked
Bot had an off-by-one in its time-of-day filter - meant to pause at 02:00 UTC, was actually pausing at 03:00 UTC. During the un-paused 02:00-03:00 hour, Polygon RPC was rate-limiting our requests heavily; the bot's read path was returning stale data.
The bot kept trading on stale prices. PnL on the hour: -$3.20 across 22 trades. The daily-loss kill switch triggered at -5%, halted the bot, sent a Telegram alert at 03:08 UTC. Builder woke up to a halted bot at 09:00, total damage limited to the kill threshold.
Lesson: the bug was real but the kill switch worked. -$3.20 instead of -$50.00. The risk controls don't prevent bugs; they cap the cost of bugs you didn't see coming.
Lessons that generalize
Across all postmortems, four patterns repeat.
- API time ≠ chain time. Settlement lag, RPC lag, WebSocket lag - all introduce gaps that bot code must explicitly handle.
- Retries need idempotence. A retry without a client-order-id is a duplicate-order risk. Always.
- Read every market property explicitly. NegRisk flag, tick size, expiration. Never default; always read from the source of truth.
- The kill switch is the floor, not a feature. Risk controls cap losses on bugs. Strategies don't prevent bugs; they assume the bot works correctly. The bot will not always work correctly.
Every chapter in this series has one of these patterns embedded somewhere. They're the load-bearing principles of a production bot. Skip them and you'll find them again in your own postmortems.





