Question it answers
“Who holds token 0x…, with their current balance and USD value? Give me the full holder list, biggest first.”Mirrors Moralis
GET /erc20/{address}/owners. Storing the holder set pre-aggregated, keyed by token, in your own database is the value, no per-request rebuild from raw transfers.
What you get
Token transfers carry the absolute post-transfer balance of both sides of every transfer (fromPostBalance / toPostBalance). Each transfer becomes two per-wallet balance observations, and the latest observation per (token, wallet) is the current holding, no running-sum reconstruction. Each observation is USD-enriched in-block from the same block’s price updates.
Balances are latest-wins: the current holding of a
(token, wallet) is simply the observation with the highest (block_number, log_index).
Source
The transform reads one per-block array and unpivots it:tokenTransfers
Each transfer yields (from_address, from_post_balance) and (to_address, to_post_balance). The EVM zero address (mint/burn counterparty) and any side without a resolved post-balance are skipped. USD spot prices are folded in inline from the same block’s tokenPriceUpdates (reversed so the chronologically-last update wins per token), no separate price join at read time.
This recipe is Token Balances by Token plus the in-block USD valuation leg.
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 so the aggregate sees only canonical state. The fact table’s sort key is token-first, so a token’s holder set is a contiguous range read.
Postgres and MySQL keep an append-only observations table and derive the current-holder projection: Postgres as a DISTINCT ON (token, wallet) … ORDER BY block_number DESC, log_index DESC materialized view (refresh on a schedule), MySQL via an AFTER INSERT latest-wins upsert trigger with a periodic DELETE … WHERE balance = 0 cleanup.
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 (they exceed numeric precision); usd_price is a wide decimal so a low-decimals token × price never overflows.
ClickHouse, fact_token_holders
ClickHouse, fact_token_holders
sign column drives reorg collapsing, read with FINAL (then argMax) or a sign-aware aggregate, never a bare WHERE sign = 1. leg keeps the two unpivoted rows of one transfer distinct; the +1/-1 reorg pair for one leg shares a key and collapses. A single-node setup can use CollapsingMergeTree(sign) without the replication path.Postgres, token_holders
Postgres, token_holders
DECIMAL(76,0) / DECIMAL(38,18), and replaces the materialized view with a trigger-maintained token_holders table. position is the block-level cursor used during backfill. balance is typed NUMERIC(76, 0) so the raw uint256 survives without overflow.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.tokenHolders 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 from the token’s first block for a complete holder set. A holder list is current state, the latest transfer-derived balance per
(token, wallet), so to return every holder you need the token’s full transfer history. The good news: that’s history since the token launched, not chain genesis, so set MORALIS_HISTORICAL_FROM_BLOCK to around the token’s deployment block. A recent-only window would miss holders who haven’t moved the token lately. See History & backfill.Example reads
All current non-zero holders of a token with balance and best-effortusd_value, FINAL collapses reorg ±1 pairs before argMax (ClickHouse). The example assumes 18 decimals; substitute the token’s real decimals to be exact:
Other ways to consume this data
The sink is the way to get this pre-aggregated table (token_holders), recommended for a holder list you own. The same underlying data is also available as the raw decoded feed, if you’d rather maintain holder state 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 the holder set yourself, not this pre-aggregated 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 Postgres materialized view and the MySQL trigger state are not reorg-aware on their own, under realtime you would have to re-derive or refresh them. Run realtime/hybrid on ClickHouse, where the collapsing log table corrects reorgs per-block via
sign automatically; 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. On Solana, the event identity already includes (from, to, token, amount) so each observation stays row-unique despite Solana’s repeated logIndex within an instruction; the holder set it produces is identical in shape.
How much data does this consume?
A continuous real-time feed for this recipe’s exact selection,tokenTransfers plus the inline tokenPriceUpdates mark, measured:
Measured 2026-07-31, 30 sampled partitions per chain,
metered delivery. Billing is on uncompressed bytes.
Size your allowance against the busy figure, not the typical one. Chain activity is bursty, and the same feed can land on either side of a fixed allowance depending on the month. Three caveats:
- Real-time only. A historical backfill is billed separately and is usually the larger number; its size depends entirely on how far back you go.
- Measured chains only. Volume concentrates hard per chain (BNB Smart Chain measured over 10x Ethereum on this exact selection), so these figures do not extrapolate to unmeasured chains.
- Filtering does not reduce it. Restricting to specific tokens or holders happens after the data is delivered. To reduce cost, narrow the block range, not the entity set.
Fidelity gaps
The Moralis/erc20/{address}/owners response has fields that aren’t derivable from normalized per-block data and are therefore omitted or approximated:
balance_formatted: needs the token’sdecimals, which is contract metadata not present intokenTransfers. Only the rawbalance(uint256 string) is emitted; divide by10^decimalsoff-stream to format. Source decimals from a Token Metadata sync.usd_value: emitted best-effort assuming 18 decimals (balance / 1e18 × usd_price). The faithful per-token USD spotusd_price(same-block, newest-wins) is stored alongside, so a consumer that knows the real decimals can recompute exactly:usd_value = balance / 10^decimals × usd_price. Tokens with no same-block price update getusd_price = 0(→usd_value = 0) for that observation.percentage_relative_to_total_supplyandtotal_supply, there’s no clean circulating/total-supply figure in the block stream (it would require summing all mints − burns since genesis or a contracttotalSupply()read). Omitted.owner_address_label,is_contract,entity,entity_logo, off-chain enrichment / address-classification metadata, not on-chain block data. Omitted.
token_address → holder wallet_address + current balance, is fully covered, so these gaps don’t block the recipe.
Migrating from the REST API
This recipe replaces the token-holder family of REST endpoints, the EVM Web3 API holder list and summary, and the deprecated Solana Token API holder endpoints. Data Feeds are not 1:1 replicas of those responses: you land the holder set in your own database and reconstruct each response shape with SQL. 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 or a definition you set), and the gotchas specific to that endpoint.GET /erc20/:address/owners
The per-holder list, every owner, biggest first, with balance and USD value. The most direct migration on this page: this recipe’stoken_holders table is the holder list, and the ranking becomes your ORDER BY.
For balance_formatted, percentage_relative_to_total_supply, and the envelope totalSupply, add Token Metadata (token_metadata) to the same database, the raw list needs only this recipe, the formatted fields need the join.
The reconstruction query (Postgres, adapt to your destination):
- Ordering and filtering are yours: the endpoint hard-coded biggest-first and non-zero-only; with your own table you rank by
balanceorusd_value, set any minimum, and page with plain SQL, no per-call limit. - The stored
usd_valueassumes 18 decimals, recompute asbalance / 10^decimals × usd_pricewith real decimals fromtoken_metadatafor non-18-decimal tokens (see fidelity gaps). - USD values come from real DEX trades, very close to the old endpoint, compare with a tolerance; a thinly-traded token may value at 0.
- Backfill from the token’s deployment block (see Run it) or the list silently misses holders who haven’t moved the token lately.
GET /erc20/:tokenAddress/holders
The aggregate holder summary,totalHolders, supply concentration, size tiers, acquisition split, and trend windows, not the per-holder list. There is no single replacement table: you compute the summary yourself over this recipe’s holder set, which turns the black-box buckets into definitions you control.
The headline numbers in one pass (Postgres, tier thresholds are illustrative, set your own):
- The foundation is exact, the analytics are yours: holder count and concentration are deterministic SQL; the tier boundaries and acquisition rules are conventions you define, once fixed, the counts are exact.
- Materialize for large tokens, USDC has millions of holders; compute the summary on a schedule into its own table, not per request.
- Full-history backfill matters even more here: start late and you undercount holders and skew every aggregate, including the 30-day trend and the first-acquisition split.
- Decimals matter for USD tiering, recompute
usd_valuewith realdecimalsbefore bucketing non-18-decimal tokens.
Solana: GET /token/:network/holders/:address
The Solana holder-metrics summary, holder count and size distribution for an SPL token. Replaced by this recipe withchain set to Solana: tokenHolders maintains one current balance per (mint, owner) and you aggregate the metrics in SQL. The field mapping matches the /erc20/:tokenAddress/holders table above, totalHolders is exact, holderDistribution and holdersByAcquisition are definitions you set (the acquisition split again needs Token Transfers), and holderChange is calculated from a count series you maintain.
Total plus distribution tiers (Postgres, swap in your own thresholds, by usd_value or raw balance):
- Don’t lowercase Solana addresses: EVM addresses in this recipe are stored lowercased, but base58 mints and owners are case-sensitive, match them as-is.
- SPL account model: the decoded feed resolves transfers to owner wallets, so
wallet_addressis the owner; one owner can hold across multiple token accounts, sum them for a per-owner total. - Backfill from around the mint’s deployment or the count silently undershoots.
Solana: GET /token/:network/:address/top-holders
The ranked per-holder list for an SPL token, the Solana twin of/erc20/:address/owners, and the same direct migration: this recipe on Solana plus ORDER BY balance DESC. The field mapping matches the /erc20/:address/owners table above with camelCase names (ownerAddress, balanceFormatted, usdValue, percentageRelativeToTotalSupply); isContract (is the owner a program-owned account) is add yourself from your own account-type source. Formatted balance and supply percentage again need Token Metadata.
- Sunsetting July 31, 2026 along with the holder-metrics endpoint above, plan the cutover now.
- SPL decimals vary per token, format against real
decimalsfromtoken_metadata, never a fixed 18; the stored best-effortusd_valueassumes 18. - Same base58 case-sensitivity and owner-vs-token-account notes as above.
- USD value comes from real trades, compare with a tolerance; a thinly-traded token may value at 0.
Related
Token Transfers
The per-token movement ledger this holder set is derived from.
Token Analytics
The use case holder distribution and concentration power.

