Wallet token balances API: list ERC-20 assets for an address
List a wallet's non-zero ERC-20 token balances with one GET /addresses/{address}/balances request, then add transfer history and token metadata.
One request to GET /{chain}/addresses/{address}/balances returns a wallet's non-zero ERC-20 token balances; add the Token Transfers API for history. Developers and AI agents use the same authenticated requests, and the Token Balances API page lists the fields.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances?limit=50" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"Before querying, read GET /v1/status and check the selected chain's data_features and data_status; balance coverage varies by chain. Request parameters and response schemas are in the Data API reference.
Tasks this guide helps you complete
- Read wallet token balances with a key and paginate non-zero ERC-20 holdings.
- Read wallet transfer history within a fixed block window and follow cursors for the selected address.
- Fill in token metadata to display names and symbols alongside raw integer balances, preserving missing fields.
The three kinds of data a wallet assets page needs
A wallet assets page can show an address's ERC-20 token balances, token transfer history and token metadata. The Data API provides an endpoint for each:
- Balances:
GET /{chain}/addresses/{address}/balancesreturns the address's non-zero ERC-20 balances, ordered bytokenaddress ascending, with tokensymbolanddecimalsincluded where available. An address with no balances returns200withdata: []. - Transfers:
GET /{chain}/addresses/{address}/transfersreturns token transfers involving the address within a required block window, ordered by(block_number, log_index)descending. - Token metadata:
GET /{chain}/tokens/{token}reads one token's name, symbol, decimals, and total supply by contract address;POST /{chain}/tokens:batchreads the same metadata for up to 100 addresses in one request.
All three use https://api.blockvectra.com/v1/data as the base URL and the x-api-key request header, with robinhood_mainnet as the example chain. They belong to the balances, transfers, and token_metadata capabilities respectively; for chains that offer each capability, see the Supported Chains page. On a chain without the capability, the endpoint returns 422 no_coverage.
Request 1: address balances
This endpoint takes fewer parameters, which makes it a good first request for a page:
{chain}(path parameter, required): chain identifier, thechainvalue of an entry inGET /chains(for examplerobinhood_mainnet). Matching is exact and case-sensitive; aliases and numeric chain IDs are not accepted.{address}(path parameter, required): 20-byte address; the0xprefix is optional and either case is accepted.limit(query parameter, optional): page size. Defaults to 50; values above 500 are clamped to 500;0or a non-integer returns400 bad_request.cursor(query parameter, optional): the previous response'snext_cursor, passed back unchanged to fetch the next page. A cursor is valid only for the chain, endpoint, and query parameters that issued it; reusing it elsewhere returns400 bad_request.
It is keyset-paginated: next_cursor appears only when there is another page. On the last page the key is absent entirely, never null.
export BLOCKVECTRA_API_KEY=rgw_your_api_key
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances?limit=50" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"The response envelope is AddressBalanceListEnvelope, containing data and meta. Each item of data is an AddressBalance:
| Field | Type | Description |
|---|---|---|
token | string (address) | Token contract address; canonical form is 0x plus 40 lowercase hex digits. |
balance | string (decimal) | Raw integer balance, which may exceed 2^53, returned as a plain decimal string — never a JSON number, scientific notation, or hex. |
symbol | string or null | Token symbol, or null when unavailable. |
decimals | integer or null | Token decimals, 0–255, or null when unavailable. |
Request 2: address transfers
The transfers endpoint requires an explicit block window: from_block and to_block are both required and must satisfy from_block <= to_block. It takes a few more parameters:
standard(query parameter, required):erc20orerc721. Address-scoped queries do not covererc1155; passing it returns422 no_coverage.direction(query parameter, optional):in,out, orany; defaults toanyand filters by direction relative to the address.token(query parameter, optional): restrict results to one token contract.clamp(query parameter, optional): only the literal stringtrueenables it; any other value is treated asfalse.
Window bounds and finality: an explicit to_block above as_of_block returns 409 not_indexed_yet unless clamp=true truncates it down to as_of_block; a window wider than the chain's limit (limits.max_window_blocks from GET /chains) returns 409 window_too_large unless clamp=true truncates from the older end (raising from_block and keeping to_block fixed). If from_block itself is already past as_of_block, it stays a hard 409 even with clamp=true. When the window is clamped or partially covered, the response meta.coverage is "partial"; otherwise it is "full".
In the transfer records, ERC-20 items add amount; ERC-721 items add token_id. Both include token, standard, from, to, block_number, block_timestamp, tx_hash, tx_index, and log_index.
export BLOCKVECTRA_API_KEY=rgw_your_api_key
# 1) Read as_of_block from the balances response.
AS_OF_BLOCK=$(curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" | grep -o '"as_of_block":[0-9]*' | cut -d: -f2)
# 2) clamp=true truncates a too-wide window, or a to_block above as_of_block, instead of returning 409.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=$AS_OF_BLOCK&direction=any&clamp=true" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"Paginating through all transfers
The address transfers endpoint's next_cursor is optimistic: it appears only when the page returned exactly limit rows, so a page can carry a next_cursor and still turn out to be the last page. Do not stop when a page is empty; follow next_cursor until the key is absent.
The code below fetches every transfer in the window:
const address = "0x1111111111111111111111111111111111111111";
const head = await fetch(
`https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
{ headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
const asOfBlock = head.meta.as_of_block;
const transfers: unknown[] = [];
let cursor: string | undefined;
do {
const url = new URL(
`https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
);
url.searchParams.set("standard", "erc20");
url.searchParams.set("from_block", "0");
url.searchParams.set("to_block", String(asOfBlock));
url.searchParams.set("limit", "500");
// clamp truncates from the older end
url.searchParams.set("clamp", "true");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
});
const page = await res.json();
transfers.push(...page.data);
cursor = page.next_cursor; // absent on the last page
} while (cursor);Request 3: token metadata and tokens:batch
Read a single token with GET /{chain}/tokens/{token}; the path takes only {chain} and {token}, with no pagination. The response envelope is TokenEnvelope, and data is a Token:
| Field | Type | Description |
|---|---|---|
address | string (address) | Token contract address. |
standard | string | erc20, erc721, or unknown. |
name | string or null | Token name, or null when unavailable. |
symbol | string or null | Token symbol, or null when unavailable. |
decimals | integer or null | Token decimals, 0–255, or null when unavailable. |
total_supply | string or null | Raw total supply; the API does not apply decimals scaling. null when unavailable. |
first_seen_block | integer (int64) | Block height where the token was first seen. |
metadata_updated_at | string (timestamp) | UTC time when the metadata was last updated. |
metadata_block | integer (int64) | Block height at which the metadata was read. |
metadata_status | string | ok, partial, or unavailable. |
metadata_issues | object | Per-field issue records keyed by name, symbol, decimals, total_supply, with values reverted, no_data, invalid_encoding, or temporarily_unavailable. |
An unknown or not-public {chain} returns HTTP 404 with error.code: not_found and error.data.reason: unknown_chain before token or request-body validation. On a served chain, a {token} that is not a valid 20-byte address returns 400 bad_request, and an unknown {token} returns 404 not_found.
The balances endpoint already includes symbol and decimals where available, but both can be null. To fill in the name and decimals for every token in a wallet, use POST /{chain}/tokens:batch:
- The request body is
{"addresses": [...]}with at most 100 addresses per request; more than 100 entries, or an entry that is not a valid 20-byte address, returns400 bad_request(it fails on the first invalid address it walks to). - Addresses that are not found do not trigger an error; they are listed in
data.missing, whiledata.tokenscontains only the tokens whose metadata was found. - Duplicate addresses are deduplicated in both
tokensandmissing, each in first-occurrence request order.
export BLOCKVECTRA_API_KEY=rgw_your_api_key
# Single token
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"
# Batch: up to 100 addresses per request
curl -s -X POST "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"addresses":["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"]}'Scaling amounts by decimals
The balance field balance and the ERC-20 transfer field amount are raw integers rendered as decimal strings (UInt256String); a token's total_supply is also a raw on-chain integer with no decimals scaling applied. To show a human-readable quantity, divide by 10 ** decimals for that token.
decimalscomes from the balance item's ownsymbol/decimals, or fromGET /{chain}/tokens/{token}andPOST /{chain}/tokens:batch; it can benull.- These values may exceed
2^53, so do not do the arithmetic with a JSON number: useBigIntin TypeScript andintin Python, parsing the decimal string as-is to avoid precision loss.
function toDisplayAmount(raw: string, decimals: number | null): string {
if (decimals === null) return raw; // no decimals metadata: keep the raw integer
const value = BigInt(raw);
const base = 10n ** BigInt(decimals);
const whole = value / base;
const fraction = (value % base)
.toString()
.padStart(decimals, "0")
.replace(/0+$/, "");
return fraction ? `${whole}.${fraction}` : whole.toString();
}
// balance.balance is a raw decimal string; decimals comes from the same item or tokens:batch.
const display = toDisplayAmount(balance.balance, balance.decimals);Data freshness
Every chain-scoped success response carries meta:
as_of_block: the chain's newest fully written block. Block-scoped endpoints serve data up to this height.safe_block: a marker indicating the node's consensussafeblock tag (nullwhile unknown). Never belowfinalized_block, and does not truncate, reject, or delay responses.finalized_block: a marker indicating the node's consensusfinalizedblock tag (nullwhile unknown). It does not truncate, reject, or delay responses; clients decide what safety they need from the marker (such as confirmation status).coverage:"full"or"partial". Address transfers and similar endpoints report"partial"whenclampnarrowed the served window, or when the window starts before the chain's first indexed block.refreshed_at: when the data behind the response was last updated (UTC). May benull:nullmeans the update time of the data is unknown and it should be treated as stale; block-based endpoints always return a value.- It also repeats
chain,chain_slug, andchain_external_id.
A common pattern: read meta.as_of_block from any first response to read up to the newest indexed block, and check meta.safe_block / meta.finalized_block if you want to display confirmed status.
CU estimate for one page load
Every method is billed by its CU weight, read from the platform plans API:
CU weight per call
| Method | CU per call |
|---|---|
data.address_balances | 25 |
data.address_transfers | 25 |
data.tokens_batch | 10 |
One page load (estimated)
1 balances request + 3 transfer pages + 1 tokens:batch request(s), 5 calls total, about 110 CU. Actual usage depends on the number of pages and tokens.
For billing decisions and non-billed error responses, see billing rules. If what you need is not indexed transfer history but logs from the most recent blocks, read Recent node data vs indexed history first before deciding whether to switch to eth_getLogs.
Next steps
- Browse the datasets directory to see every dataset BlockVectra indexes.
- See the free plan and pricing to check what your account includes.
- Log in to the console to create an API key.
Last updated:
Choose an RPC provider
Evaluate RPC method costs, log ranges, free credits, rate limits, chain coverage, push and data APIs, authentication and Agent access with workload self-tests.
Transaction traces
Reconstruct execution call trees for a transaction: the JSON-RPC debug_traceTransaction method with its allowed tracers and guards, and the Data API getTransactionTrace and getBlockTraces endpoints with their coverage bounds.