DEV Community

BlueWhale-Quant-Lab
BlueWhale-Quant-Lab

Posted on • Originally published at github.com

How Polymarket decides Up or Down: reconstructing the price to beat

Polymarket's crypto Up/Down markets settle on one number: the price to beat (the strike / reference price). Beat it → Up wins; miss it → Down wins. If you automate these markets, here's the part that surprises everyone:

There is no API that returns the price to beat. You reconstruct it — and the recipe changes with the cycle.

Get it wrong and you don't get an error; you get the wrong winning side. Here are the rules, and a small open-source resolver.

The data source depends on the cycle

Cycle Reference price
5m / 15m / 4h the Chainlink price pushed at the cycle open
1h the Binance 1-hour candle OPEN at the cycle start
1d the Binance 1-minute candle CLOSE at the cycle start

Use one source for all of them — e.g. the live Chainlink price for the daily too — and you'll be off, sometimes by hundreds of dollars.

The boundaries aren't all UTC

  • 5m / 15m / 1h start on UTC grid lines.
  • 4h starts on America/New_York hours (00/04/08/12/16/20). A fixed UTC formula is wrong half the year because EST and EDT differ by an hour.
  • 1d starts at noon ET — not UTC midnight, not ET midnight. "Up or Down on March 5" runs Mar 4 noon ET → Mar 5 noon ET, and the price to beat is taken at Mar 4 noon ET.

And a DST-transition day is 23 or 25 hours, not 24 — so the daily's end is "noon ET + one calendar day", never "start + 86400".

The daily's trap: don't read the candle early

The daily uses the close of the 1-minute candle at noon ET. Binance returns that candle the moment it opens, but until it closes, close is just the latest trade — not the final close. Wait for close_time:

open_time, _, _, _, close, _, close_time, *_ = candle[0]
if now_ms <= int(close_time):
    raise NotReady("1m candle hasn't closed; CLOSE is still live")
price_to_beat = float(close)
Enter fullscreen mode Exit fullscreen mode

Also: startTime is a filter, not an exact match — always check the returned candle's open_time equals the t_start you asked for.

The resolver

I packaged all of this (cycle boundaries with DST, the per-cycle source/candle/field recipe, candle validation, and the side decision) into a zero-dependency MIT module:

from polymarket_price_to_beat import cycle_start, reference_price_spec, extract_reference_price, decide_side

t_start = cycle_start("1h", time.time())
spec    = reference_price_spec("btc", "1h", t_start)   # -> Binance 1h OPEN + the URL
ptb     = extract_reference_price(spec, http_get(spec["request_url"]))
decide_side(ptb, 70123)   # 'UP' / 'DOWN' / 'TIE'
Enter fullscreen mode Exit fullscreen mode

Repo (free, MIT, 16 tests):
https://github.com/BlueWhale-Quant-Lab/polymarket-price-to-beat-up-down-resolver

The live fetch with the timing guards (wait-for-close, late-push credibility) is in a complete version — but the resolver above fully covers the rules and runs offline.

Takeaway

The price to beat is reconstructable, as long as you respect the per-cycle source, the noon-ET / DST boundaries, and the wait-for-close rule for the daily.

Top comments (0)