List the holders of a token, largest first
/v1/chains/{chain}/tokens/{token}/holdersCall this for the holder table of a token page. Each item in data.items is a holder's address, its balance and its share of the total supply (null when the supply is 0). Pass next_cursor back as cursor for the next page. It is null on the last page, and a cursor belongs to the chain and token it came from. holder_count is the number of holders the source reports.
- Ethereum: the list comes from Blockscout (
source: blockscout, coveragetoken_holders_ranked_by_provider), withis_contractand a name tag or contract name aslabel.indexisnull. - Monad and Arc: the list comes from Sona's own index of the token's transfers (
source: sona_index), andis_contractandlabelarenull. The first request for a token starts building its index, and the answer is 200 with what is indexed so far andindex.statequeuedorindexing(coveragetoken_index_partial), so poll again. Areadyindex is followed within about a minute of the chain head. When the index cannot take the token the state isunavailableand the list is empty. Inactivitymode (coveragetoken_holders_from_activity) the list holds the addresses that moved the token sinceindex.start_block, because a full history of a busy token is out of reach.
Sona keeps a holder list for 5 minutes, and an answer whose index is still queued or indexing for 15 seconds, so the progress shows. So is an answer whose coverage holds token_source_unavailable. The token is a 0x address of 40 hex digits in any case, which Sona lowercases. limit is a whole number, from 1 to 100 (default 25) as the data source decides, and an out-of-range value answers 400 invalid_query.
Path parameters
| Name | Type | Description |
|---|---|---|
chainRequired | string | The chain, as a CAIP-2 chain ID. Ethereum, |
tokenRequired | string | The token contract: |
Query parameters
| Name | Type | Description |
|---|---|---|
limit | integerDefault 25 | How many items to return, from 1 to 100. Default: 25. Sona passes the value on and the data source judges the range, so a number out of range answers 400 |
cursor | string | The |
Response
The answer, wrapped in the {data, meta, links} envelope. meta.finality is none, because the record is not about one block.
| Field | Type | Description |
|---|---|---|
| chain | string | A chain ID in the CAIP-2 format |
| coverage | array of strings | The codes of the limits that apply to this answer, in a fixed order. It is empty when none applies. |
| holder_count | integer | null | |
| index | object | null | The Monad index of the token, null on Ethereum. |
| items | array of objects | |
| kind | string | token_holders |
| next_cursor | string | null | The cursor of the next page, null on the last page. |
| source | string | blockscoutsona_index |
| token | string |
Cost
1 credit. The time limit is 5 seconds.
Errors
| Status | Code |
|---|---|
| 400 | invalid_idinvalid_query |
| 404 | unsupported_chainnot_found |
| 429 | rate_limitedcredits_exhausted |
| 500 | internal |
| 503 | upstream_unavailable |
| 504 | deadline_exceeded |
400 invalid_id for a chain that is no CAIP-2 id or a token that is not 0x and 40 hex digits (native included), 400 invalid_query for a limit out of range, a cursor the data source did not make or another query key, 404 unsupported_chain for a chain other than Ethereum, Monad and Arc, 404 not_found when the address is not an ERC-20 token (it answers decimals() and totalSupply() or it is none), 429 rate_limited or credits_exhausted, 503 upstream_unavailable with retry_after_s when a data source the route needs does not answer, and 504 deadline_exceeded when the budget runs out.