Wallet API
The Wallet API answers wallet questions over REST: who holds a token, what an address holds and what it is worth in USD, and which addresses traded in a pool. Per-position PnL and average cost are coming soon.
The Wallet API and the On-chain DEX API are the two products of the Crypto Data API. They share everything except the product page:
- One key. The key of your On-chain DEX API endpoint calls both. There is no separate Wallet API endpoint or key.
- One quota and rate limit. Wallet API calls count against the same Crypto Data API quota, separate from RPC Requests.
- One price list. There is no Wallet API price list; Crypto Data API pricing covers both.
https://api.blockreq.com/v1/dexSupported chains: Ethereum (ethereum), Base (base) and Arbitrum (arbitrum). Holder rankings on Base are still being backfilled (November 2026), so every Base token answers 404 on the holders route for now.
Authentication
Create an On-chain DEX API endpoint in the Dashboard (Endpoints → + Add Endpoint) and send its key in the X-API-Key header. Authorization: Bearer <key> works too. Key scope, the RPC-key transition and anonymous access are the same as for the On-chain DEX API: see Get a key.
export BLOCKREQ_DATA_API_KEY="your On-chain DEX API key"Every request in this page counts 1 call. Quota, rate limits, 429 handling and the usage header are described once for both products in Plans and quota and Limits and 429.
You can also try the holders and portfolio routes without a key in the tester on the Wallet API product page.
Routes
| Method and path | What it returns | Status |
|---|---|---|
GET /v1/dex/{chain}/tokens/{address}/holders | The largest holders of a tracked token | Live |
GET /v1/dex/{chain}/accounts/{address}/portfolio | The tokens an address holds, with USD value | Live |
GET /v1/dex/{chain}/pools/{id}/trades | A pool's trades, each with the trader's address | Live |
GET /v1/dex/{chain}/traders/{address}/positions | A trader's positions, average cost, realized and unrealized PnL | Coming soon |
Amounts are strings: balance and other raw amounts are integers in base units (no decimals applied); amount, prices and USD values are decimals. Addresses in responses are lowercase.
Token holders
GET /v1/dex/{chain}/tokens/{address}/holdersThe top limit holders of a token, counted from its ERC-20 Transfers. Burn addresses, Uniswap v4 PoolManagers and the pools holding the token are left out of the ranking and of the total that pct is a share of.
Only tracked tokens have a ranking. An anchor token (stablecoins and majors such as WETH and USDC), a token in no indexed pool, a blocked token, or one that has not caught up with the chain yet answers 404. A newly tracked token is backfilled from the chain's genesis and answers 404 until that is done.
| Parameter | In | Description |
|---|---|---|
chain | path | ethereum, base or arbitrum |
address | path | The token, 0x and 40 hex digits, in any case |
limit | query | Most holders returned, 1–5000 (default 5000) |
Response fields (data):
| Field | Type | Description |
|---|---|---|
chain, address | string | The chain and the token |
symbol, decimals | string, integer / null | Token metadata |
block | integer | Transfers up to this block are counted |
known_total | raw amount | Sum of every balance besides the excluded addresses'; the denominator of pct |
known_total_amount | decimal / null | known_total in whole tokens; null when the decimals are unknown |
total_supply | raw amount / null | totalSupply() read from BlockReq's own node; null when it could not be read |
consistent | boolean / null | Whether the Transfers add up to within 1% of total_supply. When false (rebasing tokens and the like), every pct is null |
excluded | string[] | The burn addresses, PoolManagers and pools left out |
top[] | object[] | Largest balance first: rank (1 for the largest), address, balance (raw), amount (whole tokens), pct (to 6 decimals) |
Examples: the 20 largest ARB holders on Arbitrum.
curl -s "https://api.blockreq.com/v1/dex/arbitrum/tokens/0x912ce59144191c1204e64559fe8253a0e49e6548/holders?limit=20" \
-H "X-API-Key: $BLOCKREQ_DATA_API_KEY"const res = await fetch(
"https://api.blockreq.com/v1/dex/arbitrum/tokens/0x912ce59144191c1204e64559fe8253a0e49e6548/holders?limit=20",
{ headers: { "X-API-Key": process.env.BLOCKREQ_DATA_API_KEY } },
);
const { data } = await res.json();
for (const holder of data.top) console.log(holder.rank, holder.address, holder.pct);import os, requests
res = requests.get(
"https://api.blockreq.com/v1/dex/arbitrum/tokens/0x912ce59144191c1204e64559fe8253a0e49e6548/holders",
params={"limit": 20},
headers={"X-API-Key": os.environ["BLOCKREQ_DATA_API_KEY"]},
timeout=30,
)
for holder in res.json()["data"]["top"]:
print(holder["rank"], holder["address"], holder["pct"])A response with limit=2 (the excluded list is shortened here; for ARB it lists every pool holding the token):
{
"data": {
"chain": "arbitrum",
"address": "0x912ce59144191c1204e64559fe8253a0e49e6548",
"symbol": "ARB",
"decimals": 18,
"block": 522352929,
"known_total": "9982381908048402768748531744",
"known_total_amount": "9982381908.048402768748531744",
"excluded": [
"0x0000000000000000000000000000000000000000",
"0x000000000000000000000000000000000000dead",
"0xdead000000000000000042069420694206942069"
],
"total_supply": "9999998977610261816650915825",
"consistent": true,
"top": [
{
"rank": 1,
"address": "0xf3fc178157fb3c87548baa86f9d24ba38e649b58",
"balance": "2556141991509656323975221470",
"amount": "2556141991.50965632397522147",
"pct": "25.606534"
},
{
"rank": 2,
"address": "0xf7f468b184a48f6ca37eeffe12733ee1c16b6e26",
"balance": "449339761507879669910372383",
"amount": "449339761.507879669910372383",
"pct": "4.501328"
}
]
}
}Address portfolio
GET /v1/dex/{chain}/accounts/{address}/portfolioThe tokens an address holds on the chain, from two sources only:
- the balances the holder rankings store: tracked tokens whose top 5,000 holders include the address (
source: "holders"); balanceOfread from BlockReq's own node for the tokens you list intokens(source: "node"), which replaces the stored balance of those tokens; a zero balance drops the token.
There is no full wallet index: a token that is neither tracked with the address in its top 5,000 nor listed in tokens does not appear. Holdings are sorted by largest USD value, holdings without one last. An address with nothing found answers 200 with an empty holdings list.
| Parameter | In | Description |
|---|---|---|
chain | path | ethereum, base or arbitrum |
address | path | The holder, 0x and 40 hex digits, in any case |
tokens | query | Up to 50 comma-separated ERC-20 addresses (duplicates count once) to read with balanceOf. The native coin's address is refused |
Response fields (data):
| Field | Type | Description |
|---|---|---|
chain, address | string | The chain and the holder |
total_value_usd | decimal | Sum of every value_usd, to 6 decimals; holdings without one are not counted |
holdings[] | object[] | See below |
node_block | integer / null | Block of the node's balanceOf reads; null when none was made |
failed | string[] | Listed tokens whose balanceOf failed; a stored balance is kept for them |
holdings[].token | string | The token |
holdings[].symbol, name | string / null | Token metadata |
holdings[].decimals | integer / null | |
holdings[].balance | raw amount | |
holdings[].amount | decimal / null | The balance in whole tokens; null when the decimals are unknown |
holdings[].price_usd | decimal / null | |
holdings[].value_usd | decimal / null | amount × price_usd, to 6 decimals; null when either is missing |
holdings[].source | holders, node | Stored ranking balance, or balanceOf on BlockReq's node |
holdings[].block | integer | Block the balance is as of |
Examples: an exchange wallet on Ethereum, plus a live USDC balance.
curl -s "https://api.blockreq.com/v1/dex/ethereum/accounts/0x28c6c06298d514db089934071355e5743bf21d60/portfolio?tokens=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" \
-H "X-API-Key: $BLOCKREQ_DATA_API_KEY"const url = new URL("https://api.blockreq.com/v1/dex/ethereum/accounts/0x28c6c06298d514db089934071355e5743bf21d60/portfolio");
url.searchParams.set("tokens", "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
const res = await fetch(url, { headers: { "X-API-Key": process.env.BLOCKREQ_DATA_API_KEY } });
const { data } = await res.json();
console.log(data.total_value_usd, data.holdings.length);import os, requests
res = requests.get(
"https://api.blockreq.com/v1/dex/ethereum/accounts/0x28c6c06298d514db089934071355e5743bf21d60/portfolio",
params={"tokens": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"},
headers={"X-API-Key": os.environ["BLOCKREQ_DATA_API_KEY"]},
timeout=30,
)
data = res.json()["data"]
print(data["total_value_usd"], len(data["holdings"]))A response without tokens (the first 2 of 12 holdings shown):
{
"data": {
"chain": "ethereum",
"address": "0x28c6c06298d514db089934071355e5743bf21d60",
"total_value_usd": "133198.620099",
"holdings": [
{
"token": "0x15d4c048f83bd7e37d49ea4c83a07267ec4203da",
"symbol": "GALA",
"name": "Gala",
"decimals": 8,
"balance": "496270440907221534",
"amount": "4962704409.07221534",
"price_usd": "0.0000268169615577973807",
"value_usd": "133084.653361",
"source": "holders",
"block": 26369822
},
{
"token": "0x03ab458634910aad20ef5f1c8ee96f1d6ac54919",
"symbol": "RAI",
"name": "Rai Reflex Index",
"decimals": 18,
"balance": "33000000000000000000",
"amount": "33",
"price_usd": "3.01662215219692683",
"value_usd": "99.548531",
"source": "holders",
"block": 26369822
}
],
"node_block": null,
"failed": []
}
}Listing tokens on a chain without a node configured, or when the node does not answer, gives 503 unavailable; the message says which. Repeat the request without tokens to get the stored balances.
Pool trades
GET /v1/dex/{chain}/pools/{id}/tradesThe same route the On-chain DEX API documents; for wallet use, each trade carries trader, the transaction's sender. Newest first. A full page comes with next_cursor; pass it as cursor for the next page. Raw trades are kept for 90 days for a listed pool and 30 days for any other pool; older history is in the candles.
| Parameter | In | Description |
|---|---|---|
chain | path | ethereum, base or arbitrum |
id | path | The pool: 0x and 40 (pool contract) or 64 (Uniswap v4 pool key) hex digits |
limit | query | Most trades per page, 1–100 (default 50) |
cursor | query | The next_cursor of the previous page; any other value answers 400 |
Response fields: data[] holds trades with block_number, log_index, time (ISO 8601), tx_hash, trader (address or null), amount0 and amount1 (raw, in the pool's view: positive means the pool received it), price (whole token1 per whole token0), amount_usd, price0_usd and price1_usd (decimal or null). next_cursor is null on the last page. The response also carries the X-Data-Tier header.
The OpenAPI document also defines earliest_available (Unix seconds where the kept window starts). As of 2026-11-08 the production API does not return it yet; do not depend on it until it appears.
curl -s "https://api.blockreq.com/v1/dex/ethereum/pools/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640/trades?limit=20" \
-H "X-API-Key: $BLOCKREQ_DATA_API_KEY"let cursor = null;
do {
const url = new URL("https://api.blockreq.com/v1/dex/ethereum/pools/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640/trades");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { "X-API-Key": process.env.BLOCKREQ_DATA_API_KEY } });
const page = await res.json();
for (const trade of page.data) console.log(trade.time, trade.trader, trade.amount_usd);
cursor = page.next_cursor;
} while (cursor);import os, requests
res = requests.get(
"https://api.blockreq.com/v1/dex/ethereum/pools/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640/trades",
params={"limit": 20},
headers={"X-API-Key": os.environ["BLOCKREQ_DATA_API_KEY"]},
timeout=30,
)
for trade in res.json()["data"]:
print(trade["time"], trade["trader"], trade["amount_usd"])A response with limit=1:
{
"data": [
{
"block_number": 26369898,
"log_index": 55,
"time": "2026-11-08T11:37:11.000Z",
"tx_hash": "0x4a2c1e87ccb499d442452a2ef17abafc4c36c7019768c9940fe748b46768d23a",
"trader": "0x8dbee8f3917049bc30fef01924a7ac79d16cf2b9",
"amount0": "6839954413",
"amount1": "-3056128806869109546",
"price": "0.000446805434998315031",
"amount_usd": "6839.954413",
"price0_usd": null,
"price1_usd": null
}
],
"next_cursor": "1794137831000.26369898.55"
}Coming soon: positions and PnL
GET /v1/dex/{chain}/traders/{address}/positionsNot open yet. As of 2026-11-08 this path answers 404. It is documented ahead of launch so you can plan for it; the field list below is a draft from the OpenAPI document and may still change before it opens. There are no sample numbers until it is live.
The route will return the trader's position in each token traded in listed pools, most recently traded first, at weighted average cost: buys add their USD to the cost; a sell realizes its USD minus the average cost of what it sold. Tokens that came in by transfer have no cost: selling them goes to unmatched_sell_qty and earns no realized PnL. Unrealized PnL prices the held amount at the token's current USD price. Positions are rebuilt hourly.
| Parameter | In | Description |
|---|---|---|
address | path | The trader (transaction sender), 0x-prefixed |
limit | query | Most positions, 1–1000 (default 100) |
| Field (draft) | Type | Description |
|---|---|---|
token, symbol, decimals | string, string / null, integer / null | The token |
qty | raw amount | Held amount in base units; negative after selling tokens that came in by transfer |
unmatched_sell_qty | raw amount | Base units sold beyond what was bought in indexed trades; they earn no realized PnL |
cost_usd | decimal | USD cost of the held amount, at weighted average cost |
avg_cost_usd | decimal / null | Average buy price: USD per whole token; null without a positive qty or known decimals |
realized_pnl_usd | decimal | Realized PnL in USD |
price_usd | decimal / null | The token's current USD price |
unrealized_pnl_usd | decimal / null | qty in whole tokens × price_usd − cost_usd; null without a price or decimals, 0 without a positive qty |
buys, sells | integer | Number of buys and sells |
first_day, last_day | date | UTC days of the first and the latest indexed trade, YYYY-MM-DD |
Errors
Errors are JSON {"error": {"code": "...", "message": "..."}}. Branch on code, not on message.
| Status | error.code | When |
|---|---|---|
| 400 | invalid_request | A parameter is invalid (a malformed address, limit out of range, a bad cursor) |
| 401 | invalid_key | The key is unknown or revoked, or was sent somewhere other than X-API-Key / Authorization: Bearer |
| 403 | forbidden | The account is suspended, or the key's endpoints are expired or do not allow your IP |
| 404 | not_found | The chain is not served, the token has no holder ranking yet, or the pool is not indexed |
| 429 | rate_limited, quota_exceeded | Over the rate, or the period's quota is used up: see Limits and 429 |
| 500 | internal | An unexpected failure |
| 502 | upstream_failed, response_too_large | The data service failed or answered too much; try again with a growing delay |
| 503 | unavailable | Briefly unavailable, or tokens could not be read from the node; try again shortly |
| 504 | timeout | Ran out of time; try again |
See also
- On-chain DEX API: prices, pools, candles and WebSocket subscriptions, with the same key
- Interactive reference (Swagger UI) and OpenAPI JSON
- Crypto Data API pricing, shared by the On-chain DEX API and the Wallet API