DEV Community

Samuel EF. Tinnerholm
Samuel EF. Tinnerholm

Posted on

How I caught Polymarket front-running a Trump headline by 7 hours, using only a public order-book API

TL;DR — On May 23, 2026, Donald Trump posted on Truth Social at 21:50 UTC that a US–Iran ceasefire was "largely negotiated." By the time the post hit, the Polymarket book for "Will the ceasefire continue through May 24?" had already done 85% of the price discovery. The first big move came at 14:18 UTC — 7 hours and 32 minutes before the post. I pulled 36 hours of L2 order book snapshots, lined them up against every wire-service headline I could find, and put together the timeline. All the data, code, and charts are in a public GitHub repo. This post walks through how I did it.

I've been playing with the PMXT historical order book API for prediction markets. The pitch is simple: pull tick-level L2 book snapshots for any Polymarket market, at any moment in its history, in four lines of Python.

I wanted to try it on a real news event. The May 23 Iran ceasefire announcement was timestamped (5:50 PM EDT, via TIME, CNBC, Al Jazeera, and WaPo), so I'd know exactly where to look. I also had a hunch — Polymarket has been increasingly cited as a leading indicator for geopolitical news, and I wanted to see whether the order book actually reflects that or whether the "prediction markets are efficient" narrative oversells the truth.

The answer turned out more interesting than I expected.

Step 1 — Pulling the data

The whole fetch is four substantive lines of code.

from pmxt import Polymarket

poly = Polymarket(pmxt_api_key="pmxt_...")
book = poly.fetch_order_book(
    "0xd7540d64e03b1894ececcbb54c02b88d9c5c0e854ba66ec4e1ece20477994ac5",
    params={"since": 1779492000000, "outcome": "yes"},
)
print(f"{book.dt}: {len(book.bids)} bids, {len(book.asks)} asks")
Enter fullscreen mode Exit fullscreen mode

The first argument is the Polymarket conditionId for the market — a 64-character hex string you can grab from the market's URL on polymarket.com (or market.contract_address if you've already done a fetch_market(slug=…) lookup). The since parameter is a Unix millisecond timestamp — any moment in the market's history. The outcome filter tells the reconstruction which side of the binary outcome to return ("yes" or "no").

That returns a full L2 snapshot — every resting bid and ask, with price and size, as the book existed at that moment. Sub-cent ticks (Polymarket uses a 0.001 tick), 30–60 levels per side typically.

To get a full time series, you just loop the call with timestamps spaced however densely you want. I went with 3-minute cadence over a 36-hour window (May 23 06:00 UTC → May 24 18:00 UTC) to cover the day of the news cycle plus a baseline period before and after. That works out to 721 requests per market.

I had to be careful about rate limits — PMXT caps you at 100 req/min, and my naive parallel fetcher blew past that the first time and started getting 429s back. The fix was a simple paced loop at 85 req/min plus a per-call 15-second timeout (one call hung forever and froze the whole script before I added the timeout). The full fetcher with streaming progress is in scripts/fetch.py.

I also pulled four other related markets — two more ceasefire deadlines (May 27, May 31), plus two regime-fall markets as controls. About 30 minutes of total fetch time at the polite cadence.

Step 2 — Looking at the headline market

First thing I plotted was the simple thing: depth heatmap of the May-24 ceasefire market over the 36-hour window.

Depth heatmap, May-24 ceasefire market

The mid-price line climbs from ~78¢ to ~97¢ over the window. The green bid wall and red ask wall on either side of it show the resting depth at each moment. Color intensity encodes log-depth (brighter = more contracts resting at that price).

Two things popped out immediately:

  1. The price moves in two distinct phases. A choppy morning where YES sits in the 70s, then a sharp climb starting mid-afternoon UTC that pushes it to the 90s by evening.
  2. The biggest single price move comes before — not after — the time I was expecting the news to land.

Trump's post was at 21:50 UTC. The market hit 95¢ by 21:00. The post moved it from 95 to ~98 — barely a wiggle.

That made me curious. Where did the rest of the move come from?

Step 3 — Cumulative price discovery

To make the asymmetry obvious, I converted price into "percent of total move priced in" — what fraction of the eventual ~25¢ rally had completed at each moment.

Cumulative price discovery

The annotation marks 82% — by Trump's Truth Social post, the market was 82% of the way to its eventual price. The post contributed the remaining ~15%. Put another way:

  • Pre-news move: +22¢ over the day
  • Post-news move: +3¢ over the next ~6 hours
  • Ratio: for every 1¢ the market moved AFTER Trump's post, it had already moved ~7¢ BEFORE it.

The negative red region at the start is interesting too — the market briefly drifted slightly AGAINST the eventual move during the morning (an early Trump "United States of the Middle East?" post had been hawkish, so the market was actually short before reversing).

Step 4 — Was there a public news item that could explain the pre-news rally?

This is where the analysis got serious. If a wire-service headline hit at 14:18 UTC, the "front-running" story is just normal market reaction time. So I built a timeline of every public Iran-related signal I could find for May 23.

I cross-referenced WaPo, CNN, Axios (which had two scoops that day), CBS, ABC News, Al Jazeera, Reuters, TIME, Pakistan ISPR, Iran MFA, and Trump's own Truth Social feed. I plotted them against the price.

Timeline with public news annotated

The yellow vertical line is 14:18 UTC — the first big Polymarket move (+7¢ in 29 minutes). The red line is Trump's 21:50 UTC post. Every other public news item is plotted as a thin dotted vertical line, with timestamps in the legend in the upper-left.

