> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

<img src="/blog-assets/polymarket-onchain-data.png" alt="How to Get Polymarket Trade Data" title="How to Get Polymarket Trade Data" width="100%"/>

<!--truncate-->

:::note TL;DR
- 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](https://github.com/enviodev/polymarket-v2-indexer) for live v2 markets, or the [v1 indexer](https://github.com/enviodev/polymarket-indexer) for both generations, and run it locally or on [Envio Cloud](/docs/HyperIndex/hosted-service). 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](https://x.com/jonjonclark)'s [realized-PnL distribution](https://x.com/jonjonclark/status/2047685184934281714) and his [Top Hundred series](https://x.com/jonjonclark/status/2049067586046816561).

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](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1) 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](https://duckdb.org/docs/installation/) 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](https://github.com/enviodev/polymarket-v2-indexer) are the last section.

## How to Query the Public Snapshot with DuckDB

The [public v1 snapshot](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1) is Polygon data indexed with HyperIndex and released under [CC BY 4.0](https://creativecommons.org/licenses/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.

<img src="/blog-assets/polymarket-hf-snapshot-json.png" alt="SNAPSHOT.json on Hugging Face for the Polymarket v1 snapshot" title="SNAPSHOT.json on Hugging Face" width="100%"/>

*`SNAPSHOT.json` on Hugging Face. Cutoff for the v1 snapshot.*

<img src="/blog-assets/polymarket-hf-whats-inside.png" alt="Hugging Face dataset card What's inside table listing order_filled, user_position, and wallet row counts" title="Hugging Face What's inside table" width="100%"/>

*Hugging Face dataset card. What is inside the v1 snapshot.*

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
      <th>How</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CLOB fills (<code>order_filled</code>)</td>
      <td>1,172,658,611</td>
      <td><code>count(*)</code></td>
    </tr>
    <tr>
      <td>Distinct CLOB makers</td>
      <td>2,630,334</td>
      <td><code>count(DISTINCT maker)</code> on <code>order_filled</code></td>
    </tr>
    <tr>
      <td>Lifetime CLOB volume</td>
      <td>$59.899B</td>
      <td>cash-leg sum, USDC 6 decimals</td>
    </tr>
    <tr>
      <td><code>user_position</code> rows</td>
      <td>303,955,230</td>
      <td><code>count(*)</code></td>
    </tr>
    <tr>
      <td>Distinct <code>user_position</code> users</td>
      <td>2,684,676</td>
      <td><code>count(DISTINCT user)</code></td>
    </tr>
    <tr>
      <td><code>wallet.parquet</code> rows</td>
      <td>7,362,437</td>
      <td>proxy + Safe rows. Not the trader count.</td>
    </tr>
  </tbody>
</table>

*Figures from the Hugging Face dataset card. The queries below are how you read them.*

Lifetime CLOB volume:

```sql
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.

[See original post on X](https://x.com/jonjonclark/status/2047685184934281714)

The [dataset card](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1) lists 2.74 billion records across the entity tables, about 127 GB.

### Step 1: Install DuckDB

Grab it from the [DuckDB installation docs](https://duckdb.org/docs/installation/) 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.

```bash
duckdb -c "
SELECT count(*)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet'
"
-- 1172658611
```

Distinct CLOB makers:

```sql
SELECT count(DISTINCT maker)
FROM 'hf://datasets/moose-code/polymarket-onchain-v1/order_filled/**/*.parquet';
-- 2630334
```

Position-holders:

```sql
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](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1).

```sql
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
```

:::note Addresses are checksummed, not lowercase

Addresses in the snapshot are stored EIP-55 checksummed. `order_filled.maker` looks like [`0x63CE342161250D705dC0b16dF89036C8E5F9Ba9a`](https://polygonscan.com/address/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.

```sql
-- 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.

```sql
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`](https://polygonscan.com/address/0x2005d16a84ceefa912d4e380cd32e7ff827875ea), controlled by [`0x5d4fd194c4181ad61b1b5cb72dab8f9c4f9a2edc`](https://polygonscan.com/address/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.

<img src="/blog-assets/polymarket-example-1.jpg" alt="Maker BUY price distribution from the Day 1 post, bids across every price from 1 cent to 99 cents" title="Example 1 maker BUY price distribution" width="100%"/>

*Maker BUY price distribution from the original post. Example 1.*

#### Step 1: Pull the fills

```sql
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

```sql
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/`.

[See original post on X](https://x.com/jonjonclark/status/2049067586046816561)

### 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`](https://polygonscan.com/address/0xdb15373c33adb64de90f23f90c0d8b86ef65497b). In the snapshot that address is a signer with no fills of its own, and the figures below land on its proxy, [`0xbddf61af533ff524d27154e589d2d7a81510c684`](https://polygonscan.com/address/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.


<img src="/blog-assets/polymarket-example-2.jpg" alt="Activity dashboard from the Day 2 post showing cumulative PnL, daily volume, and daily fills for the NBA model wallet" title="Example 2 activity dashboard" width="100%"/>

*Activity dashboard from the original post. Example 2.*

#### Step 1: Pull the fills

```sql
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

```sql
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.

[See original post on X](https://x.com/jonjonclark/status/2049450963908415800)

[Same-day correction](https://x.com/jonjonclark/status/2049492239940739477)

### Example 3: 2 Wallets, and an Account With 2 Addresses

This example covers 2 wallets. The one from the [UMA Gap article](https://x.com/jonjonclark/status/2052061246963220846), which runs a settlement sweep and a basket arb from a single address, and the one from the [Day 3 post](https://x.com/jonjonclark/status/2049831392310133035), which runs the basket arb and is where querying the snapshot gets interesting.

#### The UMA Gap Wallet

[`0x9b979a065641e8cfde3022a30ed2d9415cf55e12`](https://polygonscan.com/address/0x9b979a065641e8cfde3022a30ed2d9415cf55e12). In `wallet.parquet` it is a proxy, controlled by signer [`0x8Dcd34aeF17AB9f121d5198E80d8d683a2274EAE`](https://polygonscan.com/address/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.

<img src="/blog-assets/polymarket-example-3-buy-distribution.png" alt="Every buy by the UMA Gap wallet plotted by price, on a log scale, with almost all of the volume stacked in the bars above $0.97" title="Example 3 buy price distribution" width="100%"/>

*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.

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr>
      <th>When the fill landed</th>
      <th>Buys at &gt;$0.97</th>
      <th>Volume</th>
    </tr>
  </thead>
  <tbody>
    <tr><td><strong>After <code>market_end</code></strong>, event already over</td><td><strong>26,970</strong></td><td><strong>$37.6M</strong></td></tr>
    <tr><td>Within 1h before end</td><td>562</td><td>$0.59M</td></tr>
    <tr><td>1 to 6h before end</td><td>2,843</td><td>$4.13M</td></tr>
    <tr><td>6 to 24h before end</td><td>1,077</td><td>$1.94M</td></tr>
    <tr><td>1 to 7d before end</td><td>10,457</td><td>$15.18M</td></tr>
    <tr><td>&gt;7d before end</td><td>2,025</td><td>$10.39M</td></tr>
  </tbody>
</table>

*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.*

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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`](https://polygonscan.com/address/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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

[See Day 3 on X](https://x.com/jonjonclark/status/2049831392310133035)

[UMA Gap](https://x.com/jonjonclark/status/2052061246963220846)

### 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`](https://polygonscan.com/address/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.

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Total positions</td><td>67,900</td></tr>
    <tr><td>Positions with non-zero PnL</td><td>66,596</td></tr>
    <tr><td>Wins</td><td>33,222</td></tr>
    <tr><td>Losses</td><td>33,374</td></tr>
    <tr><td>Win rate by position</td><td><strong>49.9%</strong></td></tr>
    <tr><td>Net realized PnL</td><td><strong>$2,382,793</strong></td></tr>
    <tr><td>Average net PnL per closed position</td><td><strong>$35.79</strong></td></tr>
    <tr><td>Total redemptions claimed</td><td>84,103 events, $78,984,643</td></tr>
  </tbody>
</table>

*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

```sql
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

```sql
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.

[See original post on X](https://x.com/jonjonclark/status/2051664712606073157)

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr>
      <th>Example</th>
      <th>PnL</th>
      <th>Fills</th>
      <th>What</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1. <a href="https://polygonscan.com/address/0x2005d16a84ceefa912d4e380cd32e7ff827875ea"><code>0x2005…75ea</code></a><br/>signer <a href="https://polygonscan.com/address/0x5d4fd194c4181ad61b1b5cb72dab8f9c4f9a2edc"><code>0x5d4f…2edc</code></a></td>
      <td>~$24M</td>
      <td>2,698,796</td>
      <td>YES+NO merge. No prediction.</td>
    </tr>
    <tr>
      <td>2. <a href="https://polygonscan.com/address/0xbddf61af533ff524d27154e589d2d7a81510c684"><code>0xbddf…c684</code></a><br/>signer <a href="https://polygonscan.com/address/0xdb15373c33adb64de90f23f90c0d8b86ef65497b"><code>0xdb15…497b</code></a></td>
      <td>$23.6M</td>
      <td>116,086</td>
      <td>NBA model. 95.4% is a closed-bet filter.</td>
    </tr>
    <tr>
      <td>3a. <a href="https://polygonscan.com/address/0x9b979a065641e8cfde3022a30ed2d9415cf55e12"><code>0x9b97…5e12</code></a><br/>signer <a href="https://polygonscan.com/address/0x8Dcd34aeF17AB9f121d5198E80d8d683a2274EAE"><code>0x8Dcd…4EAE</code></a></td>
      <td>$8.05M</td>
      <td>61,095</td>
      <td>Settlement sweep and basket arb, one address.</td>
    </tr>
    <tr>
      <td>3b. <a href="https://polygonscan.com/address/0xCF3b13042CB6cEb928722b2AA5d458323B6c5107"><code>0xCF3b…5107</code></a><br/>proxy <a href="https://polygonscan.com/address/0xfe965f043613a702695f5d547c304a7c265ce962"><code>0xfe96…e962</code></a></td>
      <td>$7.18M</td>
      <td>36,740</td>
      <td>Election basket arb. Signer trades directly.</td>
    </tr>
    <tr>
      <td>4. <a href="https://polygonscan.com/address/0x63CE342161250D705dC0b16dF89036C8E5F9Ba9a"><code>0x63CE…Ba9a</code></a></td>
      <td>$2,382,793</td>
      <td>7,638,691</td>
      <td>Short-dated MM. 49.9% WR.</td>
    </tr>
  </tbody>
</table>

*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](/docs/HyperSync/overview), 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](/blog/polymarket-hyperindex-case-study). Polygon is one of <HyperSyncChainCount /> chains with native HyperSync coverage.

Which of the two you want comes down to whether you need history or head.

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr>
      <th>Repo</th>
      <th>Covers</th>
      <th>When to use it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/enviodev/polymarket-v2-indexer">v2 indexer</a></td>
      <td>CTF Exchange V2, pUSD, collateral adapters, rewards</td>
      <td>You want live v2 markets at head</td>
    </tr>
    <tr>
      <td><a href="https://github.com/enviodev/polymarket-indexer">v1 indexer</a></td>
      <td>v1 CLOB and FPMM, the 8 original subgraphs merged into one, plus the same v2 contracts</td>
      <td>You want v1 as well as v2, or to rebuild the snapshot from chain and verify it</td>
    </tr>
  </tbody>
</table>

*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](/docs/HyperIndex/quickstart#prerequisites) in our docs, plus a free [Envio API token](https://envio.dev/app/api-tokens). Querying the snapshot needs none of that, only DuckDB.

### Run the Live Polymarket Indexer

The [open v2 indexer](https://github.com/enviodev/polymarket-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](/docs/HyperIndex/overview) and [CLI commands](/docs/HyperIndex/cli-commands) for current setup.

#### Step 1: Clone the repo and copy the env file

```bash
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](https://envio.dev/app/api-tokens) 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.

```bash
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](/docs/HyperIndex/hosted-service), 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.

```typescript title="src/handlers/CTFExchangeV2.ts"
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](https://github.com/enviodev/polymarket-indexer), and the snapshot you queried above is its output. Learn more in our [case study](/blog/polymarket-hyperindex-case-study).

The [repo's README](https://github.com/enviodev/polymarket-indexer) 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](https://discord.gg/envio).

## Resources

- [Live v2 indexer](https://github.com/enviodev/polymarket-v2-indexer)
- [Open v1 indexer](https://github.com/enviodev/polymarket-indexer)
- [Public v1 snapshot](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1)
- [How Envio Indexed 4 Billion Polymarket Events](/blog/polymarket-hyperindex-case-study)
- [The Largest Public Polymarket Dataset](/blog/developer-update-july-2026#the-largest-public-polymarket-dataset-ever-released)
- [How to Track Polymarket Trades Using Envio HyperSync](/blog/track-polymarket-trades-hypersync)
- [HyperIndex overview](/docs/HyperIndex/overview)
- [Event handlers](/docs/HyperIndex/event-handlers)
- [Realized-PnL distribution](https://x.com/jonjonclark/status/2047685184934281714)
- [Day 1. Buying both sides](https://x.com/jonjonclark/status/2049067586046816561)
- [Day 2. 95% win rate](https://x.com/jonjonclark/status/2049450963908415800)
- [Day 2 correction](https://x.com/jonjonclark/status/2049492239940739477)
- [Day 3. Election basket](https://x.com/jonjonclark/status/2049831392310133035)
- [UMA Gap](https://x.com/jonjonclark/status/2052061246963220846)
- [15-minute BTC](https://x.com/jonjonclark/status/2051664712606073157)

## 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](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1) 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](https://github.com/enviodev/polymarket-v2-indexer), which covers CTF Exchange V2, pUSD, the collateral adapters and rewards, either locally or deployed to [Envio Cloud](/docs/HyperIndex/hosted-service). The v1 exchange kept trading to block 86,126,998, so if you need that tail run the [v1 indexer](https://github.com/enviodev/polymarket-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](https://github.com/enviodev/polymarket-indexer), and the [public snapshot](https://huggingface.co/datasets/moose-code/polymarket-onchain-v1) 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](https://github.com/enviodev/polymarket-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](https://creativecommons.org/licenses/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](https://github.com/enviodev/polymarket-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](https://envio.dev/app/api-tokens), 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](/docs/HyperIndex/hosted-service) or self-host. If you're building onchain, come talk to us about your data needs.

[Subscribe to our newsletter](https://envio.beehiiv.com/subscribe?utm_source=envio.beehiiv.com&utm_medium=newsletter&utm_campaign=new-post)

[Website](https://envio.dev/) | [X](https://twitter.com/envio_indexer) | [Discord](https://discord.gg/envio) | [Telegram](https://t.me/+BeS5ihVUFONjNGFk) | [GitHub](https://github.com/enviodev) | [YouTube](https://www.youtube.com/channel/UCR7nZ2yzEtc5SZNM0dhrkhA) | [Reddit](https://www.reddit.com/user/Envio_indexer)
