polyledger README

The project's README from github.com/nahrek/polyledger. As one markdown file: README.md.

PolyLedger

A resumable indexer for Polymarket market metadata and on-chain trade data, backed by DuckDB.

PolyLedger pulls every market from the Polymarket CLOB API, streams every OrderFilled event from Polygon via Envio HyperSync, and writes both into a single DuckDB file you can query with SQL immediately.


Table of contents

Features

Requirements

The CLOB and Gamma APIs are public and need no credentials.

Installation

git clone https://github.com/nahrek/polyledger
cd polyledger
python -m venv .venv
.venv\Scripts\activate.bat
pip install -e .

Then configure the HyperSync token. Copy .env.example to .env, open it in any editor, and set HYPERSYNC_BEARER_TOKEN. PolyLedger reads .env from the working directory on startup, so there is no export step. Real environment variables still take precedence if you prefer to set them that way.

Quick start

Run the full pipeline:

polyledger sync

The first backfill is slow. On the free HyperSync tier it takes anywhere from a few hours to a couple of days depending on how much history you want. It is safe to interrupt at any point. Every subsequent run is incremental and finishes in seconds.

To verify the setup on a small slice first:

polyledger markets                    # metadata only, a few minutes
polyledger chain --max-blocks 200000  # roughly five days of Polygon history
polyledger stats

stats reports what is actually in the database:

  markets          14231
  tokens           28455
  fills            1204773
  unmatched_fills  392
  first_block      75014820
  last_block       75214820
  first_time       2026-05-14 08:11:23
  last_time        2026-05-19 03:44:02
  checkpoints:
    order_filled:v2: block 75214820, 1204773 rows, updated 2026-09-02 15:11:23

Then query it:

polyledger query "SELECT question, sum(usd_size) AS volume FROM trades GROUP BY 1 ORDER BY 2 DESC LIMIT 10"

CLI reference

Command Description
polyledger markets [--backfill] Sync market metadata from the CLOB. --backfill also resolves unmatched token ids via Gamma.
polyledger chain [options] Index OrderFilled logs from Polygon.
polyledger trades [--drop-unmatched] Materialise the joined trades_mat table.
polyledger sync [options] Run markets → chain → backfill → trades in order.
polyledger stats Show row counts, block range, and checkpoints.
polyledger export [--out DIR] Write every table to Parquet.
polyledger query "SELECT ..." Run one SQL statement and print the result.

chain options:

Flag Description
--contracts v2|v1|all Which exchange generation to index. Default v2.
--from-block N Override the start block. Ignored if a checkpoint exists.
--to-block N Stop at this block.
--max-blocks N Index at most this many blocks, then exit.

Global flags: --db PATH to use a different database file, -v for verbose logging.

The polyledger executable is installed by pip install -e .. Without installing, python -m polyledger and python -m polyledger.cli are equivalent. The entry point is main() in polyledger/cli.py.

Data model

trades

The main analytical view, joining chain fills to market metadata. Always live and always current. Run polyledger trades to materialise it as trades_mat if you plan to issue many queries against it.

Column Description
block_time, block_number Block timestamp (UTC) and height
question, market_slug, outcome Human-readable context
token_id, condition_id Outcome and market identifiers
maker, taker Counterparty addresses
maker_side, taker_side BUY / SELL, both sides stated explicitly
price Execution price, 0 to 1
shares Outcome share quantity
usd_size Notional in dollars
fee Fee from the event
exchange_name, exchange_version Which contract emitted the fill
unmatched_market true when no market metadata was found

price, shares, and usd_size are not stored on chain. They are derived from makerAmountFilled and takerAmountFilled according to the maker's side: when the maker buys, their amount is collateral and the taker's is shares; when the maker sells, it is the other way round. Both are base units with 6 decimals, matching pUSD.

Many datasets collapse the two sides into a single ambiguous side column. PolyLedger records both, so you never have to guess whose perspective a row is written from.

Other tables

Table Contents
markets One row per market (condition_id), with question, slug, active/closed/neg_risk flags, tick size, and fees
tokens One row per outcome (token_id). This is the join key against fills
order_fills Raw decoded logs with no interpretation applied. Build your own join from here if you disagree with the price math
checkpoints One row per indexing stream

Ready-made queries for daily volume, OHLC candles, VWAP, per-address positions, and top traders are in examples/queries.sql.

The database is a plain DuckDB file:

import duckdb
con = duckdb.connect("data/polyledger.duckdb", read_only=True)
df = con.sql("SELECT * FROM trades WHERE token_id = '1001'").df()

Or export it with polyledger export and read the Parquet files from anywhere.

Indexed contracts

V2 contracts, live since the April 2026 migration, are indexed by default:

Contract Address
CTF Exchange V2 0xE111180000d2663C0091e4f400237545B87B996B
NegRisk CTF Exchange V2 0xe2222d279d744050d28e00520010520000310F59
NegRisk CTF Exchange V2 (b) 0xe2222d002000ba0053cef3375333610f64600036

Use --contracts v1 for the frozen V1 contracts (0x4bFb41d5…, 0xC5d563A3…), or --contracts all for both generations. Each stream keeps its own checkpoint, so switching modes will not corrupt a cursor built by earlier runs.

The two generations emit different event layouts, and the decoder handles both:

V2: OrderFilled(bytes32 orderHash, address maker, address taker, uint8 side,
                uint256 tokenId, uint256 makerAmountFilled,
                uint256 takerAmountFilled, uint256 fee,
                bytes32 builder, bytes32 metadata)

