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.
| Column | Description |
|---|---|
wallet_address | The holder, leading sort key |
token_address | The token contract |
balance | Absolute post-transfer balance, raw uint256 as text |
block_number, log_index | Recency tuple, the latest at or below N wins |
leg | from / to, which side of the transfer produced this observation |
tx_hash, event_ts | The transaction and block time |
vendor_event_id | Stable 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
| Destination | Table | As-of-block access |
|---|---|---|
| ClickHouse (first-class) | fact_balance_observations | argMax(balance, (block_number, log_index)) over FINAL WHERE block_number <= N |
| Postgres | wallet_token_balance_history (append-only event log) | DISTINCT ON (wallet, token) … WHERE block_number <= N ORDER BY … block_number DESC, log_index DESC |
| MySQL | wallet_token_balance_history (append-only event log) | correlated subquery per (wallet, token) served by (wallet, token, block_number, log_index) |
token_address serves the inverse “holders of token T” access path.
Full schema
ClickHouse, fact_balance_observations
ClickHouse, fact_balance_observations
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.Postgres, wallet_token_balance_history
Postgres, wallet_token_balance_history
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 withcontinuum:readscope (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.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.
Example reads
Balance of one(wallet, token) as of block N (ClickHouse):
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.
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: ClickHousehybrid (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 thechain 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’sdecimals(contract metadata, not intokenTransfers). Only the rawbalanceis emitted; divide by10^decimalsoff-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
nativeTransferspost-balances would unlock it.
Migrating from the REST API
This recipe replaces the legacy historical token holders endpoints on EVM and Solana. 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
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 field | Data Feeds | Fidelity |
|---|---|---|
timestamp | your chosen bucket boundary, resolved to a block | your definition |
totalHolders | count of the as-of-block holder set | exact |
netHolderChange, holderPercentChange | deltas between adjacent buckets | calculated |
holdersIn / holdersOut (whales … shrimps) | size-tier flows on thresholds you set | your definition |
newHoldersByAcquisition (swap / transfer / airdrop) | classify new holders’ first inbound transfer/swap | your definition |
- Materialize, don’t recompute. Large tokens have millions of holders, compute
total_holdersper 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
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):
- 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.
Related
Token Holders
The latest-state holder set this time-travels.
Token Balances by Wallet
Current balances per wallet, the latest-state sibling.

