How to read a Polymarket Up/Down outcome (and the Price To Beat) before the oracle settles
Polymarket runs hugely popular short-window crypto markets — "Will BTC be Up or Down at the close?" — on BTC, ETH, SOL, XRP and DOGE, over 5m / 15m / 1h / 4h / 1d windows. If you automate them, two numbers decide everything:
- Price To Beat (PTB) — the opening reference price. Final price > PTB → Up wins; otherwise Down.
- Outcome — once the window closes, the market settles Up or Down.
Here's the part that surprises everyone: for crypto markets, neither number is in the public API in a way you can actually use to trade. This is where they live, and how to read the outcome about 20 seconds after close instead of waiting minutes for on-chain settlement.
The API gap is real (and specific)
Polymarket does expose an opening-price endpoint — but only for non-crypto:
GET https://polymarket.com/api/equity/price-to-beat/{slug} # stocks, FX, gold, oil
For crypto Up-or-Down windows there is no public PTB endpoint. And the settled Outcome? Neither the Gamma API (gamma-api.polymarket.com) nor the CLOB API (clob.polymarket.com) returns it quickly — on-chain Chainlink settlement can take 5–10 minutes for a 5-minute market. By then your window is long gone.
The outcome isn't in the HTML, either
The obvious move — curl the event page and parse it — doesn't work:
| Source | Can you read the outcome? |
|---|---|
On-chain oracle (closed / outcomePrices) |
Authoritative, but lags by minutes |
Raw HTML (curl the page) |
No — "Outcome: Up" isn't in the HTML, any XHR, or __NEXT_DATA__
|
| Front-end render | Yes — React shows it within ~20s of close ✅ |
The frontend computes the result client-side from a live oracle pipe and paints it into the DOM. It's never in a response you can fetch directly. So the reliable approach is to become the front-end.
Become the front-end: headless Chromium
Load the page in headless Chromium (Playwright), let React render, then poll the DOM inside the browser until the outcome text appears:
import asyncio
from polymarket_ptb_scraper import PolymarketScraper
async def main():
async with PolymarketScraper(asset="btc", window="5m") as s:
t_start = s.previous_window_start()
r = await s.fetch(t_start)
if r.success:
print(f"{t_start}: {r.outcome.upper()} PTB=${r.ptb} final=${r.final_price}")
asyncio.run(main())
# BTC 1778839200: DOWN PTB=80478.47 final=80470.32
Three things make this fast and stable in production:
-
In-browser polling —
page.wait_for_function()at 100ms looking for/Outcome:\s*(Up|Down)/, so it resolves the instant the text renders, not on a fixed Python-side timer. -
SPA navigation — Polymarket is Next.js, so
page.goto(next_slug)is a client-side route change (JS/CSS stay cached) — roughly 5× faster thanpage.reload(). - Resource interception — block images/fonts/media and known analytics domains, but never block first-party JS/CSS or React won't render.
Add backoff-retry to a deadline and a periodic browser recycle (Chromium leaks a few MB per fetch) and it stays under ~300 MB RSS for hours.
The catch across timeframes: the slug
The scrape is the easy part. Building the URL is where most people get stuck, because the periods don't share a slug format:
| Period | Slug | Derivable from a timestamp? |
|---|---|---|
| 5m / 15m / 4h |
{asset}-updown-{w}-{t_start} (t_start % w == 0) |
✅ yes |
| 1h | bitcoin-up-or-down-may-29-2026-10pm-et |
❌ human-readable date slug |
| 1d | what-price-will-bitcoin-hit-on-may-29 |
❌ different market shape |
So a deterministic builder covers 5m/15m/4h, but 1h and 1d need a slug-discovery step (look the event up by date via the Gamma events API, then feed its slug to the scraper) — and the daily is a different price-target market type entirely.
The free build
The open-source version covers the 5m / 15m windows for BTC/ETH/SOL/XRP/DOGE — point it at a market, get back outcome, ptb, final_price:
https://github.com/BlueWhale-Quant-Lab/Polymarket-crypto-updown-PTB-Outcome-Scraper
It's a clean, dependency-light reference (Playwright only) and fully standalone — no API keys, no accounts.
When you need every coin and every timeframe
The free build stops at 5m/15m on purpose — that's the part you can derive from docs. When you hit the 1h / 4h / 1d markets you run into the slug-discovery problem above, plus the daily's noon-ET / different-market-shape edge cases. I packaged the complete version — all coins, all timeframes (5m / 15m / 1h / 4h / 1d) with the slug discovery and the period quirks handled — here:
Polymarket PTB & Outcome Scraper PRO — All Coins, All Timeframes
Good to know
- The rendered label is an early indicator (~20s), not the final on-chain settlement. If correctness is critical, reconcile against the oracle once it lands — the free build covers the fast read completely; the on-chain reconcile is your call.
- It reads public, already-rendered content. Keep request rates modest (12 fetches/hour per window has never been rate-limited in testing) and respect Polymarket's Terms of Service.
- If Polymarket changes its markup, the outcome regex is the one place to update.
- This is a data primitive — it reads PTB and outcome. It does not place trades or promise profit; what you build on top is yours.
Takeaway
For crypto Up/Down, the API won't hand you the PTB or a timely outcome — but the front-end already computes both, and you can read them ~20s after close by rendering the page yourself. The only real complexity is the slug across timeframes; solve that and you have a clean data feed for the whole BTC/ETH/SOL/XRP/DOGE board.