DeBank Portfolio
Use this reference for the wallet-wide current holdings of a public EVM address across target chains: native and
fungible-token balances plus DeFi protocol positions (liquidity, lending, staking, vesting). DeBank covers more target
chains than Blockscan, including non-Etherscan chains. For one named chain, use
references/workflows/blockscan-balances.md. Historical balances, NFT inventories, and transaction history stay on
references/workflows/provider-routing.md.
Global Queue
DeBank's WAF limits the whole browser, so every agent on this host shares one allowance and one agent's burst blocks the
rest. Hold a lease from scripts/debank-gate.py for all debank.com work: navigating, pasting the collector, start,
and DOM reads. Leases are granted one at a time in FIFO order; state lives under
${XDG_STATE_HOME:-~/.local/state}/evm-atlas/debank-gate.
python3 scripts/debank-gate.py acquire --label '<skill>: <purpose>' --profiles <n>- Each command prints one JSON line. Exit 0 with
"status": "granted"means proceed and keep theticket. Exit 3 with"status": "queued"(after--wait, default 240 s) reportsposition,holder, andcooldownUntil: rerunacquirewith--ticket <ticket>to keep the place. A ticket not polled for 120 s is dropped. In Claude Code, runacquire --wait 3600in the background and continue when it exits. --profilesis the number of addresses the lease covers, at most 25. Bulk work takes one lease per batch, so other agents' single-address checks get a turn between batches.- A lease expires after
--ttl(600 s). Runrenew --ticket <ticket>before then for longer work. Exit 4 ("lost") means it expired and another agent may hold the gate: stop touching DeBank and acquire again. - When done, or on any failure, close the owned DeBank page, then run
release --ticket <ticket>. - On a WAF block, run
block --ticket <ticket>: it releases the lease and pauses the queue for every agent for 15 minutes. statusshows the holder, cooldown, and queue. Never touch debank.com without a lease, even when the queue is long. A caller that cannot wait uses Fallbacks and reports the queue wait as the cause.
Chromium Workflow
- With a lease held, validate each address (20-byte hex), then open an owned page on
https://debank.com/profile/<addr>with Chrome DevToolsnew_page. When the cookie dialog appears, chooseReject. - Never call
api.debank.combalance endpoints yourself (DeBank's Cloud OpenAPI is paid). The profile page's calls arefetchGETs signed by the app (x-api-*headers): unsigned calls, even from inside the page, return429 Request too fast, andcredentials: 'include'fails CORS. The collector captures the app's own responses instead. - Token discovery, for one address or many, uses the collector. Read
scripts/debank-collect.jsand pass its whole contents verbatim as theevaluate_scriptfunction; the file has no trailing;on purpose, so it stays a pasteable arrow function. It returns{ installed: true, reused: false }(reused: truewhen the page already has it), installswindow.__debankCollect, and wrapswindow.fetchto record the app'sused_chainsandbalance_listresponses. Per address it routes the page to the profile and succeeds onceused_chainsand abalance_listfor every listed chain return 200 witherror_code0. DeBank lists the chains an address used, not the chains where it holds tokens, so anokrecord with chains can hold zero tokens. The API includes small balances the UI folds, so noShow allclick is needed. Start the run with a secondevaluate_scriptcall:
async () => window.__debankCollect.start(["<addr>"])start lowercases and de-duplicates the addresses, throws for an invalid address, an active run, or a failed
chain/list call, and otherwise returns { queued } without waiting for the run, so awaiting it surfaces those errors.
An optional second argument sets { timeoutMs: 30000, maxAttempts: 3, cooldownMs: 20000, haltAfter: 3 } (the defaults).
- Poll
window.__debankCollect.status()with shortevaluate_scriptcalls untilrunningisfalse. It returns{ running, total, done, ok, failed, pending, rateLimited, blocked, startedAt, elapsedMs }, withstartedAtan ISO string. A profile takes about 2.5 s (1.2-4.6 s). A429, error, or timeout pauses the page forcooldownMsand requeues the address untilmaxAttempts, after which its record isfailed. AfterhaltAfterconsecutive failed attempts that saw a429, the run stops withblocked: trueand fails every queued address; see Rate Limits and WAF Blocks. - Save
window.__debankCollect.results()by callingevaluate_scriptwithfunction: () => window.__debankCollect.results()and afilePath. The path must be inside the MCP workspace roots (a git-ignored project directory); other paths are refused.results()returns the latest run's completed records in input order, and a newstartclears them, so save first. Each record hasaddress,status(okorfailed),attempts,error(whenfailed),chains(theused_chainsslugs),observedAt(ISO string), andtokens, each{ chainId, chain, contract, symbol, decimals, rawAmount, amount, price }.contractis the lowercased ERC-20 address or"native",chainIdis the chain's numericnetwork_id(nullwhenchain/listhas none for the slug), andrawAmountis an exact decimal string in raw units. Afailedrecord carrieschains: []andtokens: []; those arrays mean nothing, and onlystatuscounts. - Read the
All Chainsummary and the DeFi protocol sections from the DOM only, on that address's own profile (after a run, the page shows the last address collected). Wait forData updated; clickUnfold <n> chainswhen present, then read per-chain USD values from the summary. DeFi sections follow the wallet table, each with a protocol name, USD value, and position type; read them from a fresh snapshot when the user requests positions.
Data updated <age>is the DeFi project-snapshot time, not wallet-token freshness.20727 days ago(the Unix epoch) meansportfolio/project_listfailed to load and says nothing about tokens. Token observation time is each record'sobservedAt.- The DOM wallet table folds small balances behind a toggle reading "Tokens with small balances are not displayed. Show
all". It appears only when the wallet has at least 15 tokens and at least 4 are under min(0.1% of wallet USD, $1000).
Click
Show allonly when you must read the table itself; the clickable is a<span>with an<svg>child, so a leaf-element text matcher never finds it. Scraping the folded table misses those tokens.
Many Addresses
One start call takes any number of addresses and routes a single owned page through their profiles (DeBank refuses to
render in an iframe), so a loop never needs one page per address.
- Use one owned page, opened with
new_page(it reports visible, so timers are not throttled). Each profile costs about10 + <chains>requests, and bulk runs were blocked after every 30-55 profiles. - Split the addresses into batches of at most 25. Per batch:
acquire --profiles <batch size>,startthe batch, poll, saveresults(), thenreleaseand acquire again for the next batch. - The loop runs without being awaited, so no DevTools protocol timeout applies; poll
status()with short calls. - A new
startthrows while a run is active and clears the previous results once it begins. To abort an active run, reload the page and paste the script again. - A
failedrecord is a coverage gap, never an empty wallet; handle it per Fallbacks. Anokrecord with no tokens is DeBank's indexed zero. - Never read the page UI to decide that a wallet is empty. During rate limiting, a
429onused_chainsmakes the page show "No assets yet" with a "Request too fast" toast,429s on the balance endpoints remove the wallet table, and DOM rows can linger from the previous profile after a route change.
Rate Limits and WAF Blocks
Request too fast (HTTP 429, body error_code: 429, or the page toast) is DeBank's WAF rejecting the request. It is
a coverage gap, never evidence about the wallet, and never a reason to call api.debank.com another way. Classify it by
how it arrived:
- Unsigned call. A direct
fetch,curl, or WebFetch ofapi.debank.combalance endpoints always gets429, so retrying it never works. Switch to the collector; never present the429as DeBank being down. - Burst.
rateLimited > 0while records still finishokis the collector absorbing short bursts; no action. - Block.
status().blockedistrue,startthrowschain/list failed: HTTP 429, or the toast persists. DeBank is rejecting the whole browser; blocks lasted 5-13 minutes. Do not reload, re-paste, open more pages, or restart in a loop: each request prolongs it. Close the page and rundebank-gate.py block --ticket <ticket>so every queued agent waits out the cooldown instead of extending it. Then acquire again for thefailedaddresses only; the queue grants the lease after the cooldown. If that retry is also blocked, take the remaining addresses through Fallbacks.
Confirm a block in Chromium before reporting it: the status() output, the record error strings, or the 429
responses in list_network_requests. Report it as DeBank WAF rate-limit block ("Request too fast") with the
verification method, the retry made, and the affected addresses.
Chain Mapping
- DeBank names chains by slug (
eth,scrl,xdai,era). The collector maps slugs to chain IDs through thenetwork_idfield of the keylesshttps://api.debank.com/chain/list. Match target chains by exactchainId, never by slug or display name. - A token whose
chainIdisnull(chain/listhas no numericnetwork_idfor its slug, as for non-EVM balances such as Hyperliquid spot and perps) is not a target chain: report it with DeFi positions, never as a target chain.
Coverage and Scope
- Derive coverage at runtime: target chains whose ID appears in
chain/listare DeBank-supported.used_chainsbounds the chains the collector queries, so a supported target chain absent from a record is DeBank's indexed zero, not an RPC-confirmed zero. - Ignore non-target chains. Sum target-chain tokens for any target-only total; never report the page-wide total as one.
- DeBank drops spam server-side (every returned token had
is_scamandis_suspiciousfalse), so it never reports spam tokens, and absence from a record does not show that a token is not held. rawAmountis exact in raw units (withdecimals);amountandpriceare DeBank's. They are still indexer data, andaddress-usd-value.mdconfirms amounts by RPC before using them. Apply the Pricing Hygiene there before using DeBank prices in totals.- Keep DeFi positions separate from wallet balances.
Fallbacks
- Target chains missing from
chain/list: useblockscan-balances.mdwhen Blockscan lists the chain ID, otherwiseprovider-routing.md. - Navigation fails, an error or challenge persists, an address's record is
failed, or a WAF block survives the one retry in Rate Limits and WAF Blocks: useblockscan-balances.md, thenaddress-sweeps.mdfor remaining target chains. - Chrome DevTools MCP or Chromium is unavailable: use
address-sweeps.md(or the API passes inaddress-usd-value.md). - On-chain precision required: confirm with RPC
balanceOfandeth_getBalanceasaddress-usd-value.mddoes.
Report which condition caused each fallback.
Output
Return the profile URL and the collector observedAt per address; add the Data updated age only when DeFi positions
were read, labeled as the DeFi snapshot time. Then one row per non-empty target chain (name, ID, native and token
amounts, DeBank USD), the target-only sum, and DeFi positions as a separate labeled list (protocol, chain, position
type, USD). For many addresses, also give the saved results path and the ok and failed counts. Add counts of
excluded non-target chains and coverage gaps (including each failed address) with their cause. Separate
fallback-derived facts by provider.