You’ll need: a signed-in user (Authentication & sessions) with an
EVM wallet linked to the account, and client code that can ask that
wallet to sign.
@agg-build/sdk, @agg-build/hooks and @agg-build/ui 4.33.0 or later.
Result: funds moved out of the user’s own wallet, signed by that wallet, polled to a final
state.POST /execution/withdrawals/{withdrawalId}/signatures for the wallet’s signatures. Sending
signingAddress is the only opt-in.
Before you start
signingAddressmust be an EVM wallet already linked to the signed-in account. A Solana wallet cannot be the signer, though a Solana address can be the recipient.- The feature can be off on an environment. A Max quote, preview or withdrawal with
signingAddressthen returns400Self-custody withdrawals are not enabled. - Send the same
signingAddressand the sameapproveMode(see Approve mode) on the Max quote, the preview and the withdrawal. All three pick sources by the same rules, so they agree only when they are given the same inputs. - A user has one withdrawal in progress at a time, managed or self-custody. A second one returns
409until the first is final.
What counts as the wallet’s balance
The flow
1
Quote Max for the destination
POST /execution/withdrawable/quote with signingAddress. maxDeliverableRaw is the largest
amountRaw the withdrawal accepts for that token and destination chain.2
Preview the exact amount
POST /execution/withdraw/preview with the full withdrawal body. It prices the route from the
wallet and returns unviable, with the reason, for anything the withdrawal would refuse.
Optional, but it is the only call that checks recipient rules before you submit.3
Withdraw
POST /execution/withdraw with the same body. The response may already carry the first
pendingSignatures[].4
Sign what the withdrawal asks for
Sign each request with the wallet and submit the results to
POST /execution/withdrawals/{withdrawalId}/signatures. Keep polling
GET /execution/withdrawals/{withdrawalId}: it returns any further pendingSignatures[], and
later steps can ask for more after earlier ones finish.5
Poll to a final state
Stop when
status is completed, partial or failed.1. Quote Max
maxDeliverableRaw: the largestamountRawa withdrawal from this wallet accepts. It is an amount debited, not an amount delivered (see Amounts). It leaves out balances too small to move and sources the chosenapproveModecannot spend.rawBalanceRaw: everything the wallet, its deposit wallet and its legacy Safe hold in the accepted tokens, before those exclusions. It can be larger thanmaxDeliverableRaw.
amountRaw and maxDeliverableRaw for a managed withdrawal. With
signingAddress, both are amounts debited, as above.
- Both are in the destination token’s decimals (6, or 18 on BNB Chain).
2. Preview
receiveAmountRaw (the estimated amount delivered), feeRaw, and
quoteExpiresAt (60 seconds out). An unviable one returns pricingStatus: "unviable" and
unviableReason; when no combination of sources covers the amount it also sets maxDeliverableRaw. The
preview creates nothing. See Errors and refusals for the reasons.
Preview a withdrawal.
3. Withdraw
withdrawManaged for history; with signingAddress it withdraws from the
wallet. max: true is refused with signingAddress: send the exact amount, for example the
maxDeliverableRaw from step 1. requestId works as on a managed withdrawal.
A shortened response:
expectedis this withdrawal’s quote:outputRawis the estimated delivery andfeeRawthe estimated fee taken from the amount. The status endpoint returns the actual fee once the withdrawal is final.pendingSignaturesis present only when the wallet already has something to sign. Its absence does not mean “nothing to sign”: poll the status endpoint.legs[].typeistransferfor same chain and same token, andbridgefor another chain or another token on the same chain.
4. Sign and poll
Each request has the same shape as a trade’s, so the signer you wrote for fills works unchanged. See A complete signer.signature, or
txHash for a transaction) and returns the withdrawal with whatever is still outstanding in
pendingSignatures. An unknown withdrawal, or one that is not this user’s, returns 404
Withdrawal not found. Per-step errors (expired, malformed, wrong signer) are the same as for
fills: see Driving the loop yourself.
Every request expires at its expiresAt. Read it; never hard-code a deadline.
Submit signatures for a pending self-custody withdrawal
· Get withdrawal status.
Statuses
A withdrawal can draw from several sources, for example the wallet’s Arbitrum USDC and its HyperCore
balance. Each source moves on its own signatures. That is why
partial exists: one source can
deliver while another expires or fails. sources[] and legs[] show each one’s status and
txHash.
Amounts
amountRaw is the amount debited from the wallet. The recipient gets amountRaw minus fees.
This is the opposite of a managed withdrawal, where amountRaw is the amount to deliver.
- Integer string in the token’s decimals: 6, or 18 on BNB Chain.
41250000is 41.25 USDC. - The minimum is 1 whole token after any same-chain network fee.
- Supported tokens are
USDC,USDC.eandUSDT, on the chains where each exists.
Approve mode
approveMode declares what the wallet can sign. It follows the same rule as fills, with one
difference: a withdrawal never asks the wallet to send a transaction or hold gas, in either mode.
In React, pick the mode with the same helper as fills:
"sponsored" only when your signer’s canSignEip7702Authorization(address) returns
true. useWagmiSolanaWalletSigner from @agg-build/hooks/self-custody does not declare it, so a
wallet connected through it gets "user_broadcast". useWithdrawFlow and WithdrawModal do this
for you.
Sources and destinations
✅ available, ❌ not available. “Other token” means a different token on the same chain, for example Polygon USDT to Polygon USDC.- A HyperCore destination is chain
1337, with an EVM-format address and tokenUSDC. - A Solana destination (chain
792703809, base58 address,USDC) is reached by a bridge from any source the mode allows. - When a source is not available, Max, preview and the withdrawal use the wallet’s other balances instead. They never pick a source the mode cannot sign.
What the wallet signs
Readtype on each request. The signing reference
says how to answer each type.
Each source adds its own requests. Sources on the same chain run one after another, so the second
one’s requests arrive only after the first finishes.
Fees
Every fee comes out of the withdrawn amount. The user never needs a gas token, and your app is never billed for it.
Hyperliquid charges the sender 1 USDC the first time anything is sent to an address with no
HyperCore account, on top of the amount. The wallet has to hold that extra dollar. A Max withdrawal
to a new HyperCore address therefore fails at Hyperliquid and nothing moves. Leave at least 1 USDC
behind for a first send.
The preview’s
feeRaw and the withdrawal’s expected.feeRaw are the estimates; the status endpoint
returns the actual fee once the withdrawal is final.
Max and small balances
Max leaves out any source too small to move economically. Today the floors per source are about $0.05 for a same-chain transfer ($0.10 on BNB Chain, $0.20 on Ethereum), $0.25 for a same-chain swap, and $0.50 for a bridge ($3 into Ethereum, $2.50 into Solana). An amount below Max can still be refused when splitting it across sources would leave one part under its floor. The withdrawal returns400 Insufficient balance for withdrawal. The preview
returns unviable with Requested amount exceeds withdrawable balance. Maximum deliverable is …
and sets maxDeliverableRaw, even though the amount is under that maximum. Try a slightly
different amount, or Max.
Errors and refusals
Returned on the call
These come back from the quote, preview or withdrawal as400 (or 403, 409) with { message }. The
preview returns the reasons marked “also preview” as pricingStatus: "unviable" with
unviableReason instead. A withdrawal refused while pricing it (the fee and route rows below) is
recorded as failed and moves nothing.
Reported on a failed or partial withdrawal
These come back in errorMessage on the status endpoint after the withdrawal started.
Always show
errorMessage. A declined prompt cannot be revived: start a new withdrawal.
React
Hooks
useWithdrawFlow drives the whole form (balances, Max, preview, submit). Pass signingAddress to
withdraw from that wallet. It then reads Max from the server quote, sends signingAddress and the
wallet’s approveMode, and never sends max. useWithdrawalSignatures answers the requests until
the withdrawal is final. Both need an AggWalletSignerProvider above them.
withdrawFlow.withdrawable is the wallet’s Max for the chosen token and network, in token units.
withdrawFlow.availability reports whether that quote is loading or failed. useWithdrawalSignatures
stops on a declined request, which it leaves to expire, and on a 4xx it cannot retry, both shown as
error.
Prebuilt modal
WithdrawModal from @agg-build/ui/modals adds a “Withdraw from” selector when all of these hold:
features.enableSelfCustodyWithdraw: true(off by default),features.tradingCustodyis"both"(see Prebuilt trading UI),- an
AggWalletSignerProviderwith a signer is above it, - the user has a linked EVM wallet, and the app is not in paper mode.
useWagmiSolanaWalletSigner reads the wagmi and Solana wallet-adapter contexts, so it must render
inside WagmiProvider (with its QueryClientProvider) and the Solana WalletProvider. Any other
AggWalletSigner works in its place.
The selector lists the AGG balance and each linked EVM wallet. The AGG balance stays selected by
default, so the modal behaves as before until the user picks a wallet. The selected wallet’s row
shows what it can withdraw to the chosen token and network, the same server Max the Max button fills
in. The modal then signs through the provider’s signer and shows each prompt until the withdrawal
is final. With the flag off, or any condition missing, the modal is unchanged.
FAQ
Why does Max change when I change the destination?
Max is per token and destination chain. The floors depend on the route (a bridge into Ethereum needs more than a same-chain transfer), a same-chain withdrawal of a different token is a swap with its own floor, and some sources only reach some destinations, for example deposit-wallet pUSD never goes to Polygon USDC.e. Quote again whenever the token, chain or wallet changes.Why does a MetaMask user see less than a wallet that can sign batches?
MetaMask cannot sign an EIP-7702 authorization, so it withdraws withapproveMode: "user_broadcast".
That mode never asks for a transaction, so anything that would need a token approval or a batch is
left out: the wallet’s USDC.e, USDT and pUSD, and any same-chain swap. Native USDC, HyperCore, the
legacy Safe, and (where enabled) the deposit wallet remain. rawBalanceRaw still counts everything,
which is why it can be larger than maxDeliverableRaw.
Sending to the signing wallet itself
A same-chain, same-token withdrawal from the wallet’s own balance back to the same wallet is refused withFunds are already in this wallet, and so is HyperCore to HyperCore to itself. Moving funds
to the wallet itself from its deposit wallet or legacy Safe is allowed, and so is sending to the
wallet’s own address on another chain. Because the Max quote does not know the recipient, this is
caught by the preview and the withdrawal, not by the quote.
What happens if the user declines a prompt?
Nothing is retried. The request expires, that source fails, and the withdrawal endsfailed or
partial. Funds already moved from the deposit wallet into the wallet itself stay in the wallet. Start a new withdrawal to try again.
Can the deposit wallet pay out Polygon USDC.e?
Its USDC.e can, withapproveMode: "sponsored" (or omitted). Its pUSD cannot, in either mode:
withdraw it to Polygon USDC or USDT, or to another chain, instead. Under "user_broadcast" neither
of the deposit wallet’s tokens is a source for Polygon USDC.e, so Max, preview and the withdrawal
use the wallet’s other balances.
Related
Self-custody trading
Quote and fill from the user’s own wallet.
Self-custody signing reference
Request types, a complete signer, and the per-step errors.
Funding & withdrawals
Managed withdrawals from the AGG balance.
Submit withdrawal signatures
The endpoint behind the signing loop.