AymarData

Pull Polymarket historical order books, trades, and settlements with AymarData REST, Python, cURL, and bulk files — with working examples.

How to get Polymarket historical data

Teams need Polymarket history for backtests, academic work, compliance audits, and AI agent workflows. AymarData exposes historical L2 books, trades, and settlements through REST, optional bulk files, and MCP. This guide shows practical ways to download and query that history without scraping the live site.

Start from the Polymarket data hub to see which families are covered, then authenticate with a Bearer key (ad_live_…).

Choose a delivery path

PathBest forEntry point
REST point-in-timeInteractive research, agents/v1/book, /v1/trades
REST deltasHigh-frequency replay/v1/book/deltas
Bulk filesMulti-day offline jobs/v1/files, Bulk
MCPCursor / Claude toolsMCP platform

Builder and Pro plans differ on concurrency and file access — check Pricing. Bulk listing via /v1/files is available on Pro and Enterprise.

Decision rule of thumb

  • Need one book at one timestamp → REST /v1/book?at=
  • Need a path every few milliseconds → snapshot + /v1/book/deltas
  • Need weeks of many markets offline → list/download files, then DuckDB/Polars
  • Exploring interactively in an IDE → MCP, then export a REST script for CI

Authenticate

export AYMARDATA_API_KEY=ad_live_YOUR_KEY

All requests use:

Authorization: Bearer ad_live_YOUR_KEY

Details: Authentication. Rotate keys if they appear in shared notebooks; prefer environment variables over hardcoded secrets.

Discover markets and windows

curl -s "https://api.aymardata.com/v1/markets" \
  -H "Authorization: Bearer $AYMARDATA_API_KEY" \
  -G \
  --data-urlencode "source=polymarket" \
  --data-urlencode "q=btc" \
  --data-urlencode "interval=15m" \
  --data-urlencode "status=all" \
  --data-urlencode "limit=50"

Each row includes identifiers, interval, status, and history_from when available. Use those ids for subsequent calls. Family pages like btc-updown-15m map to the same q / slug vocabulary.

Interpreting discovery fields

  • market — Pass this string into book/trades/settlement calls.
  • history_from — Earliest retained reconstruction time for that window; skip rows without it when building historical grids.
  • interval — Confirms you filtered 15m vs 1h families correctly.
  • Status — Use resolved windows for labeled P&L; open windows for live monitoring only.

Cache market lists locally. Discovery changes slowly compared with books, and repeated discovery calls burn rate-limit budget needlessly (Rate limits).

Historical books

Pass at= as ISO-8601 UTC:

import os
import requests
from datetime import datetime, timedelta, timezone

API = "https://api.aymardata.com/v1"
H = {"Authorization": f"Bearer {os.environ['AYMARDATA_API_KEY']}"}

def historical_books(market: str, outcome: str, start: datetime, minutes: int = 15, step_s: int = 60):
    out = []
    t = start
    end = start + timedelta(minutes=minutes)
    while t <= end:
        at = t.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        r = requests.get(
            f"{API}/book",
            headers=H,
            params={
                "source": "polymarket",
                "market": market,
                "outcome": outcome,
                "at": at,
                "depth": 10,
            },
            timeout=30,
        )
        r.raise_for_status()
        out.append(r.json())
        t += timedelta(seconds=step_s)
    return out

# Example: sample every 60s across a known window open
series = historical_books(
    "btc-updown-15m",
    "up",
    datetime(2026, 10, 22, 14, 0, tzinfo=timezone.utc),
)
print(len(series), "snapshots")

For denser paths, fetch one snapshot then apply deltas (see deltas vs snapshots) instead of polling every second. Always pass outcome for two-sided Polymarket books — Up and Down are separate ladders.

Field notes on book rows

Store ts_exchange and ts_recv alongside mid/spread. Exchange time is what you want for strategy clocks; receive time helps latency audits. spread is best ask minus best bid when both sides exist; empty sides should skip mid calculation rather than coerce to zero.

Historical trades

curl -s "https://api.aymardata.com/v1/trades" \
  -H "Authorization: Bearer $AYMARDATA_API_KEY" \
  -G \
  --data-urlencode "source=polymarket" \
  --data-urlencode "market=btc-updown-15m" \
  --data-urlencode "outcome=up" \
  --data-urlencode "start=2026-10-22T14:00:00Z" \
  --data-urlencode "end=2026-10-22T14:15:00Z" \
  --data-urlencode "limit=200"

Paginate with cursor / next_cursor when present. Aggregate volume, VWAP, and aggressor side in your own store if you need daily rollups.

Trade interpretation tips

Trades validate whether quoted mids ever traded and help calibrate fill models. Do not substitute last trade for top-of-book when simulating entries on a two-sided market. For supervised learning on resolution, join trades to settlements rather than treating volume alone as a label.

Settlements

