Balances & Holders Cubes
Balances and Holders are derived views that answer "what is true now", so there is no
underlying event to push. A subscription against either is a valid document and the socket
stays open, but no message is ever delivered — on EVM or Tron.
For live balances, read the balance once and then keep it current from a stream that does
fire: EVM.TransactionBalances or EVM.Transfers (and Tron.Transfers on Tron). See
which cubes support subscriptions.
Two cubes answer balance questions, and picking the right one matters more than the fields you select:
| Cube | Question it answers | Shape |
|---|---|---|
Balances | What does this address hold? | Current balance per address, live — no snapshot date |
Holders | Who holds this token? | A token's holders ranked by balance, as a dated snapshot |
Both supersede the older BalanceUpdates approach of summing deltas yourself. They read from aggregate-state tables (balances_by_address / balances_by_currency), so they return the current state directly instead of replaying every change.
Balances and Holders exist on EVM networks (EVM(network: …)) and Tron (Tron).
They do not exist on Solana. Solana balance changes are queried with Solana.BalanceUpdates and Solana.InstructionBalanceUpdates, which are the current APIs there — not deprecated.
Balances — what an address holds
Balances returns one row per address/currency pair with the current amount.
Balance fields: Address, Amount, AmountInUSD, UpdateCount, FirstChangeTime, LastChangeTime.
Its where filter accepts Balance.Address plus Currency and Block. Note there is no amount filter on Balances — see Holders if you need to filter or rank by balance size.
Token balance for one or more addresses
query TokenBalances($addresses: [String!], $token: String!) {
EVM(network: eth, dataset: combined) {
Balances(
where: {
Balance: { Address: { in: $addresses } }
Currency: { SmartContract: { is: $token } }
}
orderBy: { descending: Balance_Amount }
) {
Balance {
Address
Amount
AmountInUSD
UpdateCount
LastChangeTime
}
Currency {
Symbol
Name
SmartContract
}
}
}
}
{
"addresses": [
"0x28C6c06298d514Db089934071355E5743bf21d60",
"0x21a31Ee1afC51d94C2eFcCAa2092aD1028285549"
],
"token": "0xdac17f958d2ee523a2206206994597c13d831ec7"
}
To exclude zero balances, apply the filter on the field rather than in where:
Amount(selectWhere: { gt: "0" }).
Native balance
Filter on Currency: { Native: true } to get the chain's native asset instead of a token:
{
EVM(network: eth, dataset: combined) {
Balances(
where: {
Balance: { Address: { is: "0x28C6c06298d514Db089934071355E5743bf21d60" } }
Currency: { Native: true }
}
) {
Balance {
Address
Amount
AmountInUSD
}
Currency {
Symbol
Native
}
}
}
}
The same query works on Tron by swapping the selector for Tron and using a Tron address.
Classifying an address from its balance metadata
UpdateCount with LastChangeTime separates wallet types without any labelling data:
- Very high
UpdateCount,LastChangeTimeseconds ago — exchange hot wallet or payment processor; the balance churns continuously. - Single-digit
UpdateCounton a large balance — cold storage, treasury or custody. Funded once, rarely touched.
FirstChangeTime is not the address's first-ever activityFirstChangeTime reflects the earliest change within the balances table's retention window, not the first time the address ever moved funds. For true first-activity, query Transfers with an ascending time order instead.
Holders — who holds a token
Holders lists a token's holders, ranked. It takes a date argument for the snapshot day, and unlike Balances it does accept a balance filter — which is what makes large tokens tractable.
query TokenHolders($token: String!, $floor: String!, $date: String!) {
EVM(network: eth) {
Holders(
limit: { count: 100 }
orderBy: { descending: Balance_Amount }
date: $date
where: {
Currency: { SmartContract: { is: $token } }
Balance: { Amount: { ge: $floor } }
}
) {
Holder {
Address
}
Balance {
Amount
}
Currency {
Symbol
Name
}
}
}
}
{
"token": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
"floor": "1000000",
"date": "2026-07-29"
}
An unbounded top-N ranking across every holder of a very large token times out server-side — this is a dataset-size limit, not a syntax error. Ethereum USDT times out even with a 50,000,000 floor; Tron USDT behaves the same way.
The fix is a Balance: { Amount: { ge: … } } floor high enough to shrink the working set. For a complete holder distribution rather than the top of it, use Bitquery Cloud exports or Kafka streams rather than a synchronous GraphQL query.
Choosing a dataset
| Dataset | When to use |
|---|---|
combined | Latest balances. Queries realtime and archive and merges the results. |
archive | Historical snapshots, and balances for addresses that have not been active recently. |
realtime | Recent state only. Some aggregates are unavailable here — if you see "no table can query … consider use archive dataset", switch to combined or archive. |
Migrating from BalanceUpdates
If you previously summed balance deltas, the translation is mechanical:
Old (BalanceUpdates) | New |
|---|---|
BalanceUpdates(where: {BalanceUpdate: {Address: {is: …}}}) | Balances(where: {Balance: {Address: {is: …}}}) |
balance: sum(of: BalanceUpdate_Amount, selectWhere: {gt: "0"}) | Balance { Amount(selectWhere: {gt: "0"}) } |
BalanceUpdate { Address } | Balance { Address } |
BalanceUpdate { AmountInUSD } | Balance { AmountInUSD } |
Top holders via sum + orderBy: {descendingByField: "balance"} | Holders(date: …, orderBy: {descending: Balance_Amount}) |
The important conceptual change: you no longer aggregate. Balances already holds the summed state, so a sum(of: …) over balance updates becomes a plain field read.
EVM.BalanceUpdates, EVM.TokenHolders and Tron.BalanceUpdates sunset on 10 August 2026. They still return live data today, so nothing has broken yet — but anything still calling them stops working on that date.
EVM.TokenHolders has already been withdrawn ahead of the others and now returns no table can query TokenHolder.
Per-change history becomes daily aggregates
This is the deliberate design change, not a missing feature. BalanceUpdates gave you one row
per change. Balances gives you one row per address per day, exposed as Block.Date.
For most balance questions the daily grain is what you actually wanted, and it is far cheaper: a 30-day balance history is one query returning 30 rows, rather than a scan over every change in that period which you then aggregate yourself.
query DailyBalanceHistory {
EVM(network: eth, dataset: archive) {
Balances(
where: { Balance: { Address: { is: "0x28c6c06298d514db089934071355e5743bf21d60" } } }
orderBy: { descending: Block_Date }
limit: { count: 30 }
) {
Block {
Date
}
Balance {
Amount
AmountInUSD
}
Currency {
Symbol
}
}
}
}
Always order by Block_Date. Without it you get an arbitrary day and the query still succeeds,
which makes the mistake silent.
| You need | Use |
|---|---|
| Current balance per address | Balances (latest row) |
| Balance on a past date, or a daily series | Balances with Block.Date — or Holders(date: …), which agrees exactly |
| A token's holders, ranked | Holders |
| When a position first or last moved | Balances — FirstChangeTime, LastChangeTime, UpdateCount |
| The individual transfers behind a change | Transfers |
What genuinely does not carry over is sub-daily change attribution. BalanceUpdates exposed
Type (transfer, fee, block_reward, …) per change; the daily aggregate has no equivalent.
If you need to know why a balance moved rather than what it became, reconstruct it from
Transfers plus transaction context.
Related
- Address Labels API — identify which of those addresses are exchanges, contracts, or known entities
- Balance Updates cube — per-change history and change attribution
- Transfers cube — the transfers that drive most balance changes
- Token Holders API (Ethereum) — worked
EVM.Holdersexamples - TRC20 USDT API —
Tron.BalancesandTron.Holdersin practice