Inserts order_fills, orders_matched, fees, resolutions; upserts market_orderbooks, trader_accounts, global_stats (absolute values, works on Postgres and ClickHouse).
Output
sf.substreams.sink.database.v1.DatabaseChanges

Package ref
polymarket-orderbook-substreams@v1.0.0Run package
CLI
Run db_out from the command line.
substreams run polymarket-orderbook-substreams@v1.0.0 db_out -e polygonsubstreams auth or directly on thegraph.market (see docs).README
Real-time orderbook analytics for Polymarket prediction markets on Polygon
Polymarket orderbook flow on Polygon: every fill, match, fee and resolution from CLOB v1, CLOB v2 and Protocol V2 (ExchangeV3). Includes per-market, per-trader and global aggregates, with schemas for PostgreSQL and ClickHouse.
| # | Change | Why |
|---|---|---|
| 1 | ExchangeV3 0xe3333700cA9d93003F00f0F71f8515005F6c00Aa (Protocol V2) is indexed | Its fills were missing (705 in a 500-block sample) |
| 2 | Single pass: map_events replaces the 8 per-exchange maps | One block decode instead of eight |
| 3 | blockFilter (ethcommon:index_events) plus lazy block decode (&BlockLazyView) and constant address bytes | Blocks with no Polymarket logs are skipped; no Hex::decode per log |
| 4 | FeeCharged (v1 and v2/V3) → fees table. Resolutions (CTF ConditionResolution, Protocol V2 ConditionResolved) → resolutions table | New data |
| 5 | OrdersMatched → orders_matched table. v1 matches are now decoded | The v0.x v1 ABI had no indexed fields, so v1 OrdersMatched never matched |
| 6 | Aggregates are cumulative (add stores) and written with upsert_row | v0.x wrote each fill's own values with update_row onto rows that were never created |
| 7 | block_timestamp on every event table | Time-range queries |
| 8 | Correct side and price: v1 side comes from makerAssetId == 0; price = collateral ÷ shares, fee-aware | v0.x used a digit-parity heuristic and maker ÷ taker amounts (inverted for sells) |
| 9 | ClickHouse schema with PARTITION BY toYYYYMM(block_timestamp) / ORDER BY (token_id, block_timestamp, id), and ReplacingMergeTree for aggregates | Query performance |
| 10 | Deterministic ordering (BTreeMap) and unit tests | Reproducible output |
map_ctf_exchange_order_filled, map_neg_risk_exchange_order_filled, map_ctf_exchange_orders_matched, map_neg_risk_exchange_orders_matched, and the four *_v2_* maps are replaced by map_events (OrderbookEvents: fills, matches, fees, resolutions).map_all_order_fills is kept, with the same OrderFilledEvents type, now fed from map_events.store_markets, store_traders and store_global_stats are replaced by store_totals, store_last_price, store_first_trade and store_last_trade.map_market_orderbooks, map_trader_accounts, map_global_orderbook_stats and db_out keep their names.order_fills.id is now tx_hash-log_index.market_orderbooks.id is the token id, and condition_id is dropped.maker), so the exchange address is no longer counted as a trader.OrderFilledEvent fields: exchange, log_index, shares, collateral.substreams-sink-sql setup) and resync from the start block.substreams run polymarket-orderbook-substreams@v1.0.0 map_events -e polygon.streamingfast.io:443 -s -1000 -t 0
substreams-sink-sql setup "psql://user:pass@localhost:5432/ob?sslmode=disable" polymarket-orderbook-substreams@v1.0.0
substreams-sink-sql run "psql://user:pass@localhost:5432/ob?sslmode=disable" polymarket-orderbook-substreams@v1.0.0
# ClickHouse: apply clickhouse-schema.sql, then use a clickhouse:// DSN
Block ─(blockFilter, lazy)─ map_events ─┬─ map_all_order_fills
├─ store_totals ─────┬─ map_market_orderbooks ─┐
├─ store_last_price ─┘ │
├─ store_first/last_trade ─ map_trader_accounts ┤
├─ map_global_orderbook_stats ──────────────────┤
└───────────────────────────────────────────── db_out
Trading.sol)maker is the owner of the order that was filled, and side is that order's side. The taker order's own fill has the exchange as taker.taking − fee. v2/V3: the fee is collateral, so a BUY pays makerAmountFilled + fee and a SELL receives takerAmountFilled − fee. shares / collateral apply this.From https://docs.polymarket.com/resources/contracts. The v1 exchanges come from github.com/Polymarket/ctf-exchange. All of them are in src/decode.rs.
| Contract | Address |
|---|---|
| CTF Exchange v1 / Neg Risk v1 | 0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e / 0xC5d563A36AE78145C45a50134d48A1215220f80a |
| CTF Exchange v2 / Neg Risk v2 | 0xE111180000d2663C0091e4f400237545B87B996B / 0xe2222d279d744050d28e00520010520000310F59 |
| ExchangeV3 (Protocol V2) | 0xe3333700cA9d93003F00f0F71f8515005F6c00Aa |
| Conditional Tokens | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 |
| BinaryModule / NegRiskModule | 0x1000008dD9001B968442c1000017eaE6E0dA00Ba / 0x200000900045e3B6259600682756002200028933 |
substreams build # CLI >= 1.25, buffa codegen with lazy_views=true (buf.gen.yaml)
cargo test --lib
MIT
Modules
Inserts order_fills, orders_matched, fees, resolutions; upserts market_orderbooks, trader_accounts, global_stats (absolute values, works on Postgres and ClickHouse).
Output
sf.substreams.sink.database.v1.DatabaseChanges
Fills only, in the v0.x OrderFilledEvents shape (now including ExchangeV3 and the new fields).
Inputs
Single pass (lazy block decode) over the five Polymarket exchanges: OrderFilled, OrdersMatched, FeeCharged; plus CTF ConditionResolution and Protocol V2 ConditionResolved. Replaces the eight per-exchange maps of v0.x.
Inputs
Store value
int64
Update policy
min
Inputs
Store value
string
Update policy
set
Inputs
Store value
int64
Update policy
max
Inputs
Counters: m:{token}:trades|buys|sells|volume|fees, t:{owner}:..., g:...|fee_revenue
Store value
bigint
Update policy
add
Inputs
Output
sf.substreams.ethereum.v1.Events
`index_events` sets the following keys on the block: * Event signatures evt_sig:0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef (signature in hex, prefixed by 0x) * Event address evt_addr:0x0123456789abcdef0123456789abcdef01234567 (address in hex, prefixed by 0x)
Output
sf.substreams.index.v1.Keys
Inputs