How a market settles

A crypto Up/Down market resolves against a Chainlink stream, not against the order book. Which stream is a property of the market, and the archive carries everything you need to recompute the result yourself.

Two conventions you need

The settlement rule has changed — read it off the market, not the date

Before 2026-08-07(UTC) the instantaneous Chainlink stream decided the outcome; after it, the TWAP streams did — a 30s lookback for 5min markets, 60s for 15min. After 2026-08-14(UTC), 5min markets have since moved to 60s as well, so both durations now settle on twap60s. Take the stream from the market’s own raw.cryptoMarketConfig (twapEnabled, and twapLookbackSeconds of 30 or 60) rather than from the date — markets on different configs coexist on the same day. twap30s is still archived daily; it is simply no longer any market’s settlement line.

Frames are stored as received

Out-of-order included; we do not reorder an archive. Sort at query time if you need chronological order.

Recomputing an outcome yourself

Nothing here needs our word for it — the strike, the window and the deciding stream are all in files you downloaded.

1Read the market line: start_sec, end_sec, strike_value, and raw.cryptoMarketConfig.twapLookbackSeconds.
2Pick the stream that lookback names — 60 means twap60s, 30 means twap30s — and take its value at the close of the window.
3Compare against strike_value. Both are fixed point at 1e18 on the Polymarket side, so parse both to integers and compare those rather than the floats: value is there for convenience and full_accuracy_value is the one to settle on, whenever the relay published it.
4Check yourself against outcome_prices on the same line. ["1","0"] is Up, ["0","1"] is Down.

The same four steps, as code

Reads the two files you downloaded and prints a verdict per market. It takes the closing tick by timestamp rather than by position in the file, because frames are archived in the order they arrived.

import gzip, json, csv

MARKETS = "BTC-5m-markets.jsonl.gz"      # one line per market, as downloaded
STREAMS = {                              # each market names the one it settles on
    60: "BTCUSD-twap60s-prices.csv.gz",
    30: "BTCUSD-twap30s-prices.csv.gz",
    0:  "BTCUSD-prices.csv.gz",          # before TWAP, the instantaneous feed
}

_loaded = {}
def stream(lookback):
    if lookback not in _loaded:
        with gzip.open(STREAMS[lookback], "rt") as f:
            _loaded[lookback] = list(csv.DictReader(f))
    return _loaded[lookback]

for line in gzip.open(MARKETS, "rt"):
    m = json.loads(line)
    # strike_value is null when it could not be established honestly
    if not m.get("resolved") or m.get("strike_value") is None:
        continue

    # Read the stream off the MARKET, never off the date: configs coexist on
    # one day. A MISSING config is a record we could not read — not evidence
    # of the pre-TWAP era — so it is skipped. Only an explicit null or 0 says
    # "this one settled on the instantaneous stream".
    raw = m.get("raw")
    cfg = raw.get("cryptoMarketConfig") if isinstance(raw, dict) else None
    if not isinstance(cfg, dict) or "twapLookbackSeconds" not in cfg:
        continue
    lookback = cfg["twapLookbackSeconds"]
    if lookback is None:
        lookback = 0
    if lookback not in STREAMS:
        continue                         # unknown config: refuse rather than guess

    open_ms, close_ms = m["start_sec"] * 1000, m["end_sec"] * 1000
    window = [t for t in stream(lookback)
              if open_ms <= int(t["feed_ts_ms"]) <= close_ms]
    if not window:
        continue

    # by timestamp, not by file order: frames are archived as received
    last = max(window, key=lambda t: int(t["feed_ts_ms"]))

    # full_accuracy_value is 1e18 fixed point ONLY when the relay published it;
    # otherwise the collector wrote str(value) and the archive cannot tell you
    fixed = last["full_accuracy_value"]
    if not fixed.isdigit():
        continue                         # a decimal fallback, not fixed point
    settled = int(fixed)
    if abs(settled / 1e18 - float(last["value"])) > 1e-6:
        continue                         # an integer, but not at the 1e18 scale

    # only a COMPLETE binary pair is an outcome; anything else is bad metadata
    prices = m.get("outcome_prices")
    if prices not in (["1", "0"], ["0", "1"]):
        continue

    # the market resolves Up when the close is >= the strike, not > it
    mine   = "UP" if settled >= int(m["strike_value"]) else "DOWN"
    theirs = "UP" if prices[0] == "1" else "DOWN"
    print(m["slug"], mine, "matches" if mine == theirs else "DIFFERS")

Predict.fun settles differently

A Predict.fun market line carries start_price and end_price as ordinary decimals, and the outcome follows from comparing them. Read settlement off end_price rather than off status: status is whatever upstream last reported, and a market leaves the re-read queue as soon as its end_price lands, so status can stay OPEN on a market that has settled. A market whose end_price equals its start_price is a tie, which is a third outcome rather than a win for either side.

Related: Polymarket order book data · Chainlink settlement data · Predict.fun historical data · Compared with other providers