The earliest positive news item I could find for the day was Marco Rubio in New Delhi at 14:23 UTC saying "good news in the next few hours, at least in regard to the straits." That hit the wires 5 minutes after the Polymarket move.

Every other positive signal — Pakistan's ISPR statement (14:55 UTC), the Axios "50/50" exclusive (15:29 UTC), the Washington Times "draft within 24 hours" scoop (17:30 UTC) — came 37 minutes to 3+ hours after the price had already moved.

The only event timestamped before 14:18 UTC was a US CENTCOM tweet at 13:33 UTC about a blockade milestone — which is hawkish, not bullish for ceasefire continuation. It can't explain why YES rallied.

Step 5 — Was it just one market, or replicated?

A single market jumping at 14:18 could be one trader getting lucky on a hunch. To distinguish luck from informed trading, I checked whether the move replicated across other ceasefire markets (different deadlines, but the same underlying news).

Term-structure overlay

Three different markets — "Will the ceasefire continue through May 24?", "…through May 27?", and "…through May 31?" — all started rallying within minutes of each other at 14:18 UTC. Same direction. Same time. Magnitudes scaled to duration (the shorter-dated market ends higher because it's more confident in a short-term ceasefire).

That's a much harder pattern to chalk up to luck. It's coordinated, multi-market buying ahead of an unannounced event.

Step 6 — Order book microstructure

The price chart shows what happened. The order book microstructure shows how it happened.

I computed the order-book imbalance — (bid_depth - ask_depth) / (bid_depth + ask_depth) — for each snapshot, and shaded the normal range (mean ± 1σ during the pre-13:00 UTC baseline).

imbalance = (bid_depth_total - ask_depth_total) / (bid_depth_total + ask_depth_total)
Enter fullscreen mode Exit fullscreen mode

Imbalance vs baseline

The bid-side imbalance breaks clearly above the baseline band during the 14:00–17:00 UTC window — the yellow-shaded "pre-news breakout window." This is what informed accumulation looks like in the order book: buyers stacking the bid side aggressively before a known catalyst, ahead of where they think the price is going.

I also looked at the absolute depth on each side over the same window:

Bid vs ask depth

Both sides oscillate around 100–200K contracts pre-news, then go crazy after the announcement — the ask side spikes to 400K+ in several intervals as people try to take profit or trade against the new high price. Pre-news, the relative ratio is what matters; post-news, sheer volatility takes over.

What I conclude (and what I don't)

The defensible reading: the Polymarket book priced ~85% of the news before the news broke. That's an empirical observation, traceable in the data I committed to the repo, with the timestamps and identifiers anyone can re-verify.

There are two ways to interpret it:

(1) Someone knew the announcement was coming and bought ahead. The replication across three markets and the bid-side imbalance breakout make this consistent with the observable pattern. But "knew" is doing a lot of work in that sentence — knew through what? Reuters terminal? Diplomatic chat group? A leak from one of the negotiating teams? I have no way to tell.

(2) Prediction markets aggregate private information so efficiently that headline news is stale on arrival. The Munir-Tehran trip, the Rubio India agenda, the Gulf-leader phone calls, the Vance-Witkoff-Kushner draft — these were all being discussed in private channels through the day. Polymarket may simply have been faster at synthesizing the signal than CNN.

Both readings are bullish for the value of prediction-market data as an information source. Whichever you believe, the practical lesson is the same: if you wait for the headline, you're trading against people who didn't.

Caveats

  • One news cycle, one set of markets. I'm not claiming this is the typical pattern. To make that claim I'd need to run the same analysis across dozens of events and check the base rates.
  • My news timeline might be incomplete. I read 50+ sources but I can't claim exhaustive coverage of every Reuters/Bloomberg terminal headline. If there was a positive Iran-related wire at 14:15 UTC that I missed, the "front-running" story collapses.
  • Sampling cadence. 3-minute snapshots are coarse. Finer-grained data (10s or even per-trade) might surface even earlier signals or change the timing claims. Worth re-running.
  • "Insider trading" requires intent and material non-public information — both legal terms. I'm not making that claim. I'm describing what's visible in the book.

Try it yourself

Everything is in the repo: github.com/realfishsam/polymarket-iran-may-23.

git clone https://github.com/realfishsam/polymarket-iran-may-23
cd polymarket-iran-may-23
pip install -r scripts/requirements.txt

# Re-render the charts from the committed data
python3 scripts/render_heatmaps.py
python3 scripts/render_overlay.py
python3 scripts/render_supporting.py
python3 scripts/analyze.py

# Or pull fresh data with your own PMXT key
export PMXT_API_KEY=pmxt_...
python3 scripts/fetch.py
Enter fullscreen mode Exit fullscreen mode
  • data/*.csv — the book snapshots in tidy long format (snapshot_ts_ms, side, price, size). One file per market, ~30K rows each.
  • data/raw/*.ndjson — the raw streaming output from the fetcher
  • charts/*.png — every chart in this post, pre-rendered
  • scripts/ — fetch, analyze, render

If you find a public news item I missed that would explain the 14:18 UTC move, open an issue.

Stack

  • PMXT — historical order book API for Polymarket / Kalshi
  • Python 3.10
  • pandas, numpy, scipy
  • matplotlib (with a custom dark theme — code in scripts/render_heatmaps.py)

Cover chart and all visualizations in this post are CC-BY 4.0. Code is MIT. Polymarket is the data source; PMXT is the API that makes it queryable.

Top comments (0)