Skip to main content

Question it answers

“What was wallet 0x…‘s balance of token 0x… at block N, for any past block? And who held the token back then?”
The latest-state balance recipes (Token Balances by Wallet, Token Holders) keep only the current balance. This recipe keeps every observation, so an as-of-block balance is a point lookup and a token’s holder set at block N is a single query.

What you get

Token transfers carry the absolute post-transfer balance of both sides (fromPostBalance / toPostBalance). Each transfer becomes two per-wallet observations, and all of them are retained (not just the latest). The balance as of block N is the observation with the highest (block_number, log_index) at or below N.
ColumnDescription
wallet_addressThe holder, leading sort key
token_addressThe token contract
balanceAbsolute post-transfer balance, raw uint256 as text
block_number, log_indexRecency tuple, the latest at or below N wins
legfrom / to, which side of the transfer produced this observation
tx_hash, event_tsThe transaction and block time
vendor_event_idStable per-observation identity

Source

The transform reads one per-block array, tokenTransfers, and unpivots each transfer into (from_address, from_post_balance) and (to_address, to_post_balance). Because the source supplies absolute post-balances, there’s no running-sum reconstruction: the balance at any block is just the latest retained observation at or below it.

Destination

DestinationTableAs-of-block access
ClickHouse (first-class)fact_balance_observationsargMax(balance, (block_number, log_index)) over FINAL WHERE block_number <= N
Postgreswallet_token_balance_history (append-only event log)DISTINCT ON (wallet, token) … WHERE block_number <= N ORDER BY … block_number DESC, log_index DESC
MySQLwallet_token_balance_history (append-only event log)correlated subquery per (wallet, token) served by (wallet, token, block_number, log_index)
ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct. The fact table’s sort key is wallet-first, so a wallet’s balance timeline is a contiguous range read; a bloom skip index on token_address serves the inverse “holders of token T” access path.

Full schema

Read with FINAL (then argMax) or a sign-aware aggregate, never a bare WHERE sign = 1. A single-node setup can use CollapsingMergeTree(sign) without the replication path.
MySQL is the same shape with DECIMAL(76,0). position is the block-level cursor used during backfill.

Run it

Everything runs in your infrastructure: the sink writes into a database you own and you query it with plain SQL, no API and no per-call limits. Prerequisites
  • Docker and Docker Compose.
  • A Data Feeds key, a cm_live_… key with continuum:read scope (this is not your legacy Web3 API key).

Sign up for a Data Feeds key

Log in to the Moralis admin panel to create your account and generate a cm_live_… key.
Start the sink Generate a starter pack for historicalBalanceAtBlock in the Moralis Admin Panel, pick your chain and destination database, then run it with Docker. The quickstart walks through it end to end. Set the backfill window (MORALIS_HISTORICAL_FROM_BLOCK) as noted below.
Backfill full history for correct point-in-time answers. An as-of-block balance (or holder set) is only correct if the observation log goes back far enough, a wallet may have last moved the token long before block N. For token-scoped questions (“holders of token T at block N”), backfill from around the token’s deployment block; for whole-wallet history, backfill from an early block. A recent-only window gives wrong historical answers. See History & backfill.
Then run the example reads below to confirm rows are landing.

Example reads

Balance of one (wallet, token) as of block N (ClickHouse):
A token’s holder set as of block N, the historical-holders query, biggest first:
Postgres (as-of-block balance for a wallet, index-driven):

Other ways to consume this data

The sink is the way to get this retained balance log (fact_balance_observations / wallet_token_balance_history), recommended for point-in-time queries you own. The same underlying data is also available as the raw decoded feed, if you’d rather retain balance history yourself:
  • Your existing message clients: stream it with Kafka / AMQP / SQS.
  • REST / Arrow Flight: pull the decoded block stream directly.
  • Your warehouse: query it via Iceberg from Snowflake, BigQuery, Spark, or DuckDB.