V1: OrderFilled(bytes32 orderHash, address maker, address taker,
                uint256 makerAssetId, uint256 takerAssetId,
                uint256 makerAmountFilled, uint256 takerAmountFilled, uint256 fee)

V2 states the side explicitly. V1 does not, so it has to be inferred: asset id 0 is collateral, and whichever leg is zero determines the maker's direction. V1 token-for-token fills between complementary outcomes are skipped, since they have no collateral leg and therefore no meaningful price.

You do not need to specify a start block. With no checkpoint and no POLYLEDGER_FROM_BLOCK, PolyLedger locates the first block containing a matching log. HyperSync skips empty ranges server-side, so this costs a handful of requests rather than a full chain scan.

Configuration

All settings are read from environment variables or from a .env file in the working directory, which is loaded automatically at startup. Environment variables take precedence.

Variable Default Description
HYPERSYNC_BEARER_TOKEN - Envio API token. Required
POLYLEDGER_DB data/polyledger.duckdb Database file path
POLYLEDGER_EXPORT_DIR data/parquet Parquet export directory
POLYLEDGER_CONTRACTS v2 v2, v1, or all
POLYLEDGER_FROM_BLOCK 0 0 means auto-detect
POLYLEDGER_REORG_BUFFER 64 Blocks to stay behind the chain head
POLYLEDGER_FLUSH_EVERY 50000 Fills buffered before a checkpointed flush
POLYLEDGER_HTTP_RPS 8 Request rate limit for CLOB and Gamma
POLYLEDGER_HTTP_CONCURRENCY 8 Parallel HTTP requests
POLYLEDGER_HTTP_RETRIES 6 Attempts per request
POLYLEDGER_HTTP_TIMEOUT 30 Request timeout in seconds

REORG_BUFFER guards against chain reorganisations by stopping 64 blocks short of the head, so a fill from a block that later gets orphaned is never written.

Design notes

DuckDB rather than CSV. Appending to a multi-gigabyte CSV degrades without bound, and any read means parsing every byte. DuckDB gives columnar compression, real types, indexes, and a proper SQL join instead of a pandas.merge that has to hold both sides in memory. It remains a single file with no server or daemon.

Atomic checkpoints. Rows and the new cursor value are written in one transaction. There is no state where data landed but the cursor did not, or the reverse.

Idempotent inserts. Duplicates within a single batch are collapsed before the write, and duplicates against existing rows are dropped by the primary key. A crash mid-batch cannot produce double rows on restart.

Loud failures on schema drift. Polymarket changed its response format in April 2026 and broke every consumer built on the old shape. Pydantic validation means the next change surfaces as an error on the first affected row.

Visible gaps. Some token ids from older fills are absent from the CLOB listing. Those rows are marked unmatched_market = true, counted in stats, and recovered from Gamma with --backfill. Nothing is discarded; the trade stays in the database, just without human-readable context.

Development

pip install -e ".[dev]"
pytest

41 tests, none requiring network access. They cover decoding for both event versions against fixtures, insert idempotency and transaction rollback, price and side arithmetic, CLOB pagination including the stuck-cursor guard, Gamma batching and fallback, the rate limiter, and the backoff schedule. One test recomputes topic0 from the textual event signatures and compares it against the hard-coded constants, so a typo in either cannot go unnoticed.

Limitations

License

MIT

polyledger (nahrek/polyledger)

Searching Database…

这是什么?PolyLedger 是一个可断点续传的索引器,从 CLOB API 拉取 Polymarket 的市场元数据,从 Polygon 拉取每一笔链上 OrderFilled 交易,并把两者写入一个可以用普通 SQL 查询的 DuckDB 文件。
这是 Python 库还是命令行工具?它是命令行工具。用 pip 安装后会得到一个 `polyledger` 可执行文件,带有 markets、chain、trades、sync、stats、export、query 等子命令。
怎么把它跑起来?用 `pip install -e .` 安装,把 `.env.example` 复制为 `.env` 并设置你的 HyperSync 令牌,然后运行 `polyledger sync`,执行从市场到链上再到交易的完整流水线。
使用它需要 API 密钥吗?需要,你需要一个来自 envio.dev 的免费 HyperSync API 令牌来读取 Polygon 链上数据;它用到的 Polymarket CLOB 和 Gamma API 是公开的,无需凭据。
它捕捉实时订单簿还是只捕捉已完成的交易?只捕捉到达链上的已成交交易;订单簿状态、报价和撤单从不被索引,因为它们不会出现在链上。
它索引的 V1 和 V2 合约有什么区别?V2 明确写出每笔成交的买/卖方向,而 V1 只给出原始代币数量,PolyLedger 必须根据哪一方是抵押品来推断方向;每一代合约都有各自的解码器和检查点。
为什么用 DuckDB 而不是常规数据库或 CSV 文件?DuckDB 在单个文件、无需服务器的情况下提供列式压缩、真实类型、索引和 SQL 连接,而不断增长的 CSV 会随时间退化,并且每次读取都要解析整个文件。
如果索引器运行中途崩溃会怎样?数据行和区块检查点在同一个事务中提交,插入按主键去重,因此重启只需从最后提交的区块继续,不会产生部分数据或重复数据。
除 Polymarket 外,它能跟踪 Kalshi 或其他预测市场吗?不能,目前只支持 Polymarket。Kalshi 不在链上结算,因此需要单独的采集器,不过分阶段架构为将来添加留出了空间。
Creator
nahrek
Size
0.7 MB
Stars
599
Updated
CommitOct 17
Age
1mo
Commits · 90 Days