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_call accepts for the call, by bisection.
  • Gas price: the chain's eth_gasPrice plus 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 answers TRY_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) with value = 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 fee to ZeroDust, sends the rest to target with data, 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's l1Fee estimate, l1Formula and absorb. 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 the GasPriceOracle values (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() on 0x4200000000000000000000000000000000000015 is 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_estimateGas cannot size a call that burns its gas (it answers the block gas limit); the planner finds the smallest limit eth_call accepts 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 block n, wait until the chain's block number is at least n + txGapBlocks before 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 feePaidTx and 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: false means 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 to recipient, which must not be from. Cross-chain asks Gas.zip and Relay and keeps the larger output.
  • With bridge=native (chains with nativeExit): the sweep goes into the chain's own bridge to recipient on Ethereum (route: "native", receive: null): Blast through ZeroDustGuard into its L2StandardBridge, Abstract through the paymaster into L2BaseToken.withdraw, Gravity into ArbSys.withdrawEth (G arrives on Ethereum as an ERC-20, named in receiveToken). 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 to 0x000000000000000000000000000000000000dEaD. toChainId and recipient are 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