DEV Community

BlueWhale-Quant-Lab
BlueWhale-Quant-Lab

Posted on

What it takes to run a Polymarket Up/Down arbitrage bot in production

A "risk-free" arbitrage on Polymarket's crypto Up/Down markets is easy to describe and hard to actually run. The edge — the cross-cycle sandwich — is real, but the money is won or lost in execution. Here's the engineering that turns the idea into a bot that survives production, and the specific things that break if you skip them.

The edge in two sentences

Polymarket runs Up/Down markets on the same asset across cycles (5m/15m/1h/4h/1d). Two cycles that settle at the same time share one final price but have different strikes (the per-cycle "price to beat"). Buy UP on the lower strike and DOWN on the higher strike: at least one leg always wins, and if the settlement price lands between the two strikes, both win. If the legs cost under $1.00 combined, that's a structural edge — on paper.

total_cost = up_price_low_strike + down_price_high_strike
edge = total_cost < 1.00          # at least one leg pays $1 -> can't lose if both fill
Enter fullscreen mode Exit fullscreen mode

The phrase that matters is "if both fill." Everything below is about that.

Problem 1 — direction is the whole game

Assign the sides from the strikes: UP on the lower PTB, DOWN on the higher. Get it backwards and the middle band — the region you most want — loses both legs (a "death sandwich"). So the bot's correctness hinges on a trustworthy price-to-beat per cycle, and the per-cycle rules are not uniform:

  • 5m/15m/4h: the Chainlink price pushed at the cycle open.
  • 1h: the Binance 1-hour candle Open.
  • 1d: the Binance 1-minute candle Close at the noon-ET boundary (DST-aware).

A PTB that locks a second or two late can already have drifted enough to flip which strike is "lower." So you gate on lock delay and refuse to fire on a stale strike.

Problem 2 — fire both legs, fast, together

Two legs posted seconds apart is two chances for the market to move between them, leaving you one-sided. The fix is boring but essential: one keep-alive connection, TCP_NODELAY, and asyncio.gather so both orders hit the book within milliseconds.

results = await asyncio.gather(place(leg_up), place(leg_down))
Enter fullscreen mode Exit fullscreen mode

Problem 3 — when only one leg fills

It will happen. Now you hold a naked leg and must top up the other side — and this is where bots quietly hemorrhage:

  • Overbuy loop. Size the top-up from the positions API and it lags (reads 0), so you resubmit, it fills, reads 0 again... A real run targeted 4.29 shares and bought 37.5. Size from an internal fill tally, never a lagging read.
  • Fee drift. Fees mean the two legs never match to the exact share — use a tolerance band instead of chasing crumbs.
  • The $1 floor. Orders under ~$1 notional are rejected; a 1-share crumb loops forever on invalid amount. Below the floor, let it settle.

Problem 4 — timeouts must not double-fill

On an HTTP timeout, the naive retry resubmits the order. If the first one actually landed, you've now got double exposure. Confirm against /data/trades before any resubmit; treat a timeout as "unknown," not "failed."

Problem 5 — the 2026-04 SDK migration

Late April 2026, Polymarket shipped a V2 SDK that broke older bots two ways. If yours started failing then, this is why:

# order_version_mismatch -> the wire body must be built by order_to_json_v2
from py_clob_client_v2.order_utils.model.order_data_v2 import order_to_json_v2
body = order_to_json_v2(signed, api_key, OrderType.GTC.value, False, False)

# 'not enough balance / allowance' (with funds!) -> sync the cache once at startup
client.update_balance_allowance(params=BalanceAllowanceParams(asset_type=AssetType.COLLATERAL))
Enter fullscreen mode Exit fullscreen mode

The old hand-built {order, owner, orderType} body is dead, and the CLOB caches balance=0 after a deposit until you force a re-read.

The honest part

With cost < 1 and both legs filled, a pair can't lose. The risk is execution — one-sided fills, slippage, the gap between your two orders, rate limits while you scan and fire. A production engine is mostly machinery for shrinking that risk: direction lock, concurrent posting, a hedge/overbuy guard, timeout idempotency, and staying under the API limits. It is not a money printer; prediction-market trading can lose money, and you should paper-test before risking real funds.

The complete engine

Everything above — three execution routes (immediate / pre-place / downstream-lock), the dual engine (arbitrage + strangle), the hedge-leg hound for one-sided fills, live Oracle + market WebSockets, timeout idempotency, and both 2026-04 SDK fixes (order_to_json_v2 + startup update_balance_allowance) wired in — is the ready-to-run V25 build. Bilingual, configurable, fill your keys and run:

https://bluewhalequantlab.gumroad.com/l/polymarket-updown-sandwich-bot-v25

Respect that the strategy is the easy 20% and the execution is the other 80%.

Top comments (0)