Skip to main content
Coming from GET /erc20/{address}/owners or GET /erc20/{address}/holders? The field-by-field migration for those endpoints lives on the Token Holders recipe; this page is the same balance data in its raw by-token shape.

Question it answers

“Give me the non-zero balance of every wallet that holds token 0x…, largest first.”
This is the by-token sort of the same balance data the public API exposes per holder, the holder list and balances for a single token. The sibling recipe Token Balances by Wallet is the exact same data sorted the other way (wallet-first); in production you land the source once and add both read shapes.

What you get

The recipe lands one balance observation per side of every transfer, then resolves the latest observation per (token, wallet) as the current balance. Moralis-indexed transfers carry the absolute post-transfer balance of both sides (fromPostBalance / toPostBalance), so no running-sum reconstruction is needed; the highest (block_number, log_index) observation is the truth. Each transfer yields two observations: (from_address, from_post_balance) and (to_address, to_post_balance). The EVM zero address (mint/burn counterparty) and any side without a producer-resolved post-balance are skipped.

Source

The transform reads one per-block array, tokenTransfers, and unpivots each transfer into the two per-wallet balance observations it carries. Balance comes straight from the transfer’s fromPostBalance / toPostBalance; there is no separate balance feed or join.

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, and FINAL collapses the pair before argMax runs. Read canonical state with FINAL or a sign-aware aggregate, never a bare WHERE sign = 1. On Postgres and MySQL the sink appends observations and the current-balance projection is derived: Postgres via a DISTINCT ON (token_address, wallet_address) materialized view, MySQL via an AFTER INSERT latest-wins upsert trigger plus a periodic zero-balance cleanup.

Full schema

Below is the complete read table this recipe produces, the per-wallet balance observations, keyed by token. Keep the columns you need and drop the rest (see Schema & flexibility). Raw uint256 balances are stored as text in ClickHouse (they exceed numeric precision); Postgres uses an explicit NUMERIC(76, 0) so large raw balances don’t overflow a narrower inferred type.
The leg (from / to) keeps the two unpivoted rows of one transfer distinct; the +1/−1 reorg pair for one leg shares a key and collapses. Read with FINAL then argMax, or a sign-aware aggregate. A single-node setup can use CollapsingMergeTree(sign) without the replication path.
The materialized view is the current-balance projection, refresh it on a schedule with REFRESH MATERIALIZED VIEW CONCURRENTLY token_balances; (the CONCURRENTLY form needs the unique index above and never blocks readers). MySQL is the same shape with a trigger-maintained token_balances table and a periodic DELETE … WHERE balance = 0 standing in for the cleanup index.

Example reads

All current non-zero holders of a token, largest first, FINAL collapses reorg ±1 pairs before argMax resolves the latest balance per wallet (ClickHouse):
Holder count for a token:
Postgres, refresh the view, then query the partial index:

Modes

Shipped defaults: ClickHouse hybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the overview.
Postgres / MySQL are backfill-first here. The realtime reorg path needs a single-column unique key on the position column, but the recipe’s position is block-level, so the Postgres / MySQL configs target historical. The Postgres materialized view and the MySQL cleanup are also not reorg-aware on their own; under realtime you would re-derive them. ClickHouse handles reorgs automatically via sign, so run realtime/hybrid there.

Multichain

The recipe is chain-parametrized, point it at any supported EVM chain or Solana. On Solana, the per-observation vendor_event_id already folds in (from, to, token, amount), so each observation stays row-unique despite Solana’s repeated logIndex within an instruction; the holder list it produces is identical in shape.

Fidelity notes

  • Latest-wins, not a delta sum. Balances are absolute post-transfer observations, so the current balance is purely the most recent observation per (token, wallet). There is no running-sum reconstruction and no dependence on having seen every prior transfer.
  • Raw amounts only. balance is the raw uint256 (text in ClickHouse, NUMERIC(76, 0) in Postgres). It is not decimal-scaled and carries no USD value, divide by 10^token_decimals for human units (fold in a decimals lookup from a Token Metadata sync), and join Token Prices if you need USD.
  • Zero address excluded. Mint/burn counterparties (the EVM zero address) and sides without a producer-resolved post-balance are not holders and are skipped at unpivot time.

Token Balances by Wallet

The sibling, the same balance data sorted wallet-first.

Token Analytics

The use case holder lists and balances power.