facts_asset_tokens.balance_changes_daily records one row for each day, each deployment and each holding account whose balance moved, with that day’s net change, in whole token units.
Balances themselves are not stored. The balance functions add these rows up at query time.
Sparse, so a balance is an accumulation
A day with no row means the balance did not move that day, not that it was zero. So a holder’s balance on a date is the sum of every row at or before that date, never a lookup of that date. Ask for one day in isolation and you get that day’s movement, which is rarely the question. The functions exist so this is not something every reader has to get right.Read the coverage and attribution before the numbers
One thing gates a cap table: whether a ledger summed from transfer legs can describe the deployment at all. Coverage answers that per deployment, and it answers about the mechanism rather than about activity. So a deployment with no rows here is one of two things, and they are not the same. Either coverage refuses it, in which case it names the reason, or coverage serves it and the deployment has simply never been transferred. An asset issued but never moved has an empty cap table, which is the correct answer rather than a missing one. The curated functions read coverage and refuse with that reason instead of returning a figure they cannot stand behind.Tables
- Balance changes
facts_asset_tokens.balance_changes_daily records one row per (timestamp, chain_id, token_address, account_address).| Column | Type | Description |
|---|---|---|
timestamp | TIMESTAMP | Day the balance moved. The partition column. |
chain_id | STRING | Chain the deployment sits on. |
asset_id | STRING | Asset the deployment belongs to. |
token_id | STRING | Key of the deployment, formatted {token_address}-{chain_id}. |
token_address | STRING | Contract address of the deployment. Filter this and chain_id together. |
account_address | STRING | The chain’s native holding account: a wallet on EVM chains, a token account on Solana. |
account_owner | STRING | The holder. Equal to account_address where the chain does not distinguish them, and the wallet behind the token account where it does. Group by this for a cap table. Group by account_address instead and one wallet’s several Solana token accounts each count as a separate holder. |
balance_delta | BIGNUMERIC | Net balance movement that day, signed, in whole token units. |
What is not here, and why
The families currently declared out, and absent from this table rather than approximated:- Chains without a balance arm. Every asset on them, whatever its mechanics.
- Assets whose balances revalue without moving. A rebasing token grows every holder at once with no transfer to observe, so a ledger summed from transfer legs holds the value each transfer had and never revalues it.
- Assets hand-declared out with measured evidence. Two cases are worth knowing about generally: a chain that migrated balances between token standards without emitting the credit, and a token whose confidential transfers hide the amount from the event that would record it.
Notes
- Filter
token_addressandchain_idtogether. They are the cluster keys; a filter ontoken_idalone reaches neither. balance_deltaisBIGNUMERICthroughout, deliberately. Float arithmetic leaves dust where a closed position should read exactly zero, and dust counts as a holder.