Funding & withdrawals
Deposit addresses, balances, and withdrawals tracked to a final state, for managed accounts
You'll need: a signed-in user (Authentication & sessions).
Result: the user's deposit addresses, a funded balance, and a withdrawal polled to
completed, partial, or failed.
In a managed account, each user gets deposit addresses on the supported chains. The user deposits to any of them, without picking a venue first. sufwan-local-test tracks the balance per chain and moves funds to the venue a trade routes to. Self-custody users fund from their own wallet instead; see Self-custody trading.
This includes a balance in the user's Polymarket deposit wallet. A Hyperliquid buy, for example, can be paid from it: sufwan-local-test moves the funds out of the deposit wallet and bridges them to Hyperliquid, with no signature from the user. The bridge fee applies as on any cross-chain move.
All calls below are user-tier: send x-app-id and the user's Authorization: Bearer token.
1. Get deposit addresses
GET /execution/deposit-addresses returns 202 with { "ready": false } while the addresses are
being created. Poll until it returns 200.
let deposit = await client.getDepositAddresses();
while (!deposit.ready) {
await new Promise((r) => setTimeout(r, 2000));
deposit = await client.getDepositAddresses();
}Get deposit addresses. 202 with ready: false, then 200 with ready: true.
decimals below is 6. BNB Chain tokens on this response are 18-decimal; read decimals.
{
"ready": true,
"evmAddress": "0x8e2d6a1c4f7b0e3d9a5c2f8b1e6d4a7c0f3b9e25",
"svmAddress": "5Hq3sT8mZr2vKc9Lw4NpXy7BdJfGa6UeRk1oQi3VtC8n",
"supportedChains": [
{ "chainId": 137, "name": "Polygon", "tokens": [{ "symbol": "USDC", "address": "0x3c49...3359", "decimals": 6 }] },
{ "chainId": 8453, "name": "Base", "tokens": [{ "symbol": "USDC", "address": "0x8335...2913", "decimals": 6 }] }
]
}evmAddress is the user's deposit address on every supported EVM chain and Hyperliquid: for a Privy wallet-provider app it is the Privy embedded wallet sufwan-local-test is authorized to sign (409 when the Privy wallet does not grant signing rights), and for other apps it is a managed wallet sufwan-local-test creates. svmAddress receives on Solana.
Only send the tokens listed for each chain. Treat supportedChains as the source of truth; it can
change. Card deposits are Onramp.
The wallets.ready webhook fires when a user's addresses are created.
2. Deposit and wait for the balance
The user sends a supported token to the address. The deposit shows in the balance after on-chain
confirmation. There is no confirm call on your side. The deposits.confirmed webhook fires for
each confirmed deposit.
const balances = await client.getManagedBalances();availableRaw, totalRaw, reservedRaw, and balanceRaw are integer strings in token decimals (125000000 with decimals: 6 is 125 USDC). positions[].balance and costBasis are display USD, not availableRaw.
{
"cash": [
{
"tokenSymbol": "USDC",
"totalRaw": "125000000",
"availableRaw": "125000000",
"reservedRaw": "0",
"decimals": 6,
"chains": [{ "chainId": 137, "balanceRaw": "100000000", "decimals": 6 }]
}
],
"positions": [{ "venue": "polymarket", "balance": 16.2, "costBasis": 15.3 }]
}availableRaw is what can be spent now. reservedRaw is held for open limit orders and trades in
progress. Pass custody=self to read the user's linked self-custody wallets instead.
To keep a minimum USDC balance on one chain after trades, see Managed balance refills.
What a buy can spend
availableRaw does not account for transfer minimums, so it can be more than a buy can spend.
A chain's balance only funds a buy when it clears the minimum for the move it needs to reach the
venue's chain:
| Balance | Minimum |
|---|---|
| Polymarket deposit wallet, for a Polymarket buy | None |
| Same chain as the venue, other stablecoin | $0.25 |
| Another chain | $0.50 ($3 from Ethereum) |
| Any chain | Under $0.05 ($0.10 on BNB Chain, $0.20 on Ethereum) is not counted |
A balance below its minimum is skipped entirely, never partly used. The destination minimums listed under Withdrawal minimums do not apply to buys.
To set a Max button, add up the chain balances that clear these minimums. Fees come out of
maxSpend, so do not subtract them. A larger maxSpend returns insufficient_balance.
A buy bridges only what it needs, so a balance under the minimum can be left on the source chain. If a buy fails after its bridge completes, the bridged funds stay in the user's managed balance on the venue's chain. They are not moved back.
3. Withdraw
Withdrawals come from the managed balance, to any address on a supported destination chain. To withdraw from a user's own linked wallet instead, see Self-custody withdrawals.
Destination chains are Ethereum (1), Polygon (137), Arbitrum (42161), Base (8453), BNB Chain
(56), Solana (792703809), HyperCore (1337) and HyperEVM (999). HyperEVM takes USDC or
USDT (paid out as USD₮0).
HyperEVM is a withdrawal destination only. It is not a deposit chain and never appears among the
deposit addresses' supportedChains: funds sent to a deposit address on HyperEVM are not credited.
Supported tokens are USDC, USDC.e, and USDT. Withdrawal amounts are 6-decimal integer strings, except BNB Chain, which is 18-decimal. Every other raw amount in the samples below is 6-decimal. The minimum is one whole token. Fees come out of the
amount.
Show the maximum
For a Max button, ask how much can actually arrive after fees:
maxDeliverableRaw of 124880000 with decimals: 6 is 124.88 USDC. Not display units.
const max = await client.getWithdrawalQuote({
tokenSymbol: "USDC",
destinationChainId: 8453,
});When the user picks Max, send "max": true on the withdrawal. The server caps the amount to what
can be delivered.
Preview before submitting
Optional, but show it before the user confirms:
amountRaw of 50000000 is 50 USDC at 6 decimals. On BNB Chain the same whole token is an 18-decimal integer string.
const preview = await client.withdrawPreview({
amountRaw: "50000000",
tokenSymbol: "USDC",
destinationAddress: "0x2f9a4c7e1b3d6f8a0c5e9b2d4f7a1c3e6b8d0f52",
destinationChainId: 8453,
});The response has receiveAmountRaw, feeRaw, pricingStatus (quoted or unviable),
unviableReason, and quoteExpiresAt. The preview creates nothing.
Submit
const withdrawal = await client.withdrawManaged({
requestId: crypto.randomUUID(),
amountRaw: "50000000",
tokenSymbol: "USDC",
destinationAddress: "0x2f9a4c7e1b3d6f8a0c5e9b2d4f7a1c3e6b8d0f52",
destinationChainId: 8453,
});amountRaw of 50000000 is 50 USDC at 6 decimals (18-decimal on BNB Chain). The response has a withdrawalId and status: "pending".
requestId is an optional UUID that makes an exact-amount withdrawal safe to retry: the same
requestId and body return the original withdrawal instead of sending twice, and a changed body
returns 409. Do not combine requestId with max. Send one on every exact-amount withdrawal:
it is what makes a timeout safe to resolve. A requestId is remembered permanently, so use a new
one for each new attempt, including after a failed withdrawal. See
Errors.
When the amount cannot be delivered, POST /execution/withdraw returns 400 with the same reason
the preview gives as unviableReason, and records a failed withdrawal that moves no funds.
Poll to a final state
const terminal = new Set(["completed", "partial", "failed"]);
let w = await client.getWithdrawalStatus(withdrawal.withdrawalId);
while (!terminal.has(w.status)) {
await new Promise((r) => setTimeout(r, 3000));
w = await client.getWithdrawalStatus(withdrawal.withdrawalId);
}Get withdrawal status. completedAmountRaw is an integer string in the destination token's decimals (6, or 18 on BNB Chain).
status | Terminal | Meaning |
|---|---|---|
pending | No | Accepted, not started. |
bridging | No | Moving funds across chains. |
transferring | No | Same-chain transfer in progress. |
completed | Yes | Delivered. completedAmountRaw is the amount delivered. |
partial | Yes | Some sources delivered. See completedAmountRaw and sources[]. |
failed | Yes | Nothing delivered. See errorMessage. |
legs[] and sources[] carry per-transfer status and txHash values.
Amounts and fees
amountRaw is the amount to deliver, not the amount to debit. How much leaves the balance depends
on the route:
| Route | Debited | Delivered |
|---|---|---|
| Same chain, same token | Exactly amountRaw. | amountRaw minus a small network fee. Never more. |
| Bridge or swap | About 2% more than amountRaw (amountRaw / 0.98). | The debited amount minus the bridge fee. Can be above or below amountRaw. |
On bridge and swap routes, every bridged or swapped part is always debited a fixed 2% buffer on top, plus a few units of rounding. The buffer does not shrink when the real fee is lower. The whole debited amount goes into the bridge, so whatever the fee does not use reaches the recipient. The user only loses the real fee.
The user's balance must cover the debit. A balance of $100 can deliver at most about $98 over a
bridge. Asking for more makes the preview unviable with maxDeliverableRaw set; nothing is
debited. max: true does this calculation for you.
The real bridge fee is roughly a flat dollar amount, set mostly by the destination chain:
| Destination | Typical bridge fee |
|---|---|
| Polygon, Arbitrum, Base, BNB Chain, HyperCore | $0.02–$0.07 |
| Solana | about $0.23 |
| Ethereum | $0.15–$0.31 |
| Same-chain stablecoin swap | about $0 |
These are measured values, not guarantees. Because the fee is flat, it is a large share of a small withdrawal and a tiny share of a large one:
- A 1 USDC bridge to Polygon debits about 1.0204 and delivers about 0.99. The fee was more than
the 2% buffer, so the recipient gets less than
amountRaw. - A 100 USDC bridge to Polygon debits about 102.04 and delivers about 101.99. The fee was less
than the buffer, so the recipient gets more than
amountRaw.
receiveAmountRaw in the preview is an estimate. The route is priced again when the withdrawal
runs, so the delivered amount can differ slightly.
There is no mode that debits exactly amountRaw and delivers amountRaw minus fees. max: true
withdraws the whole withdrawable balance and delivers that minus fees.
On the status response:
completedAmountRawis the amount delivered to the recipient, not the amount debited, so it can be aboveamountRaw. For bridged parts it is the bridge's output at execution time, not an on-chain balance reading.sources[].amountRawis the amount debited from each source, in that source'sdecimals. Convert each to the same decimals, then add them up for the total debited.
Minimums
Every withdrawal must be at least 1 token. On top of that, each chain's balance only counts toward a withdrawal when it clears a minimum for the kind of transfer it needs:
| Transfer from that chain | Minimum |
|---|---|
| Same chain, same token | $0.05 ($0.10 on BNB Chain, $0.20 on Ethereum) |
| Same chain, other stablecoin | $0.25 ($10 on HyperCore) |
| Another chain | $0.50 ($2.50 into Solana, $3 into Ethereum) |
A balance below its minimum is left out of max and of maxDeliverableRaw. A small leftover on
one chain, for example $0.25, cannot be withdrawn on its own until the user adds more to that
chain.
Unviable reasons
When pricingStatus is unviable, unviableReason explains why. Show it to the user, but do not
match on the text, since the wording can change. For logic, check pricingStatus and, when set,
maxDeliverableRaw.
unviableReason | When |
|---|---|
Minimum withdrawal is 1 {TOKEN} | The amount is under 1 token. |
Minimum withdrawal is 1 {TOKEN} after fees | The amount minus the network fee is under 1 token. |
Insufficient balance for withdrawal | Nothing is withdrawable. |
Requested amount exceeds withdrawable balance. Maximum deliverable is {amount}. | The amount is above what can be delivered. maxDeliverableRaw is set. |
network fee exceeds the amount for this source | The network fee would use up a whole source. |
Withdrawal source amount {amount} on chain {chainId} is too small for Relay fees | One part is too small to bridge. |
Not enough bridge liquidity to withdraw {amount} from chain {chainId} right now. Try again shortly. | The bridge has no capacity right now. |
No safe route is available to withdraw from chain {chainId} right now ({reason}). Try again shortly. | Every available route failed a safety check. |
Withdrawal amount is too small for required fees. | Fallback when no more specific reason applies. |