
Comprehensive Substreams package for tracking Polymarket P&L with SQL sink support for persistent state accumulation. Tracks all trading activity from CTF Exchange and Neg Risk Exchange contracts across both CLOB v1 and CLOB v2.
Polymarket migrated to CLOB v2 at the 2026-04-28 ~11:00 UTC cutover. v2 ships fresh Exchange contracts at new addresses, a redesigned OrderFilled event, a different fee model, and a new collateral wrapper (pUSD). This package indexes both contract generations side-by-side so a single P&L pipeline spans the migration.
| Concept | CLOB v1 | CLOB v2 |
|---|---|---|
| Exchange contracts | 0x4bfb…982e (CTF) / 0xC5d5…f80a (NegRisk) |
0xE111…996B (CTF) / 0xe222…0F59 (NegRisk) — fresh deploys |
| Order uniqueness | nonce per maker |
timestamp (ms) — nonces removed |
| Order side | Inferred from makerAssetId == 0 |
Explicit side enum on the order and event (BUY=0, SELL=1) |
| OrderFilled event | 8 fields, including makerAssetId + takerAssetId |
10 fields: single tokenId + side + new builder (bytes32) + metadata (bytes32) |
| Fees | Embedded in order (feeRateBps), maker + taker |
Protocol-determined at match time, taker only, dynamic per market via getClobMarketInfo() |
| Collateral (wallet) | USDC.e directly | pUSD — a 1:1-backed ERC-20 wrapper. USDC.e converts via CollateralOnramp.wrap() |
| Collateral (CTF level) | USDC.e | USDC.e (unchanged — pUSD is purely wallet-facing) |
| Builder attribution | HMAC headers on API orders | Single builderCode (bytes32) on the order, surfaced as builder on the event |
| EIP-712 domain version | "1" |
"2" for exchange signing (L1 API auth still "1") |
| Open orders at cutover | — | All wiped during the maintenance window |
map_order_fills watches all four Exchange addresses (v1 + v2). When a log lands on a v2 contract, it routes to the v2 decoder; v1 contracts still use the original v1 decoder. Both produce the same internal OrderFilledEvent shape, so the rest of the pipeline (stores, P&L math, SQL sink) sees one homogeneous stream.tokenId with an explicit side. We synthesize the legacy maker_asset_id / taker_asset_id pair from (side, tokenId) using the same convention as v1 (collateral-side gets "0", the conditional-token side gets the token id). This keeps your existing WHERE side = 'buy' queries and the cost-basis store working with zero changes.builder and metadata are decoded but currently land only in the in-memory event (not yet in the SQL trades table — see Migration to richer v2 schema below for an opt-in extension).exchange column tags every row with which contract emitted it: ctf / neg_risk (v1) or ctf_v2 / neg_risk_v2. So you can filter, partition, or audit by generation.fee field but represent a single realized taker fee rather than maker+taker fees.trades, user_pnl, user_positions, markets, daily_stats tables work unchanged — v2 fills slot in as additional rows with the new exchange values.map_order_fills → stores → analytics → db_out DAG.If you want to query v2-specific fields like builder (for builder attribution analytics) or metadata, you can add columns and surface them in db_out:
ALTER TABLE trades
ADD COLUMN exchange_version VARCHAR(4) DEFAULT 'v1',
ADD COLUMN builder VARCHAR(66),
ADD COLUMN metadata VARCHAR(66);
CREATE INDEX idx_trades_builder ON trades(builder) WHERE builder IS NOT NULL;
Then add the matching .set("builder", ...) / .set("metadata", ...) calls in db_out and republish. This is opt-in — the package as shipped doesn't require it.
| Feature | Description |
|---|---|
| CLOB v1 + v2 | Decodes both Exchange generations into the same P&L pipeline |
| Real P&L Tracking | Realized & unrealized P&L with cost basis |
| SQL Sink | PostgreSQL/Clickhouse for persistent state |
| Trader Analytics | Volume, win rate, max drawdown |
| Market Stats | Price, volume, trade counts per market |
| Whale Detection | Large trade tracking with trader context |
# Install CLI
brew install streamingfast/tap/substreams
# Authenticate
substreams auth
# Stream order fills
substreams run https://spkg.io/PaulieB14/polymarket-pnl-v1.2.0.spkg \
map_order_fills \
-e polygon.substreams.pinax.network:443 \
-s 65000000 -t +1000
# Stream user P&L
substreams run https://spkg.io/PaulieB14/polymarket-pnl-v1.2.0.spkg \
map_user_pnl \
-e polygon.substreams.pinax.network:443 \
-s 65000000 -t +1000
Important: P&L requires accumulated state. Use the SQL sink for accurate calculations.
# Install sink
brew install streamingfast/tap/substreams-sink-sql
# Create database
createdb polymarket_pnl
# Setup schema
substreams-sink-sql setup \
"psql://localhost:5432/polymarket_pnl?sslmode=disable" \
https://spkg.io/PaulieB14/polymarket-pnl-v1.2.0.spkg
# Run sink (start from beginning for full history)
substreams-sink-sql run \
"psql://localhost:5432/polymarket_pnl?sslmode=disable" \
https://spkg.io/PaulieB14/polymarket-pnl-v1.2.0.spkg \
-e polygon.substreams.pinax.network:443
-- Top traders by P&L
SELECT * FROM leaderboard_pnl LIMIT 20;
-- Whale trades with trader stats
SELECT * FROM whale_trades;
-- User positions
SELECT * FROM user_positions
WHERE user_address = '0x...' AND quantity > 0;
-- Daily stats
SELECT date, total_volume, total_trades
FROM daily_stats ORDER BY date DESC;
Polygon Blockchain
│
▼
┌─────────────────────────┐
│ Firehose Blocks │
└─────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ map_order_fills │ │map_token_transf │ │map_usdc_transf │
│ (CTF + NegRisk)│ │ (ERC1155) │ │ (USDC) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────┼───────────────────┘
▼
┌─────────────────────────┐
│ STORES (State) │
│ • user_positions │
│ • user_cost_basis │
│ • user_realized_pnl │
│ • latest_prices │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ map_user_pnl │
│ (Computed Analytics) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ db_out │
│ (SQL Sink) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ PostgreSQL │
└─────────────────────────┘
| Module | Description |
|---|---|
map_order_fills |
OrderFilled events from CTF & NegRisk exchanges |
map_token_transfers |
ERC1155 TransferSingle events |
map_usdc_transfers |
USDC transfer events |
| Store | Key | Description |
|---|---|---|
store_user_positions |
{user}:{token} |
Position quantities |
store_user_cost_basis |
{user}:{token} |
Total cost basis |
store_user_realized_pnl |
{user} |
Realized P&L |
store_user_volume |
{user} |
Trading volume |
store_user_trade_count |
{user} |
Trade count |
store_market_volume |
{token} |
Market volume |
store_latest_prices |
{token} |
Latest prices |
| Module | Description |
|---|---|
map_user_pnl |
Real-time P&L calculations |
map_market_stats |
Market-level statistics |
| Module | Description |
|---|---|
db_out |
Database changes for SQL sink |
| Table | Description |
|---|---|
trades |
All order fills with price, amount, side |
user_pnl |
Aggregated P&L per user |
user_positions |
Current positions with cost basis |
markets |
Market statistics |
daily_stats |
Daily aggregates |
| View | Description |
|---|---|
leaderboard_pnl |
Top 1000 by P&L |
leaderboard_volume |
Top 1000 by volume |
whale_trades |
Trades >$10K |
| Contract | Address | Start Block |
|---|---|---|
| CTF Exchange v1 | 0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e |
33,605,403 |
| NegRisk Exchange v1 | 0xC5d563A36AE78145C45a50134d48A1215220f80a |
50,505,492 |
| Contract | Address |
|---|---|
| CTF Exchange V2 | 0xE111180000d2663C0091e4f400237545B87B996B |
| NegRisk CTF Exchange V2 | 0xe2222d279d744050d28e00520010520000310F59 |
| Contract | Address | Start Block |
|---|---|---|
| Conditional Tokens | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 |
4,023,686 |
| USDC | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
4,023,686 |
# Clone
git clone https://github.com/PaulieB14/Polymarket-P-L-Substreams
cd Polymarket-P-L-Substreams
# Build
substreams build
# Test
substreams run substreams.yaml map_order_fills \
-e polygon.substreams.pinax.network:443 \
-s 65000000 -t +100
# Package & publish
substreams pack substreams.yaml -o polymarket-pnl-v1.2.0.spkg
substreams publish polymarket-pnl-v1.2.0.spkg
P&L calculation requires state accumulation over time:
| Without Sink | With SQL Sink |
|---|---|
| No history | Full history persisted |
| P&L = $0 | Accurate P&L |
| Stateless | Tracks cost basis |
| Demo only | Production ready |
MIT
Extracts OrderFilled events from CTF Exchange and NegRisk Exchange across both CLOB v1 (legacy) and v2 (deployed 2026-03-31, cutover 2026-04-28). This is the primary source of trading activity.
substreams gui polymarket-pnl@v1.2.1 map_order_fillsExtracts ERC1155 TransferSingle events for position tracking.
substreams gui polymarket-pnl@v1.2.1 map_token_transfersExtracts USDC (ERC20) transfer events for collateral tracking.
substreams gui polymarket-pnl@v1.2.1 map_usdc_transfersComputes real-time P&L for users based on their positions and trades. Outputs user P&L updates when positions change.
substreams gui polymarket-pnl@v1.2.1 map_user_pnlComputes market-level statistics (volume, trades, prices).
substreams gui polymarket-pnl@v1.2.1 map_market_statsOutputs database changes for SQL sink (PostgreSQL/Clickhouse). Creates/updates tables: trades, user_pnl, user_positions, markets
substreams gui polymarket-pnl@v1.2.1 db_outall_events gives you all the events in a block (from successful transactions), with basic block hash/number/timestamp and transaction hash
substreams gui polymarket-pnl@v1.2.1 ethcommon:all_eventsAccumulates user positions per token. Tracks quantity held. Key: {user_address}:{token_id}
substreams gui polymarket-pnl@v1.2.1 store_user_positionsTracks total cost basis per user per token for average price calculation. Key: {user_address}:{token_id}
substreams gui polymarket-pnl@v1.2.1 store_user_cost_basisAccumulates realized P&L when users sell positions. Key: {user_address}
substreams gui polymarket-pnl@v1.2.1 store_user_realized_pnlTracks total trading volume per user. Key: {user_address}
substreams gui polymarket-pnl@v1.2.1 store_user_volumeTracks number of trades per user. Key: {user_address}
substreams gui polymarket-pnl@v1.2.1 store_user_trade_countTracks total volume per market (token_id). Key: {token_id}
substreams gui polymarket-pnl@v1.2.1 store_market_volumeStores latest price per token for unrealized P&L calculation. Key: {token_id}
substreams gui polymarket-pnl@v1.2.1 store_latest_pricesindex_events sets the following keys on the block:
substreams gui polymarket-pnl@v1.2.1 ethcommon:index_events