All packages
polymarket-trades

polymarket-trades

colindickson
v1.2.0/0 downloads/Repository

Package ref

polymarket-trades@v1.2.0

Run package

CLI

Run db_out from the command line.

substreams run polymarket-trades@v1.2.0 db_out -e polygon
Authenticate by running substreams auth or directly on thegraph.market (see docs).

README

Polymarket Trades Substreams Package

A trade tape for Polymarket on Polygon: one Trade per order fill across all three exchanges, with shares, fee-aware collateral and price. It is the base of the order-book-style views (market stats, candles, per-order fills) built on top of it.

Chain data only carries matched orders. Resting limit orders, cancellations and book depth live in Polymarket's off-chain CLOB, so this is a tape of what traded, not a live book.

Sources

ExchangeAddressexchange
CTF Exchange V20xE111180000d2663C0091e4f400237545B87B996Bctf_v2
Neg Risk CTF Exchange V20xe2222d279d744050d28e00520010520000310F59neg_risk_v2
Protocol V2 Exchange (ExchangeV3)0xe3333700cA9d93003F00f0F71f8515005F6c00Aaexchange_v3

Apart from map_orders, which reads settlement calldata, the package decodes no blocks. Its manifest imports the sibling ../polymarket-exchange and ../polymarket-neg-risk-ctf manifests and reads exchange:map_exchange_events and neg_risk_ctf:map_trading_events. Built from the same commit and toolchain as the published packages of the same version, those modules hash the same, so their cached outputs are reused.

Modules

ModuleDescriptionOutput Type
map_tradesOne trade per OrderFilled, in block orderproto:polymarket.trades.v1.Trades
store_token_totalsRunning per-token totals (<token_id>:<field>): trades, buys, sells, volume_collateral, volume_shares, feesstore (add, bigint)
store_last_priceLast traded price per tokenstore (set, string)
map_market_statsOne TokenStats per token traded in the block: running totals, last price, block VWAP and volumeproto:polymarket.trades.v1.MarketStats
map_candles1-minute OHLCV fragment per token from the block's is_volume tradesproto:polymarket.trades.v1.Candles
db_outPostgreSQL rows for substreams sink postgres (schema.sql)proto:sf.substreams.sink.database.v1.DatabaseChanges
db_out_clickhouseClickHouse rows for substreams sink clickhouse (clickhouse-schema.sql)proto:sf.substreams.sink.database.v1.DatabaseChanges
map_order_fillsOne OrderFill per order filled in the block: owner, token, side, and that block's filled shares, collateral, fees and fill countproto:polymarket.trades.v1.OrderFills
map_ordersThe full signed order behind each fill, decoded from matchOrders calldata: full size, limit price, signer, creation time, and this settlement's fillproto:polymarket.trades.v1.ObservedOrders

The stores start at block 84902353, so map_market_stats needs that backfill before its totals are complete.

Candles

map_candles keeps no store, since one key per candle would grow without bound. Each block emits one Candle per token with is_volume trades. It covers the 1-minute bucket that holds the block timestamp (bucket_start, unix seconds), and its prices are fee-aware trade prices. A bucket spans many blocks, so the sink merges the fragments of the same (token_id, bucket_start) like this:

FieldMerge
openfrom the fragment with the lowest (block_number, open_log_index) (ClickHouse argMin)
closefrom the fragment with the highest (block_number, close_log_index) (argMax)
high / lowmax / min (Postgres upsert with GREATEST / LEAST)
volume_shares, volume_collateral, tradessum

Larger intervals (5m, 1h, 1d) are rolled up from the 1-minute candles in the sink, with the same rule.

Order fills

map_order_fills keeps no store either. A store per order would hold one key set for every order ever filled: over blocks 96,590,000–96,600,000 that was 395,523 orders and about 1.77M keys, while 94.5% of orders finished within one block. Each block emits one OrderFill per order filled in it, and the sink keeps the running total per order_hash: shares, collateral, fees and fills summed, first fill block = min block_number, last = max. Chain data has no full order size, so this is what was filled, not what remains.

Observed orders

OrderFilled carries only what was filled. The full signed order is in the input of the operator's settlement call, so map_orders decodes it from the block's calls (successful, non-reverted calls to the three exchanges). Every settlement seen on blocks 96600000 and 96680009 was a top-level call from an operator EOA:

ExchangeFunctionSelectorEIP-712 domain
CTF Exchange V2, Neg Risk CTF Exchange V2matchOrders(bytes32,Order,Order[],uint256,uint256[],uint256,uint256[])0x3c2b4399"Polymarket CTF Exchange", "2"
ExchangeV3matchOrders(Order,Order[],uint256[],uint256[],TakerAmounts)0x0fb25ba4"Polymarket CTF Exchange", "3"
ExchangeV3matchOrdersAndPrepareCombinatorial(…, uint256[])0xe75cf6e5same

