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

Our own production diary of bugs that cost real money. The pattern matters more than the specifics - the same classes of bug recur across bots, and the cure is usually a missing watchdog, not a better strategy. This chapter is intended to save you the tuition.

  • Phantom fills (commits e68a087, 8bb7761)
  • NegRisk flag bug (commit)
  • Sticky-fail dedup (commit)
  • 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)

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. 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)

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.

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: 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.