How to Get Polymarket Trade Data
- Every Polymarket CLOB fill from 2020 to April 2026 is free on Hugging Face under CC-BY-4.0. 1.17 billion fills, 2.63 million makers and $59.9 billion of volume. At the time of writing that is the largest public Polymarket dataset available.
- You query it with DuckDB over HTTPS. No download, no Envio API token, no Polygon node.
- For data at head, both Polymarket indexers are open source HyperIndex projects. Clone the v2 indexer for live v2 markets, or the v1 indexer for both generations, and run it locally or on Envio Cloud. The v1 indexer is what produced the snapshot.
- 2 things that catch people out. Addresses are EIP-55 checksummed, so wrap filters in
lower(). And an account can trade from a proxy and directly from its signer, so query both or you can miss 99.7% of a wallet with no error, as Example 3 shows. - The worked examples are Co-Founder Jonjon's findings, with the queries that check them against the data. All of them reproduce.
2,684,676 wallets hold a position on Polymarket. Roughly 81% of them finished within $1,000 of where they started. 21 cleared more than $10 million each, and the top 100 took $853 million between them. Those figures are from Co-Founder Jonjon's realized-PnL distribution and his Top Hundred series.
Polymarket settles on Polygon, and a position there is just a token that pays $1 if the outcome happens and $0 if it does not, so entry, exit and settlement all leave a record onchain that outlives the market itself. The site shows you the odds. The chain shows you who took the other side of them, what they paid, and how it ended.
We indexed every order-book fill Polymarket has settled from its 2020 launch through 24 April 2026, plus the positions, splits, merges and redemptions around them, and published the lot as a free dataset on Hugging Face under CC-BY-4.0. That is 1.17 billion fills on the central limit order book, the CLOB, plus 2.63 million distinct makers and $59.9 billion of lifetime volume. Reading it needs no indexer, no API key and no Polygon node. It needs DuckDB and one query.
We start with the snapshot, since it needs nothing installed. Then we walk through 5 of the wallets pulled out of these tables, with queries you can run yourself to check every number. If you want markets at head rather than history, our open indexers are the last section.
How to Query the Public Snapshot with DuckDB
The public v1 snapshot is Polygon data indexed with HyperIndex and released under CC BY 4.0. You can use it for anything, including commercially, subject to the licence terms, which include crediting Envio and linking the licence.
Every CLOB fill is in it, plus splits, merges, redemptions, resolutions, and trades from the FPMM era, the automated market maker Polymarket used before the order book, going back to Sep 2020. It is a point-in-time export, not a live feed. SNAPSHOT.json records the cutoff as Polygon block 85,948,287 on 24 April 2026. Anything after it needs the live indexer.
SNAPSHOT.json on Hugging Face. Cutoff for the v1 snapshot.
Hugging Face dataset card. What is inside the v1 snapshot.
| Metric | Value | How |
|---|---|---|
CLOB fills (order_filled) | 1,172,658,611 | count(*) |
| Distinct CLOB makers | 2,630,334 | count(DISTINCT maker) on order_filled |
| Lifetime CLOB volume | $59.899B | cash-leg sum, USDC 6 decimals |
user_position rows | 303,955,230 | count(*) |
Distinct user_position users | 2,684,676 | count(DISTINCT user) |
wallet.parquet rows | 7,362,437 | proxy + Safe rows. Not the trader count. |
Figures from the Hugging Face dataset card. The queries below are how you read them.
Lifetime CLOB volume:
SELECT sum(
CASE WHEN makerAssetId = '0' THEN CAST(makerAmountFilled AS HUGEINT)
ELSE CAST(takerAmountFilled AS HUGEINT) END
) / 1e6 AS volume_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet';
-- 59899137953.107475
The 2,684,676 distinct users on user_position are position-holders, and that is the population the realized-PnL distribution covers.
The dataset card lists 2.74 billion records across the entity tables, about 127 GB.
Step 1: Install DuckDB
Grab it from the DuckDB installation docs if you do not have it. The queries below were run on DuckDB 1.5.5, and they need a version new enough to support hf:// paths natively.
Step 2: Query the Parquet over HTTPS
No Envio API token needed. DuckDB range-reads the file.
duckdb -c "
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
"
-- 1172658611
Distinct CLOB makers:
SELECT count(DISTINCT maker)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet';
-- 2630334
Position-holders:
SELECT count(DISTINCT user), count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet';
-- 2684676, 303955230
Hive partitions prune by year. This 2025 monthly-volume query is one of the examples on the dataset card.
SELECT strftime(to_timestamp(CAST(timestamp AS BIGINT)), '%Y-%m') AS month,
sum(CASE WHEN makerAssetId = '0' THEN CAST(makerAmountFilled AS HUGEINT)
ELSE CAST(takerAmountFilled AS HUGEINT) END) / 1e6 AS volume_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/year=2025/**/*.parquet'
GROUP BY 1
ORDER BY 1;
-- one row per month of 2025
Addresses in the snapshot are stored EIP-55 checksummed. order_filled.maker looks like 0x63CE342161250D705dC0b16dF89036C8E5F9Ba9a, and the casing is not consistent between tables either. A lowercase filter matches nothing and returns zero rows with no error, which reads like the wallet is missing rather than like a bad filter.
Wrap the column, not the literal.
-- returns nothing
WHERE maker = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a'
-- returns the fills
WHERE lower(maker) = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a'
Everything else is straightforward. Amounts are BigInt in smallest units, so USDC divides by 1e6. The CLOB cash leg is assetId = '0'.
5 Wallets You Can Verify
Numbers are from the original posts, linked in each section. There are 4 posts and 5 wallets, because Example 3 covers 2 of them. They are unrelated operators running different strategies, so there is no point adding their PnL together.
Checking most of them is the same 2 queries. Filter order_filled on maker or taker for the fills, and user_position on user for the positions, both wrapped in lower(). Example 3 is the exception and takes longer, because that wallet does not sit where the rule says it should.
One thing worth knowing before you start. Most Polymarket accounts are a proxy or Safe contract that does the trading, controlled by a signer address that never appears in order_filled at all. If a wallet from a post returns no fills, look it up in wallet.parquet first.
SELECT id, signer, type
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/wallet.parquet'
WHERE lower(signer) = '0xdb15373c33adb64de90f23f90c0d8b86ef65497b';
-- 0xbddf61af533ff524d27154e589d2d7a81510c684 | 0xdb15373c33ADb64de90f23f90c0d8B86eF65497B | proxy
The id is the address that trades. Query both it and the signer, though, not just the id. Most signers never appear in order_filled, but some trade directly as well as through their proxy, and querying only the id on one of those returns a number that is far too small, with no error to tell you.
Example 1: Buying Both Sides and Merging for $1
This wallet posts buy orders on every outcome token of every binary market, at every price level. When it holds both the YES and the NO token of the same market, it merges them for one dollar. That is the entire strategy.
The proxy that trades is 0x2005d16a84ceefa912d4e380cd32e7ff827875ea, controlled by 0x5d4fd194c4181ad61b1b5cb72dab8f9c4f9a2edc. Both are linked in the post. The article reports rank #24 by realized PnL, about $24 million net after fees, 2,698,796 fills, 44,954 markets traded simultaneously and 289 active days. Maker share is 90% of fills, and the maker book is BUY-only, 243 sells out of 2.43 million maker fills. Query the proxy. The controller address has no fills of its own.
Maker BUY price distribution from the original post. Example 1.
Step 1: Pull the fills
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0x2005d16a84ceefa912d4e380cd32e7ff827875ea'
OR lower(taker) = '0x2005d16a84ceefa912d4e380cd32e7ff827875ea';
-- 2698796
Step 2: Pull the positions
SELECT count(*), sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS realized_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0x2005d16a84ceefa912d4e380cd32e7ff827875ea';
-- 111318, 8553975.64
user_position carries position-level realized PnL, which on its own does not reach the $24M headline. The post breaks that $24M into 3 parts. Roughly $8M of riskless profit from the merge engine, 41,642 merges across $100.2 million of merge volume. Longshot redemptions, where bids resting at a cent or two occasionally win and redeem at $1. And post-event taker sells of tokens that have clearly won, at prices near $0.99. The merge activity has its own table in the snapshot, merge/.
Example 2: NBA Live Model, and the Same-Day Correction
This wallet watches NBA games live, updates a probability model as the score moves, and buys the side its model says has the game in hand.
The post links 0xdb15373c33adb64de90f23f90c0d8b86ef65497b. In the snapshot that address is a signer with no fills of its own, and the figures below land on its proxy, 0xbddf61af533ff524d27154e589d2d7a81510c684. That is the one to query.
The article reports $23.6M realized, 95.4% (417/437 closed bets), 116,086 fills, $60.3M volume, 523 markets, and an active period of 168 days, present on 79% of them. That 79% is 133 days of actual trading, which is what the snapshot shows.
The same-day correction says 95% is a selection artifact. Across all 864 positions opened, 47.3% were on the eventual winner. The closed-bets count drops 427 positions flattened pre-resolution at about zero PnL. Its read on the real edge is tiny mispricing at the bid, flattening bad bets fast enough to turn 38 cent losses into 2 cent ones, and holding winners to $1. What makes it work is how fast it gets out of a losing position, not what it predicts.
Activity dashboard from the original post. Example 2.
Step 1: Pull the fills
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0xbddf61af533ff524d27154e589d2d7a81510c684'
OR lower(taker) = '0xbddf61af533ff524d27154e589d2d7a81510c684';
-- 116086
Step 2: Pull the positions
SELECT count(*), sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS realized_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0xbddf61af533ff524d27154e589d2d7a81510c684';
-- 1301, 23630117.10
Both figures land on the numbers in the post, 116,086 fills and $23.6M realized. Note that user_position holds one row per token the wallet ever touched, so its 1,301 rows are not the same measure as the 864 positions opened in the correction, or the 437 that reached resolution.
Example 3: 2 Wallets, and an Account With 2 Addresses
This example covers 2 wallets. The one from the UMA Gap article, which runs a settlement sweep and a basket arb from a single address, and the one from the Day 3 post, which runs the basket arb and is where querying the snapshot gets interesting.
The UMA Gap Wallet
0x9b979a065641e8cfde3022a30ed2d9415cf55e12. In wallet.parquet it is a proxy, controlled by signer 0x8Dcd34aeF17AB9f121d5198E80d8d683a2274EAE.
The article's own summary of it lists leaderboard rank 26, $8,049,419 of lifetime realized PnL, 61,095 fills across 4,862 markets in 12 categories, $84.37M of volume, 47,754 buys at an average price of $0.97, first fill on 13 May 2023, and 93.1% of every buy landing above 95 cents. In the article's words, that last line is the whole strategy in one number. The chart below plots the same 47,754 buys, cut at $0.97 rather than 95 cents, which puts 92% of them above the line.
Every buy by the wallet, by price. The bars at the right-hand edge are the strategy. From the UMA Gap article.
This wallet only buys things the market has already decided. A Polymarket binary has a listed close time, but the cash payout only fires once UMA's optimistic oracle clears its dispute window, which is 2 hours at minimum and often a day. Inside that window the winning token still has a book, and it trades at $0.99 or $0.998 rather than $1.00, because the cash has not landed yet. This wallet is the patient buyer. The article calls it the patience premium, paid roughly 0.8 cents per dollar of near-par exposure for being willing to wait for the protocol to catch up.
The article breaks those near-par buys down by when they landed relative to the market's listed close.
| When the fill landed | Buys at >$0.97 | Volume |
|---|---|---|
After market_end, event already over | 26,970 | $37.6M |
| Within 1h before end | 562 | $0.59M |
| 1 to 6h before end | 2,843 | $4.13M |
| 6 to 24h before end | 1,077 | $1.94M |
| 1 to 7d before end | 10,457 | $15.18M |
| >7d before end | 2,025 | $10.39M |
Buy timing versus market_end, from the UMA Gap article. The rows sum to 43,934 buys and $69.83M, and the top row alone is 61.4% of them.
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12'
OR lower(taker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12';
-- 61095
61,095 is the lifetime fill count reported in the UMA Gap article.
SELECT count(*) AS positions,
count(*) FILTER (WHERE CAST(avgPrice AS DOUBLE) / 1e6 > 0.97) AS near_par,
sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS realized_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12';
-- 10046, 4022, 8049418.94
The 4,022 near-par positions and the $8,049,419 lifetime figure are exactly the numbers in the UMA Gap article.
Break those 4,022 down and the patience premium is visible directly.
SELECT
count(*) FILTER (WHERE CAST(avgPrice AS DOUBLE)/1e6 > 0.97) AS near_par,
count(*) FILTER (WHERE CAST(avgPrice AS DOUBLE)/1e6 > 0.97 AND CAST(realizedPnl AS DOUBLE) > 0) AS won,
count(*) FILTER (WHERE CAST(avgPrice AS DOUBLE)/1e6 > 0.97 AND CAST(realizedPnl AS DOUBLE) < 0) AS lost,
count(*) FILTER (WHERE CAST(avgPrice AS DOUBLE)/1e6 > 0.97 AND CAST(realizedPnl AS DOUBLE) = 0) AS flat,
sum(CAST(realizedPnl AS DOUBLE)) FILTER (WHERE CAST(avgPrice AS DOUBLE)/1e6 > 0.97) / 1e6 AS near_par_pnl_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12';
-- 4022, 3857, 69, 96, 542780.65
3,857 won, 69 lost and 96 closed flat, for $542,781 of realized profit. Of the 3,926 that actually resolved one way or the other, 98.24% went the wallet's way.
The win rate on its own oversells it. The shape underneath is why.
SELECT
sum(CAST(realizedPnl AS DOUBLE)) FILTER (WHERE CAST(realizedPnl AS DOUBLE) > 0) / 1e6 AS winning_dollars,
sum(CAST(realizedPnl AS DOUBLE)) FILTER (WHERE CAST(realizedPnl AS DOUBLE) < 0) / 1e6 AS losing_dollars,
avg(CAST(realizedPnl AS DOUBLE)) FILTER (WHERE CAST(realizedPnl AS DOUBLE) > 0) / 1e6 AS mean_win,
avg(CAST(realizedPnl AS DOUBLE)) FILTER (WHERE CAST(realizedPnl AS DOUBLE) < 0) / 1e6 AS mean_loss
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12'
AND CAST(avgPrice AS DOUBLE) / 1e6 > 0.97;
-- 652402.55, -109621.90, 169.15, -1588.72
$169 on the average win, $1,589 on the average loss. Each loss undoes roughly 9 wins, which is why 98.24% is the minimum viable accuracy here rather than a comfortable one. Half a cent of edge does not survive being wrong very often.
Set that $542,781 against what it was earned on and the edge is thinner still.
WITH buys AS (
SELECT CASE WHEN lower(maker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12' AND makerAssetId = '0'
THEN CAST(makerAmountFilled AS DOUBLE)
WHEN lower(taker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12' AND takerAssetId = '0'
THEN CAST(takerAmountFilled AS DOUBLE) END AS cash,
CASE WHEN lower(maker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12' AND makerAssetId = '0'
THEN CAST(makerAmountFilled AS DOUBLE) / NULLIF(CAST(takerAmountFilled AS DOUBLE), 0)
WHEN lower(taker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12' AND takerAssetId = '0'
THEN CAST(takerAmountFilled AS DOUBLE) / NULLIF(CAST(makerAmountFilled AS DOUBLE), 0) END AS price
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12'
OR lower(taker) = '0x9b979a065641e8cfde3022a30ed2d9415cf55e12'
)
SELECT count(*) FILTER (WHERE cash IS NOT NULL) AS buys,
avg(price) FILTER (WHERE cash IS NOT NULL) AS avg_buy_price,
sum(cash) FILTER (WHERE price > 0.97) / 1e6 AS buy_volume_above_097
FROM buys;
-- 47754, 0.9677, 69799307.44
47,754 buys at an average price of $0.97, and $69.8M of them above $0.97. That $542,781 of profit on $69.8M of near-par buying is an ROI of 0.78%. Take a guaranteed half-cent thousands of times, and accept that being wrong once costs you 9 of them.
The losses are not random either. The UMA Gap article sorts them, and they cluster where an outcome looked settled but the resolution criteria had not actually been met. Method-of-victory markets are the clearest case, where the fighter wins but the judges' cards come in rather than the stoppage everyone was pricing, and the win-by-KO token that had been quoting $0.99 settles at zero. Draw tokens are the other, where a stoppage-time goal turns 1-1 into 2-1 and a token sitting at $0.999 goes to nothing.
That article is also worth reading for the single-market walkthrough, a Monday night NFL game where the final whistle went at 04:30 UTC and this wallet bought $311K of the winning token 58 minutes later, from 2 different sellers in the same Polygon block, with the market's listed close still a week away.
The Day 3 Wallet, and Its Second Address
Some Polymarket accounts trade from two addresses at once, and order_filled records both. The rule from earlier only finds one of them. Day 3's wallet is the clearest example of it in the dataset.
That wallet is 0xCF3b13042CB6cEb928722b2AA5d458323B6c5107, a different account from the one above.
Here is what it was doing. In Polymarket's 2024 US presidential book, the candidate-YES tokens have to sum to $1.00 by identity, and for 21 days they summed to more. The wallet split USDC into one token of every candidate, then sold Trump-YES and Harris-YES simultaneously into the book. Day 3 counts 762 splits totalling $15,275,535 and 20,214 simultaneous sell events, with the 2 legs summing above $1.00 on 77.9% of them, averaging 1.00078 and peaking at 1.01914. It never took a view on who would win.
In wallet.parquet that address appears as a signer, with a proxy beneath it.
SELECT id, signer, type
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/wallet.parquet'
WHERE lower(signer) = '0xcf3b13042cb6ceb928722b2aa5d458323b6c5107';
-- 0xfe965f043613a702695f5d547c304a7c265ce962 | 0xCF3b13042CB6cEb928722b2AA5d458323B6c5107 | proxy
The rule says the id is the address that trades. On this account that is half the story. The proxy carries 103 fills.
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0xfe965f043613a702695f5d547c304a7c265ce962'
OR lower(taker) = '0xfe965f043613a702695f5d547c304a7c265ce962';
-- 103
The signer, which the rule tells you to resolve away, carries the rest of them itself.
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0xcf3b13042cb6ceb928722b2aa5d458323b6c5107'
OR lower(taker) = '0xcf3b13042cb6ceb928722b2aa5d458323b6c5107';
-- 36637
Together they are the account, and together they are exactly the 36,740 fills Day 3 reports.
SELECT count(*) AS fills,
min(to_timestamp(CAST(timestamp AS BIGINT))) AS first_fill,
max(to_timestamp(CAST(timestamp AS BIGINT))) AS last_fill
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) IN ('0xcf3b13042cb6ceb928722b2aa5d458323b6c5107',
'0xfe965f043613a702695f5d547c304a7c265ce962')
OR lower(taker) IN ('0xcf3b13042cb6ceb928722b2aa5d458323b6c5107',
'0xfe965f043613a702695f5d547c304a7c265ce962');
-- 36740, 2024-10-26 19:14:49, 2024-11-16 05:11:30
Every one of them falls inside the 26 October to 16 November 2024 window Day 3 describes. So the rule from earlier needs a second half. Resolve the signer to its proxy, then query both, not one or the other. An address sitting in the signer column can still be a trading address in its own right, and nothing tells you when it is. The narrow query returns rows rather than an error, so a 99.7% miss looks exactly like a correct answer.
The money reconciles on the same union.
SELECT sum(CASE WHEN makerAssetId = '0' THEN CAST(makerAmountFilled AS HUGEINT)
ELSE CAST(takerAmountFilled AS HUGEINT) END) / 1e6 AS volume_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) IN ('0xcf3b13042cb6ceb928722b2aa5d458323b6c5107',
'0xfe965f043613a702695f5d547c304a7c265ce962')
OR lower(taker) IN ('0xcf3b13042cb6ceb928722b2aa5d458323b6c5107',
'0xfe965f043613a702695f5d547c304a7c265ce962');
-- 23239626.855936
SELECT sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS realized_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) IN ('0xcf3b13042cb6ceb928722b2aa5d458323b6c5107',
'0xfe965f043613a702695f5d547c304a7c265ce962');
-- 7182125.580323
Day 3 reports $23.24M of volume and $7,182,126 of realized PnL on the election basket arb. Both land.
Where They Sit on the Leaderboard
The 2 wallets are 7 places apart on realized PnL, at 26 and 33.
WITH agg AS (
SELECT lower(user) AS u, sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS pnl
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
GROUP BY 1
), ranked AS (
SELECT u, pnl, row_number() OVER (ORDER BY pnl DESC) AS rank FROM agg
)
SELECT u, round(pnl, 2) AS realized_usd, rank
FROM ranked
WHERE u IN ('0x9b979a065641e8cfde3022a30ed2d9415cf55e12',
'0xcf3b13042cb6ceb928722b2aa5d458323b6c5107')
ORDER BY rank;
-- 0x9b979a065641e8cfde3022a30ed2d9415cf55e12 | 8049418.94 | 26
-- 0xcf3b13042cb6ceb928722b2aa5d458323b6c5107 | 7182554.41 | 33
Both ranked across all 2,684,676 addresses that hold a position. Worth noticing while you are here, the leaderboard ranks each address on its own row, and the Day 3 proxy sits at 2,599,446 on a realized PnL of minus $428.83. That is why the $7,182,126 the article quotes, which is the signer and the proxy together, comes in just under the signer's own $7,182,554.
Example 4: 15-Minute BTC Market-Maker
This wallet market-makes Polymarket's 15-minute crypto binaries, sitting on both sides of the book and collecting the spread. It does not depend on Bitcoin going up or down. In the post's words, "the edge isn't predictive, it's compensation for being the resting liquidity that takes the other side of impatience."
Wallet 0x63CE342161250D705dC0b16dF89036C8E5F9Ba9a, a Safe rather than a proxy.
The article reports $2,382,793 realized, 7,638,691 fills, $128.39M volume, 32,021 markets, 111 days, and a 49.9% win rate by construction.
| Metric | Value |
|---|---|
| Total positions | 67,900 |
| Positions with non-zero PnL | 66,596 |
| Wins | 33,222 |
| Losses | 33,374 |
| Win rate by position | 49.9% |
| Net realized PnL | $2,382,793 |
| Average net PnL per closed position | $35.79 |
| Total redemptions claimed | 84,103 events, $78,984,643 |
Closed-out books from the original post. Wins and losses sum to the 66,596 positions that resolved either way, which is where the 49.9% comes from.
Step 1: Pull the fills
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
WHERE lower(maker) = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a'
OR lower(taker) = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a';
-- 7638691
Step 2: Pull the positions
SELECT count(*), sum(CAST(realizedPnl AS DOUBLE)) / 1e6 AS realized_usd
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/user_position.parquet'
WHERE lower(user) = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a';
-- 67900, 2382793.28
Both match the post, 7,638,691 fills and $2,382,793 realized.
| Example | PnL | Fills | What |
|---|---|---|---|
1. 0x2005…75easigner 0x5d4f…2edc | ~$24M | 2,698,796 | YES+NO merge. No prediction. |
2. 0xbddf…c684signer 0xdb15…497b | $23.6M | 116,086 | NBA model. 95.4% is a closed-bet filter. |
3a. 0x9b97…5e12signer 0x8Dcd…4EAE | $8.05M | 61,095 | Settlement sweep and basket arb, one address. |
3b. 0xCF3b…5107proxy 0xfe96…e962 | $7.18M | 36,740 | Election basket arb. Signer trades directly. |
4. 0x63CE…Ba9a | $2,382,793 | 7,638,691 | Short-dated MM. 49.9% WR. |
4 write-ups, 5 wallets. Stats from the original posts. Query the proxy or Safe, and the signer too, as Example 3 shows.
Indexing Polymarket with Envio
The snapshot stops at its cutoff block. For anything after that you would need to run an indexer, though for Polymarket you do not have to write one, because we have open sourced both of ours.
HyperIndex points at a set of contracts, runs a handler for each event you care about, and turns that into a Postgres database with a GraphQL API in front of it. It ingests through HyperSync, our data layer for EVM chains, rather than walking the chain over RPC. That is how the v1 indexer backfilled Polymarket's full history on Polygon, over 4 billion events from block 3,764,531, in 6 days. Polygon is one of 80+ chains with native HyperSync coverage.
Which of the two you want comes down to whether you need history or head.
| Repo | Covers | When to use it |
|---|---|---|
| v2 indexer | CTF Exchange V2, pUSD, collateral adapters, rewards | You want live v2 markets at head |
| v1 indexer | v1 CLOB and FPMM, the 8 original subgraphs merged into one, plus the same v2 contracts | You want v1 as well as v2, or to rebuild the snapshot from chain and verify it |
The 2 open Polymarket indexers. Both are HyperIndex projects on Polygon.
For history up to the cutoff you do not need to run either one. That is what the snapshot above already is.
Before You Start
The v2 indexer needs the standard HyperIndex toolchain, listed under prerequisites in our docs, plus a free Envio API token. Querying the snapshot needs none of that, only DuckDB.
Run the Live Polymarket Indexer
The open v2 indexer is a HyperIndex v3 project. Handlers live in src/handlers/CTFExchangeV2.ts. Contracts and start_block are in config.yaml. See the HyperIndex overview and CLI commands for current setup.
Step 1: Clone the repo and copy the env file
git clone https://github.com/enviodev/polymarket-v2-indexer
cd polymarket-v2-indexer
cp .env.example .env
Step 2: Add your API token
Put your Envio API token in .env as ENVIO_API_TOKEN. Create one if you do not have it yet.
Step 3: Install and start the indexer
This installs dependencies, generates types, and starts syncing. Docker needs to be running.
pnpm install
pnpm codegen
pnpm dev
Step 4: Open the GraphQL playground
It runs at http://localhost:8080 with the password testing. You now have a queryable endpoint that fills as the indexer syncs.
Step 5: Deploy it to Envio Cloud
When you want it running somewhere other than your laptop, deploy the same repo to Envio Cloud, which serves it from a production-ready GraphQL endpoint. Environment variables, including ENVIO_API_TOKEN, are set in the Envio dashboard.
The playground is the query layer. The part that decides what gets written is the handler. Here is the one that records every CLOB fill, using the v3 indexer.onEvent API.
import { indexer } from "envio";
indexer.onEvent(
{ contract: "CTFExchangeV2", event: "OrderFilled" },
async ({ event, context }) => {
const stats = await getOrInitStats(context, event.srcAddress);
const marketId = await ensureMarket(context, event.params.tokenId);
context.OrderFill.set({
id: eventId(event),
orderHash: event.params.orderHash,
maker: event.params.maker,
taker: event.params.taker,
side: Number(event.params.side),
tokenId: event.params.tokenId,
market_id: marketId,
makerAmountFilled: event.params.makerAmountFilled,
takerAmountFilled: event.params.takerAmountFilled,
fee: event.params.fee,
builder: event.params.builder,
metadata: event.params.metadata,
exchange: event.srcAddress,
timestamp: event.block.timestamp,
blockNumber: event.block.number,
transactionHash: event.transaction.hash,
txFrom: event.transaction.from ?? "",
});
const hasBuilder = event.params.builder !== ZERO_BYTES32;
const collateralAmount =
Number(event.params.side) === 0
? event.params.makerAmountFilled
: event.params.takerAmountFilled;
context.ExchangeStats.set({
...stats,
totalOrdersFilled: stats.totalOrdersFilled + 1n,
totalVolume: stats.totalVolume + collateralAmount,
totalFees: stats.totalFees + event.params.fee,
totalBuilderFills: stats.totalBuilderFills + (hasBuilder ? 1n : 0n),
});
},
);
2 things happen per fill. The OrderFill row is the raw event, and ExchangeStats is a running aggregate updated in the same handler, so totals are available without a scan at query time.
8 Subgraphs, One Indexer
Polymarket's v1 data was originally served by 8 separate subgraphs on The Graph, several of them indexing the same contracts. We rebuilt all 8 as a single HyperIndex project, the v1 indexer, and the snapshot you queried above is its output. Learn more in our case study.
The repo's README lists all 8 and what each one tracked, so you can see how the domains map onto one schema.
One thing to be clear about if you are debugging a stalled pipeline. The v1 exchange contracts stopped producing fills at block 86,126,998, so a v1 subgraph-shaped source has had no new orderbook data since then wherever it is hosted, because there is none left to index.
If you are weighing up the same move for your own subgraphs, the repo is the reference. If you have any questions, come and ask us in Discord.
Resources
- Live v2 indexer
- Open v1 indexer
- Public v1 snapshot
- How Envio Indexed 4 Billion Polymarket Events
- The Largest Public Polymarket Dataset
- How to Track Polymarket Trades Using Envio HyperSync
- HyperIndex overview
- Event handlers
- Realized-PnL distribution
- Day 1. Buying both sides
- Day 2. 95% win rate
- Day 2 correction
- Day 3. Election basket
- UMA Gap
- 15-minute BTC
Frequently Asked Questions
What is in the public Polymarket v1 snapshot?
It is the full onchain lifecycle of Polymarket v1 on Polygon, about 2.74 billion records across roughly 25 entity tables and 127 GB of Zstd Parquet. That includes 1,172,658,611 CLOB fills in order_filled, 303,955,230 rows in user_position, 7,362,437 rows in wallet, plus splits, merges, redemptions, resolutions, orderbook state and FPMM-era AMM activity going back to September 2020. It is published on Hugging Face under CC-BY-4.0.
Do I need to download 127 GB to query the Polymarket dataset?
No. The files are Hive-partitioned Parquet served over HTTPS, and DuckDB reads only the byte ranges a query actually needs. A count(*) over all 1.17 billion fills reads the Parquet footers and returns in seconds. Partitioning by year means a query scoped with year=2025 never touches the other years at all.
Why do my Polymarket address filters return zero rows?
Because addresses in the snapshot are stored EIP-55 checksummed rather than lowercase, and the casing is not consistent between tables. A filter like WHERE maker = '0xdb15…' in lowercase matches nothing and returns an empty result with no error. Wrap the column instead, WHERE lower(maker) = '0xdb15…', and compare against a lowercase literal.
Which Polymarket address actually holds the trades, the wallet or the signer?
Usually the proxy or Safe contract, but not always only it. Most Polymarket accounts are a proxy or Gnosis Safe that executes the trades, controlled by a signer that never appears in order_filled. If an address from a post returns no fills, look it up in wallet.parquet with WHERE lower(signer) = '0x…' and the id column is the trading address. Some signers trade directly as well as through their proxy, so the safe habit is to query both addresses with IN, not to pick one. On the Example 3 wallet the proxy alone returns 103 fills and the two together return 36,740.
When does the Polymarket v1 snapshot stop, and how do I get data after that?
SNAPSHOT.json puts the event-log cutoff at Polygon block 85,948,287, 24 April 2026. The snapshot is frozen there and will not advance. For live v2 markets, run our open v2 indexer, which covers CTF Exchange V2, pUSD, the collateral adapters and rewards, either locally or deployed to Envio Cloud. The v1 exchange kept trading to block 86,126,998, so if you need that tail run the v1 indexer over the range.
I was using the Polymarket subgraphs. Where is that data now?
Polymarket v1 was indexed by 8 separate subgraphs on The Graph, covering the orderbook, PnL, wallets, activity, open interest, FPMM, fees and the sports oracle. All 8 are consolidated into the open v1 indexer, and the public snapshot is that indexer's output, so the same data is queryable with DuckDB and no endpoint at all. The repo's README lists all 8 and what each one tracked. Note that the v1 exchange contracts stopped producing fills at block 86,126,998, so no v1 source of any kind has new orderbook data after that block. For markets after it, run the v2 indexer, or the v1 indexer, which covers both generations.
What is a CLOB fill, and how is it different from an FPMM trade?
A CLOB fill is a match on Polymarket's central limit order book, where a maker's resting order is filled by a taker. That is how essentially all Polymarket trading works today, and order_filled is the table holding all 1,172,658,611 of them. FPMM stands for Fixed Product Market Maker, the automated market maker Polymarket ran before the order book existed. Those trades priced against a liquidity pool rather than against another trader, and they live in fpmm_transaction and the related funding tables. Both are in the snapshot, so a query over order_filled alone covers the order book but not the earlier AMM era.
Can I use this Polymarket dataset commercially?
Yes. It is published under CC BY 4.0, which permits any use including commercial products, redistribution and derivative work, provided you meet the licence terms. In practice that means crediting Envio, linking the licence, indicating any changes you made, and not adding restrictions of your own. There is no fee, no signup and no separate agreement to accept.
Can I rebuild the Polymarket snapshot myself instead of trusting it?
Yes. The v1 indexer that produced it is open source, so you can run it against Polygon and diff your output against the published Parquet. That is the point of shipping both the dataset and the indexer that made it.
Do I need an Envio API token to query the snapshot?
No. Reading the Hugging Face Parquet with DuckDB uses no Envio infrastructure and needs no token. A token is only required when Envio is the data provider, which means indexing through HyperSync, as both Polymarket indexers do. Tokens are free to create, and on Envio Cloud you set it as an environment variable in the dashboard.
Build With Envio
Envio is a real-time multichain blockchain indexer that turns onchain events into a queryable GraphQL API. Supports any EVM chain, plus Solana and Fuel. Use Envio Cloud or self-host. If you're building onchain, come talk to us about your data needs.
Website | X | Discord | Telegram | GitHub | YouTube | Reddit
Jordyn Laurier