Those give you the source array (tokenTransfers, with absolute post-balances) to build the history yourself, not this retained log. All of them read the one data lake into your infrastructure. See What are Data Feeds?.

Modes

Shipped defaults: ClickHouse hybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the overview.
The append-only log on Postgres / MySQL isn’t reorg-aware on its own. Run realtime/hybrid on ClickHouse, where the collapsing log table corrects reorgs per-block via sign; the Postgres / MySQL configs target historical backfill.

Multichain

The recipe is chain-parametrized via the chain setting, point it at any supported EVM chain or Solana. The Solana feed’s tokenTransfers carry the same absolute fromPostBalance / toPostBalance fields, so the recipe produces an identically-shaped balance history; the vendor_event_id already folds in (from, to, token, amount) to stay row-unique under Solana’s repeated logIndex.

Fidelity gaps

  • balance_formatted: needs the token’s decimals (contract metadata, not in tokenTransfers). Only the raw balance is emitted; divide by 10^decimals off-stream, sourcing decimals from a Token Metadata sync.
  • No USD value: this recipe lands quantities. Join a price feed at read time if you want historical USD.
  • Native balance at block: not covered in v1; the same pattern over nativeTransfers post-balances would unlock it.
On-chain primitives, wallet, token, raw post-event balance, block, log index, tx hash, are fully covered.

Migrating from the REST API

This recipe replaces the legacy historical token holders endpoints on EVM and Solana.
Both legacy endpoints below are deprecated and sunset on July 31, 2026. Migrate before that date.
Neither move is 1:1: the legacy endpoints returned a pre-bucketed time series, while this recipe lands the retained balance log that produces it. You resolve each bucket boundary to a block, count the as-of-block holder set, and diff adjacent buckets, and a full backfill from the token’s deployment block is required, or early buckets undercount (see the backfill note in Run it and History & backfill).

EVM: historical token holders

The holder-count time series becomes an aggregation over this recipe’s balance log: the holder set as of block N query, run per bucket boundary.
This endpoint needs more than one recipe. Counts and deltas come from this recipe alone, but newHoldersByAcquisition (swap / transfer / airdrop) classifies each new holder’s first inbound event, which you build from Token Transfers + Swaps by Token.
Legacy fieldData FeedsFidelity
timestampyour chosen bucket boundary, resolved to a blockyour definition
totalHolderscount of the as-of-block holder setexact
netHolderChange, holderPercentChangedeltas between adjacent bucketscalculated
holdersIn / holdersOut (whales … shrimps)size-tier flows on thresholds you setyour definition
newHoldersByAcquisition (swap / transfer / airdrop)classify new holders’ first inbound transfer/swapyour definition
Holder count as of a bucket’s end block (ClickHouse, repeat per bucket, or drive from a calendar):
Gotchas:
  • Materialize, don’t recompute. Large tokens have millions of holders, compute total_holders per bucket on a schedule and store the small series; the deltas are then trivial.
  • No fixed timeFrame. Every observation is retained, so the same log serves hourly, daily, or custom windows.
  • Tiers and acquisition are conventions you set: a feature, not a black box, but budget time to define them.
  • Backfill from the token’s deployment block, not chain genesis, the recipe is token-scoped for this question.

Solana: historical holders

The same recipe, pointed at Solana in the Admin Panel wizard, replaces it, Solana’s tokenTransfers carry the same absolute post-balances, so the balance log is identical in shape (see Multichain). The field mapping matches the EVM table above, with the mint as token_address; newHoldersByAcquisition likewise needs Token Transfers + Swaps by Token layered in. Holder set as of a past block, the building block for each bucket (ClickHouse):
Gotchas:
  • SPL account model. Count owner wallets, summing a wallet’s token accounts for its per-owner balance.
  • Backfill from the mint’s deployment: a short window gives wrong historical counts.
  • Slot boundaries. Resolve each interval boundary to a block/slot before running the as-of query.

Token Holders

The latest-state holder set this time-travels.

Token Balances by Wallet

Current balances per wallet, the latest-state sibling.