Skip to main content

Question it answers

“What are the current reserve0 / reserve1 of DEX pair 0x…?”
Mirrors Moralis GET /{pair_address}/reserves{ reserve0, reserve1 }. The recipe lands the pool’s absolute token balances and keeps the latest observation per pair, so a single read returns each pair’s current reserves.

What you get

The recipe lands one observation per swap, the pool’s absolute token balances captured after each trade, and the current reserves of a pair are the highest-(block_number, log_index) observation for that pair: These are absolute snapshots, not deltas, no running sum, no pre-window baseline, no genesis requirement. The latest snapshot in any window is the answer (window-safe).

Source

The faithful source for reserves is tokenSwaps. Each swap carries token0PostBalance / token1PostBalance, the pool’s absolute balances after the trade, which are exactly the pair’s reserves. The transform projects one reserve observation per swap, keyed by pair. pairLiquidityChanges is not used: it only fires on add/remove liquidity, so a running sum of its deltas misses all swap-driven reserve movement and is window-relative. Aggregated swaps are also not used, they carry an aggregator address, not a pair_address, so their post-balances can’t be attributed to a specific pool.

Destination

ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct: a reorg negates the rolled-back block’s observations (sign = -1) and re-emits the corrected ones. Read with FINAL to collapse the ±1 pair, then argMax to pick the latest observation, never a bare WHERE sign = 1. Postgres and MySQL append append-only observations and derive a latest-wins current-reserves surface per pair: Postgres via a DISTINCT ON (pair_address) … ORDER BY block_number DESC, log_index DESC materialized view, MySQL via an AFTER INSERT upsert trigger.

Full schema

The ClickHouse fact table is the per-pair observation table; the current reserves are an argMax over it. Reserves are raw uint256 stored as text (matching the API’s string reserves), argMax over text ordered by the numeric recency key is exact. Keep the columns you need (see Schema & flexibility).
The sign column drives reorg collapsing, read with FINAL then argMax. A single-node setup can use CollapsingMergeTree(sign) without the replication path.
MySQL is the same shape: an append-only pair_reserve_observations table plus a pair_reserves table kept current by an AFTER INSERT latest-wins upsert trigger, with reserve0 / reserve1 as DECIMAL(65,0). position is the block-level cursor used during backfill.

Example reads

Current reserves of a pair, latest observation; FINAL collapses reorg ±1 pairs before argMax (ClickHouse):
Largest pools by latest reserve0 (ClickHouse):
Postgres, refresh the view, then look up the pair:

Modes

Shipped defaults: ClickHouse hybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the overview.
The Postgres / MySQL realtime reorg path needs a single-column UNIQUE on the position column, but position is block-level. Run realtime / hybrid on ClickHouse (the collapsing log table corrects reorgs per block); the Postgres / MySQL configs target historical backfill and re-derive their current-reserves surface on refresh.

Multichain

The recipe is chain-parametrized via the chain setting, point it at any supported EVM chain or Solana. On Solana, the event identity already folds in (pair_address, token0PostBalance, token1PostBalance) on top of (transaction_hash, log_index), so it stays row-unique despite Solana’s repeated log indices within an instruction.

Fidelity gaps

  • Only pairs with a swap in-window get current reserves. Reserves are read from swap post-balances, so a pair appears only if it had at least one swap with resolved post-balances in the ingested window. A pool that is quiet (no swaps) for the whole window won’t be present, but quiet pools have unchanged reserves by definition, so widening the window picks them up.
  • Swaps without resolved post-balances are skipped. token0PostBalance / token1PostBalance are nullable; rows where either is null/empty, or where the swap has no pair address, are filtered out.
  • V4 pair addresses arrive as poolId_0xaddress (~109 chars) and are stored raw for a key that’s consistent across destinations. MySQL caps pair_address at VARCHAR(160); Postgres TEXT and ClickHouse String are unbounded.
  • MySQL precision ceiling. MySQL DECIMAL(65,0) cannot represent a full 78-digit uint256, though no real reserve approaches that magnitude. Postgres (NUMERIC(76,0)) and ClickHouse (String) carry the larger range.

Swaps by Pair

Every DEX trade for a pair, the same swap stream these reserves derive from.

Pair OHLCV

Per-pair price candles for charting.