def settlement(market: str):
    r = requests.get(
        f"{API}/settlement",
        headers=H,
        params={"source": "polymarket", "market": market},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

Resolved windows are essential for labeled datasets. Combine history_from, trade prints, and settlement outcome for supervised models of resolution. Normalize outcome strings before joins. Missing settlement means the window is still open or not ingested — exclude from P&L tables.

Bulk files

When you need days or weeks offline, list available objects then download:

curl -s "https://api.aymardata.com/v1/files" \
  -H "Authorization: Bearer $AYMARDATA_API_KEY" \
  -G \
  --data-urlencode "source=polymarket" \
  --data-urlencode "prefix=btc-updown-15m"

File layouts (Parquet/CSV partitions) are described under Bulk delivery and GET /v1/files. Prefer files for DuckDB / Polars jobs; prefer REST for interactive at= lookups. Bulk downloads do not count against the REST budget the same way as chatty point-in-time polling — see plan notes in Rate limits.

def list_files(source: str, prefix: str = "", date: str | None = None, fmt: str = "parquet"):
    params = {"source": source, "prefix": prefix, "format": fmt}
    if date:
        params["date"] = date
    r = requests.get(f"{API}/files", headers=H, params=params, timeout=60)
    r.raise_for_status()
    return r.json()

REST vs files edge cases

  • Point-in-time reconstruction at arbitrary at= is a REST strength; files excel at sequential scans.
  • Empty file listings usually mean the date/prefix is wrong, the plan lacks bulk access, or partitions are not published yet — not that the market never existed.
  • Keep REST as a spot-check against a file row when schemas drift.

Storing a local research lake

A practical layout:

data/
  polymarket/
    markets.jsonl
    books/{market}/{date}.parquet
    trades/{market}/{date}.parquet
    settlements.jsonl

Write idempotently keyed by (market, outcome, ts_exchange). Keep raw API payloads when debugging schema drift. Align crypto reference series from Binance or Hyperliquid into the same partition scheme if you train cross-venue models. Related event contracts may also appear under Kalshi.

Minimal writer sketch

import json
from pathlib import Path

def append_jsonl(path: Path, row: dict):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a") as f:
        f.write(json.dumps(row, default=str) + "\n")

def ingest_settlements(markets: list[str], out: Path):
    for market in markets:
        try:
            row = settlement(market)
            row["market"] = market
            append_jsonl(out, row)
        except requests.HTTPError as e:
            append_jsonl(out.with_suffix(".errors.jsonl"), {"market": market, "error": str(e)})

Agents and MCP

If you work in Cursor or Claude Code, point the MCP server at your key and ask the agent to fetch books for a date range. Pair with Backtesting with an AI agent via MCP and Using with AI coding tools. Require the agent to emit REST scripts so CI can rerun without MCP.

Rate limits and etiquette

  • Burst carefully; respect rate limits
  • Cache market lists; they change slowly compared to books
  • Prefer deltas for fine-grained replay
  • Do not share live keys in notebooks published publicly
  • On HTTP 429, honor Retry-After rather than tight spin loops

Building a labeled dataset without leakage

Historical Polymarket work often ends in a supervised table: features known at decision time, label equal to settlement outcome. Leakage creeps in when settlement timestamps are earlier than you think, when you sample books after resolution, or when you use the full-window VWAP as a “feature” for an early-window decision. Keep a hard rule: every feature row carries an asof timestamp, and every join to settlement or crypto references must use only observations at or before asof.

A practical pipeline is: discover windows → filter to those with history_from and a settlement → sample books on a fixed relative schedule (for example +60s, +5m, +10m inside a 15m window) → attach trades aggregates that end at each sample → attach settlement once → write Parquet. That structure ports cleanly from REST to bulk files later without changing labels.

When coverage gaps appear (HTTP errors, empty books), keep them as explicit rows with a status column rather than dropping silently. Attrition that correlates with volatility will bias any edge estimate if you only keep “easy” windows. The backtest guide shows how skip counters surface that problem early.

Quality checks that catch bad pulls

Before you schedule a week-long backfill, run a one-day canary:

  1. List markets for one family and assert history_from is present on most resolved rows.
  2. Fetch a book at history_from + 60s for Up and Down; assert both sides or record one-sidedness.
  3. Fetch trades for the same window; assert timestamps fall inside the interval.
  4. Fetch settlement; assert outcome is up or down (or the family’s vocabulary).
  5. Reconstruct via deltas for one minute and compare to /v1/book?at= at the end of that minute.
  6. List /v1/files for the same source/prefix if your plan includes bulk; note whether partitions exist yet.

If step 5 fails, fix apply logic or seed depth before trusting any path-dependent model. If step 6 is empty while REST works, keep using REST for that range and revisit bulk later. These checks are boring; they are also how teams avoid discovering broken joins after training overnight.

Naming and versioning your extracts

Historical extracts rot when filenames omit source, outcome, and schema version. Prefer paths like polymarket/btc-updown-15m/outcome=up/dt=2026-10-22/ and keep a small manifest.json that records the API base URL, pull time, and code git SHA. When AymarData adds fields, manifests tell you which partitions need a refresh instead of forcing a full re-download. The same habit makes MCP-generated scripts safer: the agent can append to a known layout instead of inventing a new folder each session.

FAQ

Is scraping Polymarket allowed / enough?
Scraping is brittle, incomplete for L2 history, and often against venue terms. AymarData is built for authorized historical access under your Data License.

How far back does history go?
See each family’s data page and history_from on /v1/markets. Coverage expands as collectors run; paid plans retain stored history.

Can I export CSV only?
Use bulk files where offered, or convert REST JSON to CSV/Parquet in Python. REST remains the source of truth for point-in-time reconstruction.

Do I need outcome on every call?
For two-sided Polymarket books and trades, yes — pass up/down (or yes/no) explicitly to avoid ambiguous responses.

Educational content, not trading or investment advice. Past data doesn't predict future results.