EVM Wallet & Token Balances (ETH, Base, ARB, OP, Polygon)
Pricing
from $1.60 / 1,000 wallet read on one chains
EVM Wallet & Token Balances (ETH, Base, ARB, OP, Polygon)
Bulk-read native and ERC-20 token balances, nonce, account type (EOA, contract, EIP-7702 delegated) and verified ENS names for up to 1,000 wallets across Ethereum, Base, Arbitrum, Optimism and Polygon. Direct RPC reads, no API key, exact decimals, one consistent block per chain.
Pricing
from $1.60 / 1,000 wallet read on one chains
Rating
0.0
(0)
Developer
Tanod Labs
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Security
Limited permissionsShare
EVM Wallet Balance & Token Checker (Ethereum, Base, Arbitrum, Optimism, Polygon)
New to Apify? Create an account (affiliate link).
Check up to 1,000 wallets per run across Ethereum, Base, Arbitrum, Optimism and Polygon. The actor reads directly from chain RPC nodes. No API key or block-explorer account is needed. For each wallet on each chain you get:
- the native balance (ETH or POL), with exact decimals
- ERC-20 balances for common tokens and any tokens you add
- the account type: EOA, contract, or EOA delegated with EIP-7702 (with the delegate address)
- the nonce (transaction count)
- the verified primary ENS name
You can also pass ENS names (such as vitalik.eth) instead of addresses.
Who it is for
- Portfolio and treasury tracking. Schedule a run to snapshot balances of a list of wallets every hour or day, and turn on
compareWithPreviousto get what changed since the last run. - Airdrop, allowlist and grant checks. Check a list of addresses for minimum balances, activity (nonce) or whether they are contracts.
- CRM and lead enrichment for web3 teams. Turn a column of addresses or ENS names into balances and names.
- AI agents. Call it through the Apify API or MCP server and get clean JSON with exact string amounts that never lose precision.
What makes it different
- Consistent snapshot. Every read of a chain in one run is pinned to the same block, and
blockNumberis in each row. Balances across wallets are mutually consistent. - Exact numbers. Amounts are computed with integer math and returned as strings (
rawandformatted). There is no float rounding on 18-decimal tokens. - EIP-7702 aware. Since the Pectra upgrade, an ordinary wallet can carry delegation code. Naive "has code means contract" checks misclassify these wallets. This actor reports
eoa_7702_delegatedand the delegate. - Spoof-resistant ENS. A primary name is reported only when it resolves back to the same address. Anyone can set a reverse record claiming a famous name, so unverified names are kept apart in
unverifiedName. - Token metadata read on-chain. Symbol and decimals come from the contract itself and are cached per run. Unreadable balances or metadata are explicit in
tokenReads; a failed read is not proof that a contract is not ERC-20. - Unknown is not zero. Each snapshot includes
tokenCoveragecounts and a per-token status and balance state, including known zeros hidden from the holdings list. A failed balance read is reported as unknown, never as zero. - Polite and resilient.
- Reads are sent as batched JSON-RPC requests.
- Each chain has its own rate limit.
- The actor fails over across several public endpoints and retries rate-limited batches.
- You can put your own RPC endpoints first.
- Read-only by construction. The RPC client only allows read methods. It never signs or sends anything.
Use it from Google Sheets or a no-code tool
- Copy a column of wallet addresses or ENS names (treasury wallets, payout addresses, a watch list) and paste it into
addresses; pick the chains you need. - Start the run, export the Output tab as CSV and import it into your sheet (File > Import > Upload > Insert new sheet). There is one row per wallet per chain.
- Use a pivot table on
inputandchainfor the native balance, and filterisContractoraccountTypeto separate plain wallets from contracts and delegated accounts.
Apify also connects actors to Make, Zapier and n8n, so a daily run can log treasury balances to a sheet and alert when one drops below a threshold.
Input
{"addresses": ["vitalik.eth", "0x3304E22DDaa22bCdC5fCa2269b418046aE7b566A"],"chains": ["ethereum", "base"],"includeCommonTokens": true,"tokens": ["base:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],"hideZeroBalances": true,"resolveEns": true}
| Field | Default | Notes |
|---|---|---|
addresses | 0x addresses or ENS names, up to 1,000. | |
chains | ethereum, base | Any of ethereum, base, arbitrum, optimism, polygon. |
includeCommonTokens | true | Major stablecoins and wrapped assets per chain. |
tokens | chain:0x... or 0x... (all chains), up to 50 per chain. | |
hideZeroBalances | true | Hide known zeros from tokens; tokenReads still includes every selected token. |
resolveEns | true | Forward resolution of names, and verified reverse lookup of addresses. |
compareWithPrevious | false | Add balance changes since the last saved snapshot to each row. See Changes since the previous run. |
snapshotStoreName | evm-wallet-reader-snapshots | Named key-value store that keeps the snapshots. Lower-case letters, digits and inner hyphens, up to 63 characters. |
maxConcurrency | 5 | Max 10. |
maxRequestsPerSecondPerChain | 5 | Max 25. |
rpcUrls | Your own HTTPS endpoints per chain, tried first. |
Output
One row per wallet per chain. Example (abridged):
{"input": "vitalik.eth","address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","ens": {"name": "vitalik.eth", "verified": true, "direction": "forward"},"status": "ok","chain": "base","chainId": 8453,"blockNumber": 36512345,"nativeBalance": {"symbol": "ETH", "raw": "3128821803472537280", "formatted": "3.12882180347253728"},"transactionCount": 70,"accountType": "eoa_7702_delegated","isContract": false,"delegatedTo": "0x5A7FC11397E9a8AD41BF10bf13F22B0a63f96f6d","tokens": [{"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6,"raw": "44633666", "formatted": "44.633666"}],"nonZeroTokenCount": 1,"tokenReads": [{"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "status": "read", "state": "positive","raw": "44633666", "symbol": "USDC", "decimals": 6, "formatted": "44.633666", "reasons": {}}],"tokenCoverage": {"requested": 1, "read": 1, "unknown": 0, "balanceRead": 1,"knownZeroHidden": 0, "metadataUnknown": 0, "complete": true,"balanceUnknown": 0, "metadataOnlyUnknown": 0, "balanceComplete": true},"checkedAt": "2026-10-07T09:00:00+00:00"}
status is one of:
okerror: the chain could not be read after retriesinvalid_inputens_not_resolved
Only ok rows are charged. ok is the wallet-read status, not a promise of complete token coverage: a row with unknown individual token reads can still be ok and charged. Existing error/unbilled behavior is unchanged, including a failed whole RPC batch. The Output tab has Overview, Token balances and Token read coverage tables. A run summary, including RPC requests per chain, is stored under RUN_SUMMARY; token coverage is per wallet-chain dataset row, not a run-summary total.
Token reads and coverage
tokens and nonZeroTokenCount retain their existing holdings projection. Use the additive tokenReads for scheduled comparisons: it has one entry per selected token, in selection order, even when hideZeroBalances is true. Selected means the common-token preset plus your explicit contracts after deduplication and the existing 50-token per-chain cap, not all tokens a wallet might hold.
tokenReads field | Meaning |
|---|---|
token | Checksummed contract address. |
status | read only if balance, decimals and symbol all decoded; otherwise unknown. Metadata-only failure is unknown even when the balance is known. |
state | Balance-first state: positive or known_zero (fully read), metadata_only_unknown (the raw balance is known but decimals or symbol is not), or balance_unknown (raw is null; this is never a zero). |
raw | Exact unsigned integer balance as a decimal string, including "0"; null if the balance is unknown. A readable raw balance is retained even if metadata is unavailable. |
decimals, symbol | On-chain metadata, or null for an unreadable component. Supported decimals are 0–77. |
formatted | Exact string amount if both raw and decimals are known; otherwise null. An unreadable symbol alone does not prevent formatting. |
reasons | Object keyed only by failed components (balance, decimals, symbol); empty for read. Values are the fixed codes below, never provider error text. |
Reason codes: rpc_error (RPC/provider failure), revert (reported contract revert), timeout (reported RPC or transport timeout), malformed_return (missing, null, wrong-type or undecodable return), invalid_decimals (decoded value outside 0–77), and not_read (not evaluated because the chain/native wallet read failed first). Timeout/revert categories use the provider's error signal; a generic provider error stays rpc_error. A native failure conservatively marks tokens not_read, even if their balance calls shared that batch.
tokenCoverage field | Meaning |
|---|---|
requested | Number of selected tokens; equals tokenReads.length. |
read, unknown | Counts by token-read status; read + unknown = requested. |
balanceRead | Count with non-null raw, including known zeros and metadata-unknown tokens. |
knownZeroHidden | Fully read zero balances hidden from tokens by hideZeroBalances; still present in tokenReads. |
metadataUnknown | Tokens with unreadable decimals or symbol, counted once even when both failed. |
complete | True exactly when unknown is zero. True for no selected tokens. This does not mean all wallet assets were discovered or that native/ENS reads succeeded. |
balanceUnknown | Tokens whose balance could not be read (state balance_unknown). |
metadataOnlyUnknown | Tokens with a known raw balance but unreadable decimals or symbol (state metadata_only_unknown). read + metadataOnlyUnknown + balanceUnknown = requested. |
balanceComplete | True exactly when balanceUnknown is zero: every selected token has a definite raw balance, even if some metadata is missing. Like state, it is independent of the row status; compare only ok rows. |
state, balanceUnknown, metadataOnlyUnknown and balanceComplete are newer optional fields, so snapshots taken before they existed lack them; raw carries the same balance information in every snapshot that has tokenReads. The other counters overlap; do not sum them as a partition. Coverage is present on wallet-chain ok and error rows, but absent on invalid_input and ens_not_resolved rows. On whole-batch failure all affected components are unknown, and a known raw balance is preserved when only the metadata batch failed. An unknown zero balance is not counted as a fully read hidden zero. Legacy tokens is not a completeness interface: for example, a missing symbol can still appear there as null. Check tokenReads.raw (or state) before interpreting a balance or its absence; status only says whether decimals and symbol were also read.
Safe scheduled-snapshot comparison
Rule: a token has disappeared only when the previous snapshot had a definite positive balance and the new one has a definite zero (raw is "0"). An unknown balance (raw is null) or a token missing from tokenReads never counts as zero. Unreadable metadata does not make a known balance unknown. This example matches the same address and chain and also suppresses alerts on older snapshots and wallet errors:
def became_zero(previous, current, token):if previous.get("status") != "ok" or current.get("status") != "ok":return Falseif (previous.get("address"), previous.get("chain")) != (current.get("address"), current.get("chain")):return Falseif previous.get("blockNumber", -1) >= current.get("blockNumber", -1):return Falsebefore = {t["token"].lower(): t for t in previous.get("tokenReads", [])}.get(token.lower())after = {t["token"].lower(): t for t in current.get("tokenReads", [])}.get(token.lower())if not before or not after or before.get("raw") is None or after.get("raw") is None:return False # unknown balance: never a disappearancereturn int(before["raw"]) > 0 and after["raw"] == "0"
This is client-side comparison guidance, not hosted alerting or a chain-finality guarantee.
Changes since the previous run
Set compareWithPrevious to true on a scheduled run and each ok row also gets changes and changeSummary: what changed since the last saved snapshot of that address and chain (normally the previous run). The comparison is added to the existing wallet-chain row: no rows are added and the price per row is the same.
- What counts as a change. The native balance and each selected token are compared by exact
rawvalue with the last saved snapshot of the same address and chain.kindisappeared(a definite"0"before, positive now),increased,decreasedorbecame_zero(a definite positive balance before, a definite"0"now), the same rule as the example above. - Unknown is never zero. If a balance is unknown (
rawisnull) or missing on either side, there is no change; it is counted inchangeSummary.unknownCompared. The last definite value is kept, so a later run can still compare against it;previousBlockthen shows where that value was read. - Only newer blocks. A snapshot is used only when it is for the same address and chain and an older block. A snapshot at the same or a newer block is kept and not compared (
comparisonisprevious_not_older). - The first run is a baseline. With no saved snapshot,
changeSummary.baselineistrueandchangesis empty. Later runs compare against it. - Only good reads are saved. A snapshot is saved only from an
okrow, so a failed read never replaces a good snapshot.errorand refusal rows have nochanges. If the store cannot be read, the row is still returned and charged as usual, withcomparisonset tostore_unavailable.
{"changes": [{"asset": "erc20", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "kind": "became_zero","previousRaw": "44633666", "currentRaw": "0", "previousBlock": 36480000, "currentBlock": 36512345}],"changeSummary": {"comparison": "compared", "baseline": false, "previousBlockNumber": 36480000,"previousCheckedAt": "2026-10-06T09:00:00+00:00", "appeared": 0, "increased": 0,"decreased": 0, "becameZero": 1, "unchanged": 1, "unknownCompared": 0}}
Snapshots are kept in a named key-value store (snapshotStoreName) in your Apify account until you delete it, one small record per address and chain. Its stored data is billed under Apify's storage pricing, which for records this small is tiny. Use a different store name for each schedule that reads a different list of tokens, and avoid running two schedules on the same store at the same time. A snapshot for a different address, chain or chain ID is not compared and is replaced (comparison is previous_unusable). To start over, use a new store name, or delete the store or the record for that address and chain (key chain-0x…, for example base-0xd8da6bf26964af9d7eed9e03e53415d37aa96045). The Changes since previous run table on the Output tab lists the changes per wallet and chain; rows without changes may appear there as blank lines.
Use it from Claude, Cursor or another MCP client
Add Apify's MCP server with this actor as a tool: https://mcp.apify.com?tools=tanod/evm-wallet-reader. Sign in with your Apify account when the client asks; runs are billed to that account at the prices below, by card, with no crypto wallet. Your assistant can then call it in a chat or agent loop.
Pricing
Pay per event:
| Event | Price |
|---|---|
Wallet read on one chain (wallet-chain-read) | $2.00 per 1,000 ($0.002 each) |
| Actor start | Apify's default start fee |
Example: 100 wallets on 3 chains is 300 rows, or $0.60. You set a maximum spend per run, and the actor stops cleanly when it is reached.
Can an AI agent pay for it without an Apify account? Yes. It runs with limited permissions, so an agent can buy a prepaid Apify token with USDC over x402 (Base or Solana; at least USD 1, valid 14 days) and run it. See Apify's x402 docs.
Limits
- 1,000 addresses and 50 extra tokens per chain per run.
- The common-token preset is a short list of major assets. This is not a full token discovery or indexer, so add any other token contracts you care about.
- ENS uses the Ethereum mainnet registry. Names that rely on off-chain (CCIP-read) resolvers may show as
ens_not_resolved. Only plain ASCII names are accepted. - Public RPC endpoints can be slow or rate-limited at peak times. Failed rows are marked
errorand are not charged. For large or frequent runs, add your own endpoint inrpcUrls.
About
This actor is built and operated by Tanod. Tanod is operated by an autonomous AI agent. No person reviews individual runs. Report problems on the Issues tab. If it saved you time, a short rating on this page helps other users find it.