docs/integrations/blockchain.md
The blockchain adapter ingests DeFi data from EVM chains and exposes it through the NautilusTrader data model. It uses three backends:
The DeFi domain model lives in nautilus_model::defi.
Chain defines the target blockchain and its default service endpoints.
| Field | Type | Description |
|---|---|---|
name | Blockchain | Chain enum value, such as Ethereum or Arbitrum. |
chain_id | u32 | EVM chain ID, such as 1 for Ethereum. |
hypersync_url | String | HyperSync endpoint, by default https://{chain_id}.hypersync.xyz. |
rpc_url | Option | Optional direct RPC endpoint stored on the chain model. |
native_currency_decimals | u8 | Native gas token decimal precision, usually 18. |
Chains can be loaded by numeric ID with Chain::from_chain_id or by name with
Chain::from_chain_name.
| Chain family | Code | Name | Decimals |
|---|---|---|---|
| Ethereum and L2s | ETH | Ethereum | 18 |
| Polygon | POL | Polygon | 18 |
| Avalanche | AVAX | Avalanche | 18 |
| BSC | BNB | Binance Coin | 18 |
DEX integrations register:
Pool definitions bind the chain and DEX to a pool contract address or protocol pool ID to form a stable Nautilus instrument ID. The token pair, fee tier, tick spacing, and creation block remain pool metadata.
When the data engine processes a pool definition, it caches and publishes a CurrencyPair under
the same pool instrument ID. The instrument keeps the raw pool token0/token1 order as base/quote,
derives price and size precision from token decimals up to FIXED_PRECISION, and exposes the fee
tier divided by 1,000,000 as taker_fee. Distinct pool identifiers let same‑token pools coexist in
the cache and on the message bus.
Uniswap V3 and compatible concentrated-liquidity pools also use:
Initialize(uint160,int24) for initial price state.Mint and Burn events for position and tick state replay.Swap events for live pool price movement.slot0, liquidity, active ticks, and position data.| Option | Default | Description |
|---|---|---|
chain | Required | Target Chain, such as Ethereum or Arbitrum. |
dex_ids | [] | DEX integrations to register and sync. |
http_rpc_url | Required | HTTP RPC endpoint for contract reads and Multicall. |
wss_rpc_url | None | Optional WSS RPC endpoint for RPC live streams. |
rpc_requests_per_second | None | Optional RPC request throttle. |
multicall_calls_per_rpc_request | 200 | Requested maximum Multicall targets per RPC request. |
use_hypersync_for_live_data | false in Rust | When true, live block and event streams use HyperSync. |
from_block | None | Optional start block for historical sync. |
pool_filters | DexPoolFilters() | Pool universe filtering rules. |
postgres_cache_database_config | None | Optional Postgres cache configuration. |
proxy_url | None | Optional HTTP and WebSocket proxy URL. |
transport_backend | Tungstenite | WebSocket transport backend. |
:::note Pool snapshot requests currently require a Postgres cache database. The in-memory cache can hold tokens and pools, but latest pool profiler bootstrap reads snapshot and event state through the cache database path. :::
Set credentials outside the repository:
export ENVIO_API_TOKEN="<envio-token>"
export RPC_HTTP_URL="https://your-rpc.example"
export RPC_WSS_URL="wss://your-rpc.example"
For local .env usage, keep the file out of version control:
ENVIO_API_TOKEN=<envio-token>
RPC_HTTP_URL=https://your-rpc.example
RPC_WSS_URL=wss://your-rpc.example
ENVIO_API_TOKEN is required by the Rust HyperSync client. Missing or malformed tokens fail
client construction before any query is sent.RPC_HTTP_URL or --rpc-url is required for contract reads and snapshot hydration.RPC_WSS_URL is only needed for WSS RPC live streams.For token setup and quota details, see Envio's HyperSync API token docs.
RPC_HTTP_URL or --rpc-url must point at an EVM JSON-RPC endpoint for the target chain.
The data client resolves it at construction, and first-time pool syncs read on-chain state through it.
The HyperSync endpoint is derived from the chain ID (https://{chain_id}.hypersync.xyz).
Verified free public HTTP endpoints (June 2026, no API key):
| Chain | HTTP endpoint | Archive |
|---|---|---|
| Arbitrum One | https://arb1.arbitrum.io/rpc | No |
| Arbitrum One | https://arbitrum.gateway.tenderly.co | Yes |
| Ethereum | https://ethereum-rpc.publicnode.com | No |
Free archive endpoints exist, but availability and limits change. Snapshot validation usually needs
only a small number of eth_calls per pool, so a free archive endpoint can be enough to get
validation_state = on_chain.
Archive support affects validation, not whether event sync runs:
validation_state = on_chain.validation_state = replay,
which is still usable as a replay start point.--to-block, because non-archive
nodes only serve recent state and bootstrap reads on-chain state at the target block.For other chains or archive access, use a directory such as chainlist.org or comparenodes.com, or a keyed provider (Infura, Alchemy, dRPC).
The development compose file starts Postgres, Redis, and pgAdmin.
make start-services
make init-db
Default Postgres connection:
127.0.0.1:5432nautilusnautiluspassCheck that the schema exists:
docker exec nautilus-database psql -U nautilus -d nautilus -Atc \
"select count(*) from information_schema.tables where table_schema='public'"
For destructive DeFi tests, use a separate database or resettable Docker volume. Pool discovery and
snapshot tests can write many rows to token, pool, pool_*_event, pool_snapshot,
pool_position, and pool_tick.
sync-dex discovers pools and tokens once. analyze-pool(s) then generates pool_snapshot rows.
The diagram shows the default replay path and the --snapshot-from-rpc path.
flowchart TD
HS["HyperSync (Envio): logs and events"]
RPC["HTTP RPC + Multicall3: on-chain reads"]
PG[("Postgres cache")]
subgraph discovery["sync-dex (one-time discovery)"]
direction TB
D1["Stream factory PoolCreated logs"]
D2["Fetch ERC-20 token metadata"]
D3["Write pool and token rows"]
D1 --> D2 --> D3
end
subgraph analyze["analyze-pool(s) (snapshot generation, one task per pool)"]
direction TB
AP0{"Mode"}
AP1["Default: sync full pool events"]
AP2["Bootstrap from cache snapshot, replay events"]
AP3["extract_snapshot per --checkpoint-blocks"]
AP4["Persist snapshot + ticks + positions"]
AP5{"check_snapshot_validity"}
RP1["--snapshot-from-rpc: stream state events"]
RP2["Hydrate checkpoint from RPC"]
RP3["Persist snapshot + ticks + positions"]
AP0 --> AP1 --> AP2 --> AP3 --> AP4 --> AP5
AP0 --> RP1 --> RP2 --> RP3
AP5 -->|"matches chain"| V1["validation_state = on_chain"]
AP5 -->|"RPC cannot reach block, or --skip-validation"| V2["validation_state = replay"]
AP5 -->|"structural mismatch"| V3["validation_state = invalid"]
RP3 -->|"validated from RPC"| V1
end
R["Backtest replay: load latest usable snapshot (not invalid), replay forward"]
HS --> D1
RPC --> D2
D3 --> PG
HS --> AP1
HS --> RP1
PG --> AP2
AP4 --> PG
RP3 --> PG
RPC --> AP5
RPC --> RP2
PG --> R
analyze-pools runs one task per pool, bounded by --concurrency. Each task owns its data client.
A snapshot is usable as a replay start point unless its validation_state is invalid.
Pool discovery:
DexPoolFilters.use_hypersync_for_live_data = true: subscribe to blocks through HyperSync for live timestamps
and hold one open-ended HyperSync DEX-event stream per subscribed DEX filter.use_hypersync_for_live_data = false: use WSS RPC block and pool-log subscriptions for live
swaps, liquidity updates, fee collections, flash events, and fee-protocol events.For Uniswap V3-compatible snapshots, bootstrap:
Bootstrap modes:
--snapshot-from-rpc: skip full swap storage, stream Initialize, Mint, Burn, SetFeeProtocol, and
CollectProtocol events from HyperSync to enumerate ticks and positions, then hydrate the exact
checkpoint block from RPC.Use --snapshot-from-rpc for old high-volume pools when the required output is the final snapshot,
not a stored swap history. It cannot be combined with --from-block, --reset, or
--require-existing-snapshot.
If final RPC hydration fails, the adapter must fail closed. It must not emit a snapshot built from replayed events with stale price state.
Before marking a snapshot valid, bootstrap compares the replayed profiler against on-chain state. These structural fields must match exactly:
A structural mismatch fails closed and the snapshot is not marked valid.
Non-structural mismatches are accepted with a warning:
If only non-structural fields differ, the snapshot is accepted. This matches backtest replay behavior.
Use --require-existing-snapshot when analysis should run only from the local snapshot cache:
pool_snapshot at or before the target block.needs_bootstrap if no usable snapshot exists.nautilus blockchain analyze-pools \
--chain ethereum \
--dex UniswapV3 \
--addresses-file pools.txt \
--to-block 25218797 \
--require-existing-snapshot \
--rpc-url "$RPC_HTTP_URL"
analyze-pool(s) prints:
--checkpoint-blocks entry.--to-block when no checkpoints are given.A pool that needs a first-time bootstrap has this shape:
{
"chain": "Ethereum",
"dex": "UniswapV3",
"pool_address": "0x1111111111111111111111111111111111111111",
"target_block": 25218797,
"status": "needs_bootstrap"
}
A successful result includes validation_state:
on_chain: hydrated and matched against chain.replay: replay-derived or unchecked, still usable as a replay start point.invalid: hydrated and mismatched, not usable.{
"chain": "Ethereum",
"dex": "UniswapV3",
"pool_address": "0x1111111111111111111111111111111111111111",
"target_block": 25218797,
"status": "success",
"snapshot_block": 25218790,
"positions": 2,
"ticks": 7,
"validation_state": "replay",
"already_valid": false,
"liquidity_utilization_rate": 0.25
}
--checkpoint-blocks b1,b2,...: produces snapshots in one bootstrap pass. Blocks are sorted,
deduped, and clamped to --to-block.--concurrency: controls analyze-pools parallelism. Default: 4.--skip-validation: skips the on-chain compare and keeps replay-derived snapshots as replay.--snapshot-from-rpc: hydrates from chain at the checkpoint block and records snapshots as
on_chain.Snapshot keys:
--snapshot-from-rpc: keyed to the requested checkpoint block with a block-scoped sentinel
transaction/log index.Backtest replay needs a snapshot in the input data. The adapter does not service live snapshot requests during backtests.
load_pool_snapshot reads a full snapshot, including positions and ticks, from Postgres:
from nautilus_trader.adapters.blockchain import load_pool_snapshot
snapshot = load_pool_snapshot(
pg_config=postgres_config,
chain_id=chain_id,
pool_address=pool_address,
before_block=replay_start_block, # latest snapshot at or before this block
)
Replay rules:
on_chain snapshots are returned. Pass require_valid=False to accept replay
snapshots.None as setup failure. Do not replay without profiler state.DefiData.PoolSnapshot(snapshot) and pass it to
BacktestEngine.add_defi_data with the pool events.Cached block timestamps load into Nautilus data objects as UNIX nanoseconds. Cache rows written with second-resolution block timestamps are normalized to nanoseconds when snapshots and pool events are loaded, while nanosecond rows preserve their stored precision.
BaseContract batches contract calls through Multicall3
(0xcA11bde05977b3631167028862bE2a173976CA11):
allow_failure: true so individual contract call failures can be reported.Erc20Contract reads name, symbol, and decimals through Multicall. The adapter can skip pools
whose token metadata is malformed, raw bytes, or empty.
UniswapV3PoolContract reads global pool state, active ticks, and positions.
PancakeSwap V3 reuses the Uniswap V3 read contract because slot0, ticks, positions,
liquidity, and fee-growth reads share the same ABI. Fee-protocol encoding differs:
uint8.slot0.feeProtocol and emits
SetFeeProtocol(uint32,uint32,uint32,uint32).fee_protocol0_basis_points and
fee_protocol1_basis_points, and replay computes protocol fees as fee * basis_points / 10000.curl -fsS --max-time 15 \
-H "Authorization: Bearer $ENVIO_API_TOKEN" \
https://1.hypersync.xyz/height
Expected result: JSON with a numeric height.
query='{"from_block":25170900,"to_block":25170901,"include_all_blocks":true,"field_selection":{"block":["number","timestamp","hash"]}}'
curl -sS --max-time 30 \
-H "Authorization: Bearer $ENVIO_API_TOKEN" \
-H "Content-Type: application/json" \
--data "$query" \
https://1.hypersync.xyz/query/arrow-ipc \
-o /dev/null \
-w "http_code=%{http_code} size_download=%{size_download}\n"
Expected result: HTTP 200 with a non-zero response size.
cargo check -p nautilus-blockchain --features hypersync
This ignored test uses real HyperSync replay plus an invalid local HTTP RPC URL. It verifies that final RPC hydration fails closed instead of emitting a stale snapshot.
cargo test -p nautilus-blockchain --features hypersync \
live_hypersync_bootstrap_fails_closed_when_rpc_hydration_fails \
-- --ignored --nocapture
Expected result: one ignored test passes. This can take several minutes.
ENVIO_API_TOKEN, RPC keys, and Postgres credentials outside version control.These surface as analyze-pool(s) failures with a clear cause and fix.
analyze-pool(s) reads pool metadata from the cache and fails with Pool <address> is not registered
if the pool was never discovered. Run sync-dex for the chain/DEX once to populate the pool table
first.
A DEX can be registered for a chain yet lack the parsers a command needs. The CLI fails fast:
sync-dex (discovery) needs a PoolCreated parser.analyze-pool(s) (snapshots) need Initialize, Swap, Mint, Burn, and Collect parsers.SetFeeProtocol, so replay keeps fee-protocol settings
correct.CollectProtocol can replay protocol-fee balance withdrawals.Current support:
PoolCreated parser. Register pools
another way before analyze-pool(s).sync-blocks, but has no DEX registrations.blockchain analyze-pool --help and blockchain sync-dex --help print the current supported chain
and DEX combinations, derived from the registered parsers.
Addresses must be EIP-55 checksummed; a lowercase address fails with
Blockchain address '<address>' has incorrect checksum. Resolving a pool from
UniswapV3Factory.getPool returns lowercase, so checksum it before passing --address.
Public nodes enforce a per-call gas limit, so a large multicall returns out of gas and the adapter
falls back to slow per-item fetches. Pass a smaller --multicall-calls-per-rpc-request (for example
50 on https://arb1.arbitrum.io/rpc) to keep batches under the cap.
A first-time sync reads on-chain state at --to-block, and a non-archive node only serves recent
state, so historical targets fail the on-chain read. See RPC endpoints.
HyperSync rate limits apply per token. See Envio's HyperSync API token docs for token and usage details.
--concurrency low on free or low-quota tokens.--snapshot-from-rpc when an exact checkpoint snapshot is enough and full swap storage is not
needed.A pool with no processed Mint/Burn events up to the target block has no state to snapshot:
analyze-pools emits a per-pool "status": "failure" JSON line and keeps other pools running.analyze-pool returns the error.analyze-pool(s) exits non-zero when any pool fails, and each failed pool is also reported as a JSON
line with "status": "failure". Rely on the exit code for an overall pass/fail signal, and parse
each result line's status for per-pool detail.
Use this to check pool discovery, event parsing, and snapshot generation for one DEX on one chain. The example uses PancakeSwap V3 on Arbitrum.
ENVIO_API_TOKEN exported.--rpc-url or RPC_HTTP_URL).make start-services && make init-db).cargo build -p nautilus-cli --features defi --bin nautilus.Discover pools first, then analyze specific pools:
./target/debug/nautilus blockchain sync-dex --chain arbitrum --dex PancakeSwapV3 \
--rpc-url https://arb1.arbitrum.io/rpc \
--host 127.0.0.1 --port 5432 --username nautilus --password pass --database nautilus
./target/debug/nautilus blockchain analyze-pools --chain arbitrum --dex PancakeSwapV3 \
--address <pool-address> --address <pool-address> \
--rpc-url https://arb1.arbitrum.io/rpc \
--host 127.0.0.1 --port 5432 --username nautilus --password pass --database nautilus \
--concurrency 1
Verify by counting rows in:
pool_swap_eventpool_liquidity_eventpool_collect_eventpool_flash_eventpool_fee_protocol_update_eventpool_fee_protocol_collect_eventpool_snapshotpool_positionpool_tickFee-protocol tables are often empty or small because SetFeeProtocol and CollectProtocol rarely
fire.
--concurrency, or use --snapshot-from-rpc.sync-dex
immediately before analyze-pool(s) when in doubt.--from-block at a mid-life block skips Initialize, so snapshot bootstrap can fail with
Pool is not initialized and it doesn't contain initial price, cannot bootstrap profiler. Sync
from creation when a snapshot is required.count(*) to inspect pool rows.The event model currently targets Uniswap V3 concentrated-liquidity pools:
PoolSwap carries sqrt_price_x96 and tick.PoolLiquidityUpdate carries tick_lower and tick_upper.DexType and AmmType families exist, but most are not wired beyond discovery.Design the taxonomy before writing a parser. Most families do not fit the V3 structs:
Sync.ModifyLiquidity and Donate.Adding events piecemeal tends to create optional fields, duplicate variants, and later renames.
The design pass should:
DexPoolData variant.poolId protocols (Uniswap V4,
Balancer) and multi-token pools (Curve) break the per-pool-address, token-pair assumptions.<concept>_<verb> convention, such as fee_protocol_update. Reserve the
literal on-chain event name for signatures and error labels.Then wire each event through the full path, mirroring an existing one such as fee_protocol_collect:
DexExtended parser slotDexPoolData and DefiData variantsstream_pool_events UNION arm and row mapperCover it with a parser round-trip test, a profiler apply test, and the parser-parity test.
Incremental sync resumes from each pool's last-synced block. Adding an event type does not backfill already-synced history; run a reset sync from creation to populate the new table.
A new chain is registration only if its DEXes reuse modeled events:
Chain.A new protocol family needs the design pass above.
multicall_calls_per_rpc_request documents the intended batching limit, but some final snapshot
paths still need chunking hardening.validation_state = on_chain until the final-state hydration covers their pool contracts.