order_hash is the EIP-712 hash of the order (chain id 137, the exchange's own address), the same orderHash as in OrderFilled. fill_amount equals that OrderFilled's makerAmountFilled. V2 orders have no expiration or fee rate: the operator sets each fill's fee.

The book view is a sink join, not a store (same reason as order fills): sum fill_amount per order_hash, and maker_amount minus that sum is what the order has left, in maker-asset units (collateral for a BUY, shares for a SELL), the way the exchange tracks it. It is an upper bound: orders cancelled off-chain or never filled are not on chain.

Trade fields

  • side: side of the filled order, 0 = BUY (pays collateral for shares), 1 = SELL.
  • shares, collateral, fee: raw 6-decimal integers. The fee is in collateral: a BUY pays it on top (collateral = maker_amount_filled + fee), a SELL has it taken from the proceeds (collateral = taker_amount_filled − fee).
  • price: collateral / shares rounded down to 6 decimals, so it is the price this order's owner actually paid or got, fee included.
  • is_match: true for the taker order's own fill (its taker is the exchange itself). Every match has exactly one, so summing is_match trades counts each match once. Maker fills of the same match carry the complementary view.
  • is_volume: true for the fills that count toward per-token volume, so each match is counted once per token. That is the taker fill, plus any maker fill on a different token than the taker's (a complementary match that mints or merges, so the taker fill doesn't cover that token). Maker fills on the taker's own token are the other side of the taker fill and are not volume. Market stats count trades and volume from is_volume fills only, and fees from every fill.
  • token_id: for exchange_v3, a Protocol V2 PositionManager position id, not a Conditional Tokens ERC1155 id.

SQL sink

db_out writes six tables to PostgreSQL:

TableHow rows are written
tradesinserted, one per fill, id <tx_hash>-<log_index>
market_statsupserted per token with the absolute values from map_market_stats
candles_1mmerged per (token_id, bucket_start): open set once, close set by each block, high GREATEST, low LEAST, volumes and trades added
order_fillsmerged per order_hash: amounts and fills added, first fill block set once, last fill block set by each block
ordersmerged per order_hash from map_orders: order fields set once, filled, fees and settlements added. The order_book view adds remaining, remaining_shares and fully_filled
resolutionsinserted: UMA adapter QuestionResolved / QuestionEmergencyResolved and Protocol V2 module ConditionResolved, from polymarket-resolution

schema.sql also has candles_5m, candles_1h and candles_1d views rolled up from candles_1m.

make build-trades
export SUBSTREAMS_SINK_DSN="postgres://user:pass@localhost:5432/polymarket?sslmode=disable"
substreams sink postgres setup polymarket-trades/substreams.yaml
substreams sink postgres polymarket-trades/substreams.yaml -e polygon.streamingfast.io:443

The add/max/min/set_if_null upserts are PostgreSQL-only, so ClickHouse has its own module, db_out_clickhouse, which only inserts. Candle and order fill fragments land as rows, one per token or order per block. candles_1m_mv merges the candles into the candles_1m_agg AggregatingMergeTree, and you read them from candles_1m_merged and the candles_5m / candles_1h / candles_1d views. Order totals are in the order_fills_totals view, and observed orders with what they have left in the order_book view. Run the ClickHouse sink on final blocks, because the materialized view can't take back rows from a reorg.

substreams.clickhouse.yaml imports substreams.yaml, so module hashes and caches are shared, and points the sink at db_out_clickhouse and clickhouse-schema.sql:

export SUBSTREAMS_SINK_DSN="clickhouse://default:@localhost:9000/default"
substreams sink clickhouse setup polymarket-trades/substreams.clickhouse.yaml
substreams sink clickhouse polymarket-trades/substreams.clickhouse.yaml -e polygon.streamingfast.io:443 --final-blocks-only

market_stats comes from stores that start at block 84902353, so a sink started later than that still backfills those stores from 84902353 first.

Build and run

make build-trades          # builds polymarket-exchange and polymarket-neg-risk-ctf first
substreams run polymarket-trades/substreams.yaml map_trades -s 96600000 -t +100

Modules

Execution graph

20 modules
map

map_candles

1-minute OHLCV fragment per token from this block's is_volume trades. No store: a sink merges fragments of the same (token_id, bucket_start) — open from the lowest (block_number, open_log_index), close from the highest, high max, low min, volumes and trades summed.

from #84902353
map

map_order_fills

For every order filled in the block: owner, token, side, exchange, and that block's filled shares, collateral, fees and fill count. No store: a sink keeps each order's running totals by summing fragments per order_hash.

from #84902353
map

map_orders

The full signed orders behind the fills, decoded from the calldata of the exchanges' matchOrders calls (successful, non-reverted calls only): maker, signer, token, side, full maker/taker amounts, limit price, size in shares, creation time, and the amount and fee this settlement filled. order_hash is the EIP-712 hash, the same as in OrderFilled. A sink sums fill_amount per order_hash, so maker_amount minus that is what the order has left.

from #84902353
store

store_last_price

Price of the latest is_volume trade per token, keyed by token id.

from #84902353

Store value

string

Update policy

set

store

store_token_totals

Running totals per outcome token, keyed `<token_id>:<field>`: trades, buys, sells, volume_collateral and volume_shares over is_volume trades (each match once per token), and fees over every fill.

from #84902353

Store value

bigint

Update policy

add

Show 11 dependencies
map

eth_common:all_events

`all_events` gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash

Output

sf.substreams.ethereum.v1.Events

blockIndex

eth_common:index_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

map

exchange:eth_common:all_events

`all_events` gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash

Output

sf.substreams.ethereum.v1.Events

blockIndex

exchange:eth_common:index_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

map

neg_risk_ctf:eth_common:all_events

`all_events` gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash

Output

sf.substreams.ethereum.v1.Events

blockIndex

neg_risk_ctf:eth_common:index_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

map

resolution:eth_common:all_events

`all_events` gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash

Output

sf.substreams.ethereum.v1.Events

blockIndex

resolution:eth_common:index_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