First file in under a minute.
Time-to-first-file is the metric. Get a key, list released days, stream one, verify its hash.
1 · Get a key
Keys are created in the portal and prefixed pmk_. The token is shown once at creation and stored only as a hash. Set it in your shell:
$ export PM_KEY=pmk_your_token
2 · The three curls
"$PM_API/v1/availability?source=binance_spot" | jq .days
$ curl -sH "Authorization: Bearer $PM_KEY" -o trades.parquet \
"$PM_API/v1/download?source=binance_spot&channel=crypto_trades&scope=BTC&day=<released-day>"
$ curl -sH "Authorization: Bearer $PM_KEY" -o trades.manifest.json \
"$PM_API/v1/manifest?source=binance_spot&channel=crypto_trades&scope=BTC&day=<released-day>"
$ shasum -a 256 trades.parquet # == .sha256 in the manifest
3 · Read the parquet
# or in python:
import duckdb; duckdb.sql("select * from 'trades.parquet' limit 5")
That is the whole loop. Every file you pull is a released day; unreleased days simply are not there to fetch.
Four endpoints, released days only.
Base URL https://api.promethean.markets. Bearer auth; keys are prefixed pmk_, shown once at creation, stored hashed at rest. Every key has a daily download budget (default 20,000): only /v1/download consumes it — the catalog, availability, and manifests are free, so verification never costs budget. Exceeding it returns 429 and never charges; unauthorized requests never consume budget.
GET /v1/datasets
Machine-readable catalog: sources, channels, scopes, schema info.
GET /v1/availability
| Param | Required | Description |
|---|---|---|
| source | no | Optional filter — source id, e.g. binance_spot; omit for every source. |
{ "inception_day": "<inception-day>", "latest_day": "<latest-day>", "gapless": true, "days": { "<day>": {…}, … } }
GET /v1/download & /v1/manifest
| Param | Required | Description |
|---|---|---|
| source | yes | Source id. |
| channel | yes | Channel, e.g. crypto_trades. |
| scope | yes | Coin or tier, e.g. BTC. |
| day | yes | UTC day YYYY-MM-DD. Must be released. |
/v1/download streams the day's Parquet; /v1/manifest returns the same file's manifest JSON.
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_or_invalid_api_key | Missing or unknown bearer token. Never consumes budget. |
| 403 | source_not_in_key_scope | The key's scopes do not include this source. |
| 404 | day_not_committed | The day is not in the released chain (withdrawn or never released). |
| 404 | object_not_released | The source/channel/scope/day object does not exist in the released lane. |
| 416 | invalid_range | The Range header cannot be satisfied for this object. |
| 429 | daily_budget_exceeded | Key over its daily download budget. Resets at 00:00 UTC; never charges. |
Machine-readable spec: openapi.json — import it into any client generator or API tool.
Read it in your stack
Files are standard Parquet — anything that reads Arrow reads the archive. No SDK required.
import io, requests, pandas as pd
r = requests.get(f"{API}/v1/download",
params=dict(source="binance_spot", channel="crypto_trades", scope="BTC", day="<released-day>"),
headers={"Authorization": f"Bearer {KEY}"})
df = pd.read_parquet(io.BytesIO(r.content))-- download once with curl, then query in place
SELECT count(*), min(ts_ms), max(ts_ms)
FROM read_parquet('trades.parquet');library(arrow); library(httr)
resp <- GET(paste0(API, "/v1/download?source=binance_spot",
"&channel=crypto_trades&scope=BTC&day=<released-day>"),
add_headers(Authorization = paste("Bearer", KEY)))
df <- read_parquet(rawConnection(content(resp, "raw")))Columns, per channel.
Two representative channels below. Columns are marked identity (what the row is) or quality (freshness / lineage metadata).
binance_spot · crypto_trades
| Column | Type | Null | Description |
|---|---|---|---|
| ts_ms | int64 | no | Exchange event time, ms UTC. identity |
| receive_ms | int64 | no | Receive time; always ≥ ts_ms. quality |
| price | double | no | Trade price. |
| amount | double | no | Trade quantity. |
| trade_id | string | no | Stable row id; unique per scope/day. |
polymarket_updown · updown_book_snapshot_100ms
| Column | Type | Null | Description |
|---|---|---|---|
| bucket_ms | int64 | no | 100ms grid timestamp. identity |
| source_ts_ms | int64 | no | Timestamp of the source snapshot used. quality |
| source_lag_ms | int64 | no | bucket_ms − source_ts_ms; the true freshness. quality |
| freshness_state | string | no | Per-row freshness classification. quality |
| bid_prices | list<double> | no | Top-20 bid prices; sizes are in bid_sizes. |
One file, one manifest.
Layout
Day-partitioned Parquet, one file per source / channel / scope / day, each with a sibling manifest:
{source}/{channel}/{scope}/{day}.manifest.json
Manifest fields
| Field | Description |
|---|---|
| sha256 | Hash of the Parquet file. Re-hash yours; it must match. |
| row_count | Exact row count. Recompute to confirm. |
| schema_fingerprint | SHA256 schema fingerprint; released sample 4f8b025ecbc7c9cc7b246eda92ff863437722d4c06af919760ff2e0d8cd2d7fc. |
| min_event_ts_ms | Minimum event timestamp in the file. |
| max_event_ts_ms | Maximum event timestamp in the file. |
Re-verify
$ jq -r .sha256 trades.manifest.json # must be identical
$ duckdb -c "select count(*) from 'trades.parquet'" # == row_count
Honest caveats, per family.
Every dataset carries millisecond-UTC timestamps: receive_ms is when our collector received the row (our clock, never rewritten); source_ts_ms / ts_ms is the venue's own event time; bucket_ms on aligned grids uses latest-prior semantics — a row can never contain information from its future. Below is every documented caveat, mirrored from the data contract; each is enforced or disclosed by the release audits, not left to fine print.
Day membership
Rows belong to the UTC day of their event timestamp — except polymarket_chainlink_rtds/price_ticks, whose day membership is receive_ms (the venue emits whole-second source stamps); a day's first ticks may carry a source stamp up to a few seconds before midnight, bounded at 60s by the audit.
Cross-midnight carry
The first buckets of a day on aligned datasets legitimately carry the last pre-midnight state (carried_forward with a pre-day source stamp). That is continuity, not leakage.
Predict.Fun venue clock skew
Predict.Fun stamps venue timestamps from its own clock, observed up to ~7.4s ahead of true receive time. Rows store both timestamps honestly; the audit bounds the skew at 30s. Expect small negative receive-minus-source values on this venue.
Predict.Fun missing venue stamps
Some venue book responses carry no update time (updateTimestampMs: 0). Such rows store source_ts_ms = receive_ms — the documented receive fallback — and the audit rejects any non-positive venue epoch. Days collected before that fix (2026-07-16..18) were withdrawn in the launch reset.
Restart seams
The legacy coordinated collector restart at the UTC boundary creates a head seam. Under the binding zero-seam policy that seam is not tolerated in a customer day: the exact receive-cadence and grid ledgers stay red until collection is continuous. Seams are never excused into a released day.
Chainlink RTDS wording
This is the Polymarket-relayed Chainlink reference feed — not paid or signed Chainlink Data Streams, and not the on-chain Data Feeds contract.
Binance trade-stream completeness
Venue trade ids are dense integers, so id arithmetic is a completeness oracle: our 2026-08-01 row-level audit measured small spot losses outside the restart seam (≈0.1–0.3% of a day's ids — sub-second frame drops in trade bursts, plus rare multi-minute upstream feed stalls that also hole trades and candles). The release ledger now computes per-coin id-continuity (missing-id runs with seam attribution, outage runs, missing share) and discloses it in every day's ledger while enforcement thresholds are calibrated. On futures, the venue stream excludes non-market trades (insurance fund / ADL), so id gaps there are structural — only an id gap bracketed by a real time gap marks an outage.
Binance intra-millisecond trade order
Within a single ts_ms, persisted row order is stable-sort order by our content-hash trade id, not venue sequence. Venue sequence is fully recoverable by sorting on venue_trade_id as an integer — tape-replay consumers should do so.
Binance books are sampled with a bounded hold
crypto_book_snapshot_20 re-emits the latest venue book every 100ms and stops once the held book is older than 30s (the stream watchdog window). During an upstream stall, rows keep arriving at receive cadence with a frozen venue ts_ms — receive_ms − ts_ms is the honest hold age. Rare venue frames without a timestamp get their ingest time pinned once (collector fix at the 2026-08-03 boundary); earlier days hold a small number of such rows whose ts_ms equals each emit's receive_ms.
Polymarket Up/Down trades before the price window
Markets are tradable from venue listing, so updown_trades legitimately contains prints before a market's price window opens (14.9% of trades on the audited day; zero after window close). Grids are window-bounded by design — joins from trades to grids must expect pre-window prints.
Binance upstream gap allowance (60s/day, disclosed)
Binance streams its market data from infrastructure roughly 9,000km from our collectors and does not replay missed frames. We run two independent merged upstream connections per stream, which mask single-connection stalls — but venue-side shutdown waves can close every connection at once, which redundancy on one venue cannot cover. A Binance day stays green while the summed receive-gap time per coin — time with no book sample at all, beyond the 1.1s freshness line — stays within 1% of the day (864 seconds), itemized in the day's ledger (gap_allowance_used_ms, daily_gap_allowance_ms). Days exceeding the allowance are red, never silently truncated. The 1% figure is calibrated, not convenient: a six-day survey of time when all five coins were unfresh at the same instant — something five independent markets do not do by chance — measured 226–658 seconds per day normally (~99.5% freshness), and 13,681 seconds on the day of our own fleet outage. Normal venue behaviour passes with ~3× headroom; that outage fails by 15×.
A known limit of that measure: during a stall we re-emit the last venue book on the 100ms cadence with a frozen ts_ms, so those rows arrive on schedule while carrying held content and the gap measure does not see them. Every row carries receive_ms − ts_ms as its honest hold age. We do not score held time today, because a quiet market produces the same signal — separating held-because-stalled from held-because-quiet needs cross-connection correlation, and that work is open.
No upper bound is claimed. Binance publishes no availability SLA or maximum recovery time for public market streams. Our largest observed book gap is 104.5s and largest held-book age 64.1s — empirical maxima from our own record, not guarantees. When every ingest path loses the venue at once, order-book states in that interval cannot be reconstructed from Binance's public depth API; trade gaps remain repairable from id-based history.
Auth, budgets, fair use.
Auth model
Bearer tokens prefixed pmk_. Shown once at creation, stored only as a hash. Rotate by creating a new key and revoking the old one; revoked keys stay listed for audit.
Budgets
Each key carries a daily request budget (default 20,000). Authorized requests count; 401s do not. Over budget returns 429 daily_budget_exceeded; the counter resets at 00:00 UTC.
Requesting a raise
Email your key label and expected daily volume. Desk plans carry higher, custom budgets and per-key overrides.