Direct chains
Some chains cannot be swept with EIP-7702 in ZeroDust. On these "direct chains" the wallet sweeps itself, with ordinary legacy transactions whose value is set so that value plus gas spends the balance to the wei. The API plans those transactions; your code signs and broadcasts them.
The planning endpoints are quote-only. They return unsigned transactions and never receive a signed transaction or a key.
Chains
As of 11 October 2026, GET /direct/chains lists 21 chains:
| Chain | ID | Token | kind |
|---|---|---|---|
| Flare | 14 | FLR | evm |
| Cronos | 25 | CRO | evm |
| Rootstock | 30 | RBTC | evm |
| Telos | 40 | TLOS | fixedprice |
| XDC | 50 | XDC | evm |
| Fuse | 122 | FUSE | evm |
| Monad | 143 | MON | gaslimit |
| Lens | 232 | GHO | zk |
| Boba | 288 | ETH | opguard |
| zkSync Era | 324 | ETH | zk |
| Flow EVM | 747 | FLOW | evm |
| HyperEVM | 999 | HYPE | evm |
| Metis | 1088 | METIS | evm |
| Vana | 1480 | VANA | evm |
| Gravity | 1625 | G | arbitrum |
| Abstract | 2741 | ETH | zk |
| Immutable zkEVM | 13371 | IMX | evm |
| Etherlink | 42793 | XTZ | etherlink |
| Avalanche | 43114 | AVAX | evm |
| Blast | 81457 | ETH | opguard |
| Ethereal | 5064014 | USDe | arbitrum |
Read the list from the API rather than hardcoding it. Blast (26 October 2026), Gravity (1 November 2026) and Abstract (15 December 2026) are shutting down: their entries carry closing, and each leaves the list when it closes. Vana has noExit: true: no bridge takes VANA out, so only a same-chain sweep to another address is planned.
How exact zero works
A legacy transaction costs exactly gas x gasPrice on these chains, so value = balance - gas x gasPrice leaves 0. That needs the exact gas, never an estimate:
- The planner finds the smallest gas limit
eth_callaccepts for the call, by bisection. - Gas price: the chain's
eth_gasPriceplus 10%. Gravity charges the base fee whatever the transaction says, so it is only planned while the base fee is at its floor. Etherlink adds its inclusion fee as gas and is only planned while its base fee is 1 gwei. Otherwise the planner answersTRY_LATER. - A same-chain sweep must go to a plain address (no contract code), because only then is the transfer's gas fixed.
ZK-stack chains
Chains with kind: "zk" (zkSync Era, Abstract, Lens) are swept with two EIP-712 (type 113) transactions that name a ZeroDust paymaster, which pays all the gas. With a paymaster set, the wallet only spends each transaction's value, so a plan is exact when the two values add up to the balance: the first pays the service fee and both transactions' gas to the paymaster, the second sends the rest to the bridge or the recipient. If an earlier fee transaction went through and its sweep did not, the plan is the sweep alone. No gas estimate is involved. The paymaster is 0x986e4Bb55AEEE6a8c80c28Ca787b13E216fD25B8 on all three chains; refuse a plan that names another. When you check such a plan, require sum(value) == balance.
Gas-limit chains
Some chains charge the whole gas limit (gasLimit x gasPrice) and refund nothing, whatever the transaction used. GET /direct/chains marks them with kind: "gaslimit" (Monad today). There any sufficient limit leaves exactly 0, so the planner never bisects: plain transfers use 21,000 and contract calls use the chain's eth_estimateGas plus 25%. When you check such a plan, require sum(value) + sum(gas x gasPrice) == balance exactly; a simulator that refunds unused gas will show a little left over, and that is expected.
Some chains charge their own fixed network gas price, whatever price a transaction offers, and reject a lower one. GET /direct/chains marks them with kind: "fixedprice" (Telos today). A transaction priced above the network price is charged the network price, so the difference would stay in the wallet as dust; the receipt still reports the offered price. Plans for these chains offer exactly eth_gasPrice. When you check such a plan, read eth_gasPrice yourself and require every transaction's gasPrice to equal it.
Guard chains (OP stack)
On OP-stack chains every transaction also pays an L1 data fee, which depends on the signed bytes and on L1 prices, so a plain transfer cannot be sized to leave exactly 0. GET /direct/chains marks these chains kind: "opguard" with their fee formula in l1Formula (ecotone on Blast, fjord on Boba). They are swept with one legacy transaction through ZeroDustGuard (0x2f95e6ED90a7dD67fc3Fae5c5628647E6A83e48e on both, runtime keccak 0x0e00b07c95af7e18d655633d521d110ce6e95ff3c330ff3a4ac2d1b44fed685e), a contract with no owner and no storage:
- The call is
sweep(address target, uint256 fee, bytes data)withvalue = balance - gas x gasPrice - L1 fee. The chain takes the gas and the L1 fee before any code runs. - The guard reverts unless the wallet is then at exactly 0, pays
feeto ZeroDust, sends the rest totargetwithdata, reverts if anything came back to the wallet, and burns every unit of gas left so none is refunded. A transaction either leaves exactly 0 or changes nothing but its own fee. - The plan carries
guard:target,data,forwarded, the API'sl1Feeestimate,l1Formulaandabsorb. Before signing, compute the L1 fee from the signed transaction's own bytes (Ecotone: 4 gas per zero byte and 16 per other byte; Fjord: the FastLZ-compressed length, at least 100 bytes) with theGasPriceOraclevalues (l1BaseFee,blobBaseFee,baseFeeScalar,blobBaseFeeScalar), set the value from it, and move the difference from the estimate into the fee (absorb: "fee": a bridge deposit must carry the quoted amount) or into what is forwarded (absorb: "amount"). Re-signing changes the bytes, so search near the first signature's size until the value holds. - The L1 fee's inputs change when the chain's L1 origin moves (about every 12 seconds). Price the transaction at the first block of an origin (
sequenceNumber()on0x4200000000000000000000000000000000000015is 0 or 1) and send it at once. If the origin moves before inclusion, the guard reverts and only that attempt's gas is spent: plan again. - Cross-chain goes through Relay only: Gas.zip and LI.FI pay refunds to the sender, which would be the guard. There is no swap exit.
eth_estimateGascannot size a call that burns its gas (it answers the block gas limit); the planner finds the smallest limiteth_callaccepts and adds 25%.
Monad also keeps a 10 MON reserve: a transaction that takes a wallet below it reverts (and still pays its gas) unless it is the wallet's only transaction in the last 3 blocks and the wallet is not EIP-7702 delegated. So:
- A delegated wallet cannot be emptied there; the planner answers
INVALID_REQUEST. - Plans carry
txGapBlocks(also on the chain in/direct/chains): after a transaction lands in blockn, wait until the chain's block number is at leastn + txGapBlocksbefore sending the next one.
Fees
The standard service fee applies, paid as a separate exact transfer to ZeroDust (0x01eD5c94DE39E73C986b98B85C2c0A3d1BEDff7D) before the sweep:
- It is priced from Gas.zip's token price. With no price, no fee is charged.
- It is left out when it would be less than twice the gas of its own transfer.
- Burn and donate plans have no fee.
- A fee already paid is not charged twice: pass the fee transaction's hash as
feePaidTxand the planner waives the fee if that transaction is on-chain, from this wallet, to ZeroDust, successful and less than an hour old.
Endpoints
Rate limits: /direct/chains, /direct/balances and /direct/status are reads (200 per minute per IP); /direct/route, /direct/prepare and /direct/exit count as quotes (60 per minute per IP).
GET /direct/chains
{
"chains": [
{ "chainId": 43114, "name": "Avalanche", "token": "AVAX", "decimals": 18, "explorerUrl": "https://snowtrace.io", "rpcUrl": "https://api.avax.network/ext/bc/C/rpc", "kind": "evm" },
{ "chainId": 143, "name": "Monad", "token": "MON", "decimals": 18, "explorerUrl": "https://monadvision.com", "rpcUrl": "https://rpc.monad.xyz", "kind": "gaslimit", "txGapBlocks": 4 }
],
"prices": { "AVAX": 10.92 }
}
kind is the chain's gas rule: evm, arbitrum (price must equal the base fee), etherlink (inclusion fee charged as gas), gaslimit (the whole limit is charged), fixedprice (the network's own price is charged, whatever the transaction offers), zk (a paymaster pays the gas) or opguard (an L1 data fee; one transaction through ZeroDustGuard, with l1Formula). prices is USD per token, for display.
Optional fields: nativeExit (stack, toChainId, bridgeUrl: the chain's own bridge, see below), noExit (no way off the chain) and closing (stage, closesAt, cutoffAt, source, note). In a chain's last 3 days (cutoff) plans carry no ZeroDust fee.
GET /direct/balances/:address
An array of { chainId, name, token, decimals, explorerUrl, balance }. A chain whose RPC fails is left out.
GET /direct/route
Whether a bridge can take the balance to a destination right now. Quotes only.
| Query | Notes |
|---|---|
chainId |
Direct chain to sweep |
toChainId |
Destination chain |
from |
The wallet |
recipient |
Destination address |
bridge |
Optional. native: through the chain's own bridge to Ethereum (chains with nativeExit; toChainId must be 1) |
Response:
{ "available": true }: Gas.zip or Relay has a route.{ "available": false, "exit": true, "reason": "..." }: no direct route, but a swap-then-bridge exit (/direct/exit) exists.exit: falsemeans neither exists.{ "available": null, "reason": "..." }: a bridge did not answer; try again. Also{ "available": null }when the balance is 0.
GET /direct/prepare
The unsigned transactions that take the chain to exactly 0.
| Query | Notes |
|---|---|
chainId, toChainId, from, recipient |
As above |
mode |
route (default), burn or donate |
feePaidTx |
Optional hash of an earlier fee transaction |
route: an optional fee transaction, then the sweep. Same chain (toChainId=chainId) is a plain transfer torecipient, which must not befrom. Cross-chain asks Gas.zip and Relay and keeps the larger output.- With
bridge=native(chains withnativeExit): the sweep goes into the chain's own bridge torecipienton Ethereum (route: "native",receive: null): Blast through ZeroDustGuard into itsL2StandardBridge, Abstract through the paymaster intoL2BaseToken.withdraw, Gravity intoArbSys.withdrawEth(G arrives on Ethereum as an ERC-20, named inreceiveToken). Nothing arrives by itself: after the chain's waiting period the recipient proves (Blast) and claims the withdrawal on Ethereum, on the chain's bridge site (nativeExit.bridgeUrl). burn: one transfer of everything to0x000000000000000000000000000000000000dEaD.toChainIdandrecipientare still required but not used.donate: one transfer of everything to ZeroDust.
Response:
| Field | Notes |
|---|---|
chainId |
|
route |
gaszip, relay, across, native, transfer, burn or donate |
requestId |
Relay's request ID, needed by /direct/status; otherwise null |
txs |
Unsigned legacy transactions, in order, with consecutive nonces: kind (fee, sweep or swap), to, data, value, gas, gasPrice, nonce |
receive |
The least that arrives: the bridge's quote less 3% (bridges settle at their own price on delivery), the exact value for a same-chain transfer, 0 for burn or donate |
quoted |
Bridge and swap routes: the bridge's raw quote, before the 3%. Pass it back to /direct/status |
fee |
ZeroDust's fee in wei, 0 when none |
balance |
The balance this plan spends |
txGapBlocks |
Gas-limit chains with a reserve rule (Monad): blocks to wait after each transaction's block before sending the next |
expiresAt |
Across only: unix seconds after which the deposit reverts on-chain. Across quotes live about 30 seconds; never send the sweep after this |
guard |
Guard chains only: how the one transaction through ZeroDustGuard is built (see Guard chains) |
receiveToken |
When a token arrives instead of gas (Telos's OFT exit, Gravity's own bridge): { chainId, symbol, address, decimals } |
Across (not offered at the moment)
The API does not currently plan Across routes: Across moves these tokens through swaps on both ends, and a failed destination swap refunds USDC rather than the native token. If it is turned on, it is used only on gas-limit chains: its deposit runs a swap that would refund gas elsewhere. The sweep is one swapAndBridge call to Across's SpokePoolPeriphery (0x97CCDBea4632140639aD5eA9b944aa034eb15fD4): the native token is swapped and bridged, and on the destination Across's MulticallHandler swaps into the native gas token and sends it to recipient. Before signing, decode it and check at least: the SpokePool is Across's for the source chain, the depositor is from, the destination chain is toChainId, there is no submission fee, the deposit goes to Across's handler for the destination, the message's fallback recipient and every drain is recipient, no destination call carries value or moves a token except approving 0x's AllowanceHolder, the 0x swap pays the handler at least receive, and its Settler is 0x's registered one (ownerOf(2) or prev(2) on 0x00000000000004533Fe15556B1E086BB1A72cEae). If the destination swap fails, Across refunds the deposit to from on the source chain (as USDC, not the native token).
Before signing, check that the plan is what you asked for and that the sum of value + gas x gasPrice over txs (plus guard.l1Fee on guard chains) equals balance and the wallet's current balance. Then sign and broadcast the transactions in order to the chain's RPC. If the balance changed, request a new plan.
GET /direct/exit
For a chain with no direct route but an exit: a swap-then-bridge (LI.FI), or the chain's own token bridge (Telos: TLOS to Base through its OFT, route: "oft", the token named in receiveToken). Takes chainId, toChainId (must differ from chainId), from, recipient and optional feePaidTx.
The response has the same shape as /direct/prepare, with route: "lifi" and a swap transaction (or route: "oft" and a sweep into the OFT), tool (the tool used) and leftoverMax. A swap cannot use its gas limit exactly, so it leaves a small remainder. After the swap lands, sweep that remainder with /direct/prepare in burn or donate mode. If the balance is then above leftoverMax, the swap failed: stop.
GET /direct/status
Delivery status of a direct sweep.
| Query | Notes |
|---|---|
route |
gaszip, relay, across, lifi, transfer, burn or donate |
hash |
The sweep or swap transaction hash |
requestId |
Required for relay |
fromChainId |
Required for across (Across looks deposits up by origin chain) |
quoted, toChainId |
Optional: the plan's quoted and the destination (with fromChainId). With them, the API records how much the bridge delivered against its quote, with no address or hash, to keep the 3% margin honest |
Response: { "state": "pending" | "delivered" | "failed", "destTx": "0x..." }. For transfer, burn and donate the state is delivered; confirm the transaction landed and the balance is 0 on the chain itself.
Errors
| Code | Status | Meaning |
|---|---|---|
NOT_DIRECT_CHAIN |
400 | chainId is not a direct chain |
NOTHING_TO_SWEEP |
400 | The balance is 0 |
INSUFFICIENT_FOR_GAS |
400 | The balance does not cover the gas or the fee |
INVALID_REQUEST |
400 | For example a same-chain sweep to the same address, or to a contract |
NO_ROUTE |
409 | No bridge route; try /direct/route for an exit |
TRY_LATER |
503 | Exact zero is not possible right now (gas price conditions, unsettled quotes) |
UPSTREAM |
503 | A bridge failed for another reason |