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.

  1. API time ≠ chain time. Settlement lag, RPC lag, WebSocket lag - all introduce gaps that bot code must explicitly handle.
  2. Retries need idempotence. A retry without a client-order-id is a duplicate-order risk. Always.
  3. Read every market property explicitly. NegRisk flag, tick size, expiration. Never default; always read from the source of truth.
  4. 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.

Frequently asked questions

What is the most expensive Polymarket bot mistake?
Going live before paper-trading meets the 30-trade gate. We have done it. The mistake is not just losing money - it is losing the chance to learn from the strategy in a controlled environment. Bots that go live too early either get nuked and abandoned, or waste months recovering before re-paper-trading.
What is a phantom fill bug?
When the bot believes an order filled but the exchange recorded it as not yet filled. Symptoms: position appears in your bots state but not on-chain, leading to double-orders on retry. Fixed in our trader via three commits (e68a087, 8bb7761, 06deaef): use FOK for buys, poll status until matched, never trust status=delayed as filled.
What is the lol-ctg-ccg whipsaw incident?
An esports market on a thin order book where our trader fired a -$2.55 stop-loss at 0.14, then watched the price recover to 0.325 within 2 minutes. We had configured stop-loss at -4 percentage points which is too tight for thin esports books. Fix: widened SL to -8pp for low-liquidity markets, kept tighter SL only for thick books (NBA, high-liquidity soccer). See memory/trader-sl-wider.md.
How did the NegRisk flag bug manifest?
Bot placed orders without setting neg_risk=true on multi-outcome markets. Orders rejected with confusing error messages, leading to multi-second delays before retry, leading to missed fills. Fix in commit 06deaef: always set neg_risk per market metadata, never assume.
What was the sleep-through-bug incident?
Wallet got wedged with a stuck order at 4am. Owner instructed bot to halt; touched data/halt_autobuy file. Bot detected the file before next trade attempt and refused to place orders. Owner woke up to a clean state instead of a worse one. Validated the halt-sentinel pattern; we now ship it default in every bot.
What is the single most generalizable lesson from these postmortems?
Never trust the happy path. Every bug we have shipped came from assuming a request succeeded, a fill was real, or a price would not move. Code defensively: assume orders fail, assume reconciliations diverge, assume one market is about to do something weird. The paranoia tax is small; the cost of skipping it is the postmortem you write later.