Question it answers
“Give me every token wallet 0x… holds, with its current non-zero balance.”This is the by-wallet portfolio read. It’s the same ingest as Token Balances by Token, sorted the other way: this recipe keys wallet-first so one wallet’s full token holdings are a contiguous range read, while the sibling keys token-first to list a token’s holders.
What you get
The recipe lands one balance observation per wallet, per transfer leg. Eachtoken_transfer carries absolute post-transfer balances (from_post_balance / to_post_balance), so every transfer becomes two observations: one for the sender, one for the receiver. The latest observation per (wallet, token) is the current balance (“latest-wins”).
The zero address is excluded, so a token’s balance going to
0 on a full transfer-out is a real observation you read as the current state, not a missing row.
Source
The transform reads one per-block array,tokenTransfers, and unpivots each transfer into two per-wallet observations: (from_address, from_post_balance) and (to_address, to_post_balance). Because the source supplies absolute post-balances on each transfer, there’s no running-sum reconstruction: the latest observation is the balance.
Destination
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 holdings are a contiguous range read; you collapse to the current balance per token with
argMax over FINAL.
Full schema
Below is the complete read table this recipe produces. It’s a starting point: keep the columns you need and drop the rest (see Schema & flexibility). Rawuint256 balances are stored as text (ClickHouse) or NUMERIC(76, 0) (Postgres) so they never overflow.
ClickHouse, fact_balances_by_wallet
ClickHouse, fact_balances_by_wallet
sign column drives reorg collapsing; 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.Postgres, token_balances
Postgres, token_balances
REFRESH MATERIALIZED VIEW CONCURRENTLY token_balances;. MySQL is the same shape with a trigger-maintained token_balances table and a DELETE … WHERE balance = 0 cleanup. 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 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.tokenBalancesByWalletAddress 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 complete balances. A balance is current state (the latest transfer-derived observation per
(wallet, token)), and a wallet can still hold a token it last moved years ago. To return a wallet’s complete holdings, backfill from an early block (MORALIS_HISTORICAL_FROM_BLOCK=0, or a token’s deploy block if you scope to one token). A recent-only window like tip-1000 captures only tokens that moved in that window, so balances would be incomplete. Unlike an event feed, a partial window is not correct here. Full-chain backfill on a busy chain is large; plan for it. See History & backfill.Example reads
Every token a wallet holds, current non-zero balance only (ClickHouse;argMax over FINAL picks the latest observation per token):
REFRESH MATERIALIZED VIEW CONCURRENTLY token_balances;):
Other ways to consume this data
The sink is the way to get this pre-shaped table (token_balances), recommended for a portfolio lookup you own. The same underlying data is also available as the raw decoded feed, if you’d rather derive balances 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, which carries absolute post-transfer balances) to compute balances yourself, not this pre-computed table. 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 backfill cursor (
position) is block-level, so realtime/hybrid on Postgres / MySQL is constrained by their single-column UNIQUE requirement. Run realtime/hybrid on ClickHouse; the Postgres / MySQL configs target historical backfill.Multichain
The recipe is chain-parametrized: point it at any supported EVM chain or Solana. On Solana, the per-observation identity already folds(from, to, token, amount) into vendor_event_id so rows stay unique under Solana’s repeated logIndex; the balances it produces are identical in shape.
Fidelity gaps
- Raw balances only.
balanceis the rawuint256post-transfer amount; divide by10^token_decimalsto read human units. Fold in a decimals lookup from a Token Metadata sync if you need it pre-scaled. - No USD value. This recipe lands quantities, not dollar value. Join to a Token Prices sync at read time for portfolio valuation.
- Latest-wins semantics. A
(wallet, token)row reflects the most recent transfer-derived post-balance. Direct mints/burns or rebases that emit a transfer are captured; balance changes with no transfer event are not.
Migrating from the REST API
This recipe replaces the wallet-balance family of Web3 API endpoints. Data Feeds are not 1:1 replicas of those responses: you land the underlying data in your own database and reconstruct the response shape with a view. The subsections below cover each REST endpoint: what replaces it, the field mapping (exact = straight from the chain, calculated = derived from real DEX trades, very close, add yourself = off-chain signal not in the feed), and the gotchas specific to that endpoint.GET /wallets/:address/tokens
The enriched balances-plus-prices endpoint. Replaced by this recipe’stoken_balances table joined with three sibling recipes for native balance, token details, and prices.
The reconstruction view (Postgres; adapt to your destination):
portfolio_percentage is usd_value / SUM(usd_value) per wallet at query time; UNION in native_balances priced with the native asset’s USD mark; derive 24-hour change from the price ~24h ago in token_price_updates.
Gotchas
- USD values come from real DEX trades: very close to the old endpoint, but compare with a tolerance, not bit-for-bit. A thinly-traded token may not have a price yet.
- No fixed spam filter: you get every token the wallet holds, then apply your own spam/dust rules.
- No wallet-size limit: the old endpoint caps very large wallets; your own dataset has none.
- Balances are current-state on Postgres/MySQL; use ClickHouse for real-time/hybrid.
GET /:address/erc20
The plain ERC-20 balances endpoint (no prices, no portfolio math). Replaced by this recipe’stoken_balances table joined with Token Metadata for name/symbol/decimals/supply: two recipes, one view, every onchain value exact.
The view is the balances-and-prices view above minus the price join: join
token_balances to token_metadata, keep balance > 0, and derive the formatted/supply-relative columns. The old exclude_spam / exclude_unverified_contracts filters become a WHERE against your own list.
Gotchas
- Everything onchain is exact: no pricing engine in the loop, nothing to reconcile with a tolerance.
- ERC-20 only, matching the REST endpoint. For native (ETH) balance, add Native Balances.
- No wallet-size limit: the old endpoint errors on very large wallets (“too many ERC20 token balances”); your dataset doesn’t, and you get real keyset pagination on top.
GET /wallets/:address/net-worth
The USD net-worth roll-up. There is no single replacement table; net worth isSUM(token balance × price) + native balance × native price, computed over the same tables as /wallets/:address/tokens.
The roll-up view (Postgres; sums the balances view above and adds the native leg):
- Curating the token set is the whole game. One spam or bogus-priced token can dominate the total. The old
exclude_spam/exclude_unverified_contracts/min_pair_side_liquidity_usdfilters become your ownWHEREin thetokensCTE; here they’re essential, not polish. - Stale prices: a token that hasn’t traded recently has no fresh mark. Decide whether to value it at zero, carry forward the last mark, or exclude it. Each choice moves the total.
- Cross-chain net worth: each chain is its own feed. Compute per chain and sum
networth_usdintototal_networth_usd. - Compare with a tolerance: USD comes from real DEX trades, very close to the old figure, not identical.
- A missed holding silently changes the total, so full-history backfill (see the note in Run it) matters even more here.
Related
Token Balances by Token
The sibling: same ingest, keyed token-first to list a token’s holders.
Portfolio Tracking
The use case this balance feed powers.

