Core concepts

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.

SDK
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.

SDK
const balances = await client.getManagedBalances();

Get balances.

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:

BalanceMinimum
Polymarket deposit wallet, for a Polymarket buyNone
Same chain as the venue, other stablecoin$0.25
Another chain$0.50 ($3 from Ethereum)
Any chainUnder $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.

SDK
const max = await client.getWithdrawalQuote({
  tokenSymbol: "USDC",
  destinationChainId: 8453,
});

Quote withdrawable amount.

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.

SDK
const preview = await client.withdrawPreview({
  amountRaw: "50000000",
  tokenSymbol: "USDC",
  destinationAddress: "0x2f9a4c7e1b3d6f8a0c5e9b2d4f7a1c3e6b8d0f52",
  destinationChainId: 8453,
});

Preview a withdrawal.

The response has receiveAmountRaw, feeRaw, pricingStatus (quoted or unviable), unviableReason, and quoteExpiresAt. The preview creates nothing.

Submit

SDK
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".

Withdraw funds.

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

SDK
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).

statusTerminalMeaning
pendingNoAccepted, not started.
bridgingNoMoving funds across chains.
transferringNoSame-chain transfer in progress.
completedYesDelivered. completedAmountRaw is the amount delivered.
partialYesSome sources delivered. See completedAmountRaw and sources[].
failedYesNothing 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:

RouteDebitedDelivered
Same chain, same tokenExactly amountRaw.amountRaw minus a small network fee. Never more.
Bridge or swapAbout 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:

DestinationTypical bridge fee
Polygon, Arbitrum, Base, BNB Chain, HyperCore$0.02–$0.07
Solanaabout $0.23
Ethereum$0.15–$0.31
Same-chain stablecoin swapabout $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:

  • completedAmountRaw is the amount delivered to the recipient, not the amount debited, so it can be above amountRaw. For bridged parts it is the bridge's output at execution time, not an on-chain balance reading.
  • sources[].amountRaw is the amount debited from each source, in that source's decimals. 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 chainMinimum
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.

unviableReasonWhen
Minimum withdrawal is 1 {TOKEN}The amount is under 1 token.
Minimum withdrawal is 1 {TOKEN} after feesThe amount minus the network fee is under 1 token.
Insufficient balance for withdrawalNothing 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 sourceThe network fee would use up a whole source.
Withdrawal source amount {amount} on chain {chainId} is too small for Relay feesOne 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.

On this page