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/dex

Supported 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 pathWhat it returnsStatus
GET /v1/dex/{chain}/tokens/{address}/holdersThe largest holders of a tracked tokenLive
GET /v1/dex/{chain}/accounts/{address}/portfolioThe tokens an address holds, with USD valueLive
GET /v1/dex/{chain}/pools/{id}/tradesA pool's trades, each with the trader's addressLive
GET /v1/dex/{chain}/traders/{address}/positionsA trader's positions, average cost, realized and unrealized PnLComing 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}/holders

The 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.

ParameterInDescription
chainpathethereum, base or arbitrum
addresspathThe token, 0x and 40 hex digits, in any case
limitqueryMost holders returned, 1–5000 (default 5000)

Response fields (data):

FieldTypeDescription
chain, addressstringThe chain and the token
symbol, decimalsstring, integer / nullToken metadata
blockintegerTransfers up to this block are counted
known_totalraw amountSum of every balance besides the excluded addresses'; the denominator of pct
known_total_amountdecimal / nullknown_total in whole tokens; null when the decimals are unknown
total_supplyraw amount / nulltotalSupply() read from BlockReq's own node; null when it could not be read
consistentboolean / nullWhether the Transfers add up to within 1% of total_supply. When false (rebasing tokens and the like), every pct is null
excludedstring[]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}/portfolio

The tokens an address holds on the chain, from two sources only:

  1. the balances the holder rankings store: tracked tokens whose top 5,000 holders include the address (source: "holders");
  2. balanceOf read from BlockReq's own node for the tokens you list in tokens (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.

ParameterInDescription
chainpathethereum, base or arbitrum
addresspathThe holder, 0x and 40 hex digits, in any case
tokensqueryUp to 50 comma-separated ERC-20 addresses (duplicates count once) to read with balanceOf. The native coin's address is refused

Response fields (data):

FieldTypeDescription
chain, addressstringThe chain and the holder
total_value_usddecimalSum of every value_usd, to 6 decimals; holdings without one are not counted
holdings[]object[]See below
node_blockinteger / nullBlock of the node's balanceOf reads; null when none was made
failedstring[]Listed tokens whose balanceOf failed; a stored balance is kept for them
holdings[].tokenstringThe token
holdings[].symbol, namestring / nullToken metadata
holdings[].decimalsinteger / null
holdings[].balanceraw amount
holdings[].amountdecimal / nullThe balance in whole tokens; null when the decimals are unknown
holdings[].price_usddecimal / null
holdings[].value_usddecimal / nullamount × price_usd, to 6 decimals; null when either is missing
holdings[].sourceholders, nodeStored ranking balance, or balanceOf on BlockReq's node
holdings[].blockintegerBlock 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}/trades

The 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.

ParameterInDescription
chainpathethereum, base or arbitrum
idpathThe pool: 0x and 40 (pool contract) or 64 (Uniswap v4 pool key) hex digits
limitqueryMost trades per page, 1–100 (default 50)
cursorqueryThe 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}/positions

Not 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.

ParameterInDescription
addresspathThe trader (transaction sender), 0x-prefixed
limitqueryMost positions, 1–1000 (default 100)
Field (draft)TypeDescription
token, symbol, decimalsstring, string / null, integer / nullThe token
qtyraw amountHeld amount in base units; negative after selling tokens that came in by transfer
unmatched_sell_qtyraw amountBase units sold beyond what was bought in indexed trades; they earn no realized PnL
cost_usddecimalUSD cost of the held amount, at weighted average cost
avg_cost_usddecimal / nullAverage buy price: USD per whole token; null without a positive qty or known decimals
realized_pnl_usddecimalRealized PnL in USD
price_usddecimal / nullThe token's current USD price
unrealized_pnl_usddecimal / nullqty in whole tokens × price_usd − cost_usd; null without a price or decimals, 0 without a positive qty
buys, sellsintegerNumber of buys and sells
first_day, last_daydateUTC 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.

Statuserror.codeWhen
400invalid_requestA parameter is invalid (a malformed address, limit out of range, a bad cursor)
401invalid_keyThe key is unknown or revoked, or was sent somewhere other than X-API-Key / Authorization: Bearer
403forbiddenThe account is suspended, or the key's endpoints are expired or do not allow your IP
404not_foundThe chain is not served, the token has no holder ranking yet, or the pool is not indexed
429rate_limited, quota_exceededOver the rate, or the period's quota is used up: see Limits and 429
500internalAn unexpected failure
502upstream_failed, response_too_largeThe data service failed or answered too much; try again with a growing delay
503unavailableBriefly unavailable, or tokens could not be read from the node; try again shortly
504timeoutRan out of time; try again

See also