v3.1 preview
OK you grubby data degens, I have been hard at work on v3.1 and hope you like it. Faster queries and smaller downloads for you. More compute and complexity in delivery for me. You are welcome.
The ask: test it and tell me whether it is ready to roll out or needs changes. Post in Discord or use the feedback form.
Three complete hours, 2026-10-11 00:00 to 03:00 UTC. They are the same hours as in /v3/, so you can put the two side by side. v3 stays the live format until the feedback is in. The README has the full reader notes, what we tested and how fast it ran. Start with quickstart.py: it needs only DuckDB and reads an hour straight from these links.
Why so many files: each hour is one file per category, so a question about one market or one category
reads one or two event files (plus their provenance files if you want those). The hour's manifest.json
lists them all, and the quickstart reads it for you.
Coming in a few days: a small per-hour twin check, so you can confirm that the mirrored rows you rebuild
match ours. The large mirror_dropped audit lists will then stop being published; we keep checking every hour.
One more, remembered while writing this: v3.1 will also carry each recorder's own record of its connection: every disconnect and reconnect, with the gaps between them stated outright rather than left for you to infer, plus a short summary per recorder for each hour. We have been recording these since 6 October. They are not in this sample yet.
A quarter of the download
The event files for an hour are about 25% of the v3 file: 519 MB instead of 2.17 GB for the biggest hour so far. wakamex spotted that every price change shows up on both tokens and that the bookkeeping columns were most of the bytes, and David found the same twins in best bid and ask and in books. Thanks both.
More detail
Three things make it smaller. With the provenance files, every v3 row and column comes back, except book snapshots identical to the token's previous book, which add nothing.
- Mirrored rows kept once. In a two-outcome market every update to one token is mirrored on the
other (price 1 - p, sides swapped). When the mirror is exact we keep one copy and set
mirror_dropped = true. That was about 44% of the rows in these hours. Only exact mirrors are dropped: if anything differs, both rows stay. - Repeat books dropped. A book snapshot identical to the token's previous book adds nothing.
- Bookkeeping moved out. Which machines saw each row, receive times and sequence numbers now live in separate provenance files, so you only download them if you want them. Event and provenance files together are about 70% of v3 (1.54 GB against 2.17 GB for hour 00), and most questions need only the event files.
| Hour | v3 | v3.1 event files | v3.1 provenance | Rows, v3 to v3.1 |
|---|---|---|---|---|
| 00 | 2.17 GB | 519 MB | 1.02 GB | 180 M to 90 M |
| 01 | 2.11 GB | 503 MB | 990 MB | 177 M to 89 M |
| 02 | 1.93 GB | 464 MB | 908 MB | 163 M to 82 M |
Faster queries, by design
Each hour is split into one file per category (17 of them; sports is big enough to come in two parts), each sorted by market, token and time, in bigger groups with half the rows. Engines skip the row groups that cannot match. The row-group size came from Kuinox's benchmarks across four engines, so thanks Kuinox.
More detail
Every Parquet file is split into row groups, and each group records the smallest and largest value of every column. A query engine reads those few bytes first and skips any group that cannot hold what you asked for. v3 was already sorted by market, token and time. v3.1 goes further in three ways: each category is its own file, so a whole category is one or two files; groups are about four times larger, so an engine makes fewer, bigger reads; and the file holds half the rows and only the columns most queries need, so there is less to read in the first place.
Group size is a trade-off. Too small and an engine makes thousands of tiny reads; too big and it reads more than it needs. Kuinox rewrote a v3 hour at sizes from 100 thousand to 10 million rows per group and timed DuckDB, Polars, DataFusion and chDB on each. Small groups were 5 to 7 times slower in chDB. We settled on about a million rows per group, cut only where the category changes. Timings on hour 01 in DuckDB:
| Query | v3 | v3.1 |
|---|---|---|
| One market, local copy | 0.037 s | 0.005 s (its category's files) |
| Latest best bid and ask per token, local copy | 0.64 s | 0.39 s |
| One whole category, local copy | no category column | 0.018 s |
| One market, over the internet | 3.7 s | 0.24 s (its category's files) |
Over the internet, read only the category you need: market_categories.parquet gives every market's
category. Reading every file of an hour costs many requests and runs into the rate limit.
Your numbers will differ. Tell us what you see in your engine.
Just the data you want, at the detail you choose
Pick the slice (a category, a market, a token) and the detail (trades only, top of book, or full depth) and read only that. In hour 01, trades are 54 thousand of 88.7 million rows. Daniel Sapkota asked for exactly this: "I want all temperature books". That is now one filter on the Weather category.
More detail
Pick the slice. Because of the layout above, a filter on category, market or token reads only the groups that hold it, and a time window inside one of those narrows it further. Crypto is 8.4 million of the 88.7 million rows in hour 01, so a Crypto-only query reads about a tenth of the file. Engines that read Parquet over HTTP, such as DuckDB and Polars, fetch only those parts, so you can query an hour without downloading all of it. For repeated work, download the hour once and query it locally.
Pick the detail. The event_type column sets the resolution. Rows in hour 01, by type:
| If you want | Read | Rows in hour 01 |
|---|---|---|
| Trades only | last_trade_price | 54 thousand |
| Top of book over time | best_bid_ask | 8.0 million |
| Full depth | book plus price_change | 80.6 million |
| Market lifecycle | new_market, market_resolved, tick_size_change | 36 thousand |
Pick the depth and the interval. For the order book itself, the quickstart's
book_snapshots gives a token's book at any interval you choose (every second, minute, 10 minutes) and to any
depth (the top N levels or the full book), straight from these files with DuckDB. It matched our replay engine on every
level we compared, and one token over a whole hour takes about 5 seconds at one-second snapshots ten levels deep.
Pick one side or both, carefully. In a two-outcome market almost every update to the second token
(complement_token in markets.parquet) is stored as a mirrored row of the first. A plain filter on
the second token's id sees about 1% of its rows. Query by market and rebuild the mirrored rows with the quickstart's
expand_twins. With the provenance files passed in, it gives exactly the rows and columns v3 had for that
market, except dropped repeat books.
Pick the extras. Receive times, sequence numbers and which machines saw each row sit in the provenance file, and the opening state and reference prices have their own files. Download them only if your question needs them.
Start at any hour
Each hour ships the state of every market at its first second, as far as our recording knows it (a token we never saw a full book for is marked unknown), so you can replay or backtest from that hour without downloading the hours before it.
More detail
opening_state.parquet gives every token's state at hh:00:00: the full book where we have one, best
bid and ask, last trade and tick size. The books come from the same replay engine that rebuilds books from these
files, carried forward from our own recording of the previous hours, and marked derived = true. Book times
are the top of the hour for now; the last trade carries its own time.
To check it, we compare every real book that arrives during the hour with our carried book for that token at that moment. They matched 98.4% to 98.8% of the time across these hours. The misses are messages missed earlier, which stay wrong until the next real book corrects them. A token we have never seen a full book for gets best bid and ask and its last trade, with the book marked unknown (null); an empty list means the venue's book really was empty.
The prices crypto markets settle on
Chainlink and Binance prices for the crypto Up or Down markets, plus the 30 and 60 second TWAPs they actually settle on. raph asked for the Chainlink streams behind these markets, and Aldous Huxley has offered Chainlink history back to December 2025, so earlier hours can get these too. Thanks raph and Aldous.
More detail
reference_prices.parquet holds the Chainlink (8 coins) and Binance (6 coins) prices Polymarket
streams, about one a second. Since August, the 5 minute markets settle on a 30 second Chainlink TWAP, and the 15
minute and 4 hour markets on a 60 second one. With these you can see exactly what a market settled against, and
backtest anything that trades into the close.
Polymarket does not publish the TWAP formula, so we worked it out from Chainlink's own TWAP prints. It is the
plain average of the one-per-second Chainlink price over the window. Our rebuild matched Chainlink's value in every
complete window we tested (3,456 of 3,456 at 30 seconds, 1,884 of 1,884 at 60). These three hours predate our
recording of the TWAP stream, so their TWAP rows are rebuilt and marked derived. About 2% of seconds
never arrive on the Chainlink stream, and the missing_seconds column counts them in each window.
The signed order behind each fill
Each hour now also has an order-intent.parquet: the full signed order behind each on-chain fill on Polymarket's two main exchange contracts, read from their mined matchOrders calls on Polygon. Abomination81's ideas around mempools led us here.
More detail
One row is one order in a settlement transaction: the transaction hash (tx_hash), the fill's log index
(fill_log_index), the order_hash, the order's wallet (maker), whether it was the taker
(is_taker), the amounts, the fee, and the order's own timestamp client_timestamp_ms. Join per fill on
tx_hash and fill_log_index, not on the hash alone: one transaction holds a taker and several makers.
Fills on the newer combinatorial contract are listed from their logs only, without the order (about 0.5% of fills in hour 00).
Each hour's order-intent.manifest.json gives the block range, the match rate and what every row kind means, and the
README has more.
Three things to know before you join it to the event files:
- Join venue hour H against on-chain hours H and H+1. A fill lands in a block a few seconds after the venue's trade message, so trades near the end of an hour settle in the next hour's blocks.
token_idis text,asset_idis bytes. Heretoken_idis the decimal uint256 as text; the event files'asset_idis the same number as 32 bytes, big-endian. In Python,bytes.fromhex(f"{int(token_id):064x}")converts it.client_timestamp_msis the signer's, and 0 means unset. Whoever signs the order sets it, and it is not when the order was signed or sent. A 0 means the signer left it unset: about 1% of order rows in hour 00, from 12 wallets. Treat it as missing.
Book-equivalent, and checked
Join the provenance file and expand the mirrored rows, and you get back everything v3 told you. The only rows not rebuilt are book snapshots identical to the token's previous book, which add nothing.
More detail
Every hour passed 19 checks before going up, with one more check not run in this preview. They cover row
counts, ordering, nulls, schema and layout. They also confirm that every dropped mirror rebuilds exactly, and that
the drops can be recomputed from the source independently. Each hour's verify_report.json lists them.
We also ran 23 user-style tests on each hour against v3: every row comes back, four busy markets rebuild exactly on
all 28 v3 columns, and books replayed minute by minute match. The scripts and results are in
user-tests/, so you can run them yourself. How to expand the mirrored
rows is in the README, and markets.parquet gives each market's two tokens.
Where v3.1 started
Abomination81 shared seven recommendations, drawn from what their own Polymarket recorder keeps and ours did not.
Two are in this sample: the Chainlink and Binance prices in reference_prices.parquet (raph asked for
them too). Another, a log of every connection with explicit gap intervals, has been recorded on all our recorders
since 6 October and is due to be published with v3.1. Their ideas around mempools also helped us come up with a
method for recording Order Intent, the signed orders behind on-chain fills. It is in this sample now, one file per
hour (above). Thanks, Abomination81.
What to tell us
How long the downloads took, how fast your queries ran and in which engine, whether the opening state and the mirror expansion were easy to use, and anything you want added. Discord or the feedback form.
Help us find bloat. Some trimming is already decided for when v3.1 goes live (the README lists it:
millisecond receive times, no public audit lists, no derivable spread column), bringing event and provenance
files to about 56% of v3. If there is a column or file you never use, or would happily trade for a smaller download,
tell us.
The files
| File | Size |
|---|---|
| README.md | 23.5 KiB |
| SHA256SUMS.txt | 16.5 KiB |
| quickstart.py | 29.9 KiB |
| 2026-10-11/market_categories.parquet | 8.3 MiB |
| 2026-10-11/markets.parquet | 24.5 MiB |
| user-tests/http-results-2026-10-11T01.json | 3.8 KiB |
| user-tests/http_tests.py | 7.1 KiB |
| user-tests/results-2026-10-11T00.json | 5.6 KiB |
| user-tests/results-2026-10-11T01.json | 5.6 KiB |
| user-tests/results-2026-10-11T02.json | 5.6 KiB |
| user-tests/user_tests.py | 12.3 KiB |