If you run a two-sided or arbitrage bot on Polymarket, you've probably hit this: one leg of a paired position fills, the other doesn't, and when you top up the missing leg your bot buys way more than you asked for. A real run targeted 4.29 shares and ended up holding 37.5 — an 8.7× overbuy. Here's how it happens and how to stop it.
The overbuy loop
You hold a naked leg and want to top up the hedge side, so you ask "how much do I already hold?" via the positions API and buy the difference.
The trap: the indexer lags. For a few seconds after a fill it still reports your old balance — often 0. So:
- You place a GTC top-up for the full size. It fills instantly.
- You re-check positions → still
0. - Thinking nothing filled, you place the full size again. Fills again.
- Repeat until the indexer catches up — by which point you're multiples over.
The positions API is fine for reconciliation; it's the wrong thing to size a hot re-submit loop against.
The fix: an internal tally
Size every attempt from your own running total of fills, updated the instant an order response reports one:
internal_filled = 0.0
def on_fill(shares): # call with takingAmount from each order/poll response
global internal_filled
internal_filled += shares
remaining = max(0.0, target_shares - internal_filled) # never re-buy what you hold
Clamp at zero so an over-fill can never produce a negative "buy more".
Two more traps in the same path
Fee drift. Buy 8 shares; fees leave you ~7.9 received while the other leg sits at 8.4. They never reconcile to the exact share, so "keep topping up until equal" never terminates. Use a tolerance band — within ~2%, call it balanced.
The $1 floor. Polymarket rejects orders under ~$1 notional. A 1-share top-up at $0.53 loops on invalid amount. Below the floor, leave the crumb to settle.
Chasing it to a fill
Once you know the size, you still have to land it on a venue that moves. The production pattern separates two cadences: poll ~5s to absorb fills, reprice ~30s to nudge your bid toward a cost ceiling — chasing only the remaining shares each round, and refusing to launch a second chaser for a leg that already has one (a duplicate doubles your exposure).
The library
I packaged the sizing math (shortfall + tolerance, the $1 floor, overbuy-safe remaining_to_buy, a cost ceiling, and a one-call plan_hedge) into a zero-dependency MIT module:
from polymarket_hedge_leg import plan_hedge
plan_hedge(20.0, 6.75, hedge_price=0.46, primary_cost=0.49, max_total_cost=0.95)
# {'need_shares': 13.25, 'ok': True, 'skip_reason': None, 'est_cost': 6.095}
Repo (free, MIT, 19 tests):
https://github.com/BlueWhale-Quant-Lab/polymarket-hedge-leg-overbuy-guard
The async hound that chases the leg to a fill — with the overbuy-safe accounting and a dedup registry — is a complete version, but the sizing math above stands on its own.
Takeaway
Never size a re-submit from a lagging positions read. Keep an internal fill tally, add a tolerance band for fees, and respect the $1 floor.