For the complete documentation index, see llms.txt. This page is also available as Markdown.

Withdrawals

Return funds from a user's manager to their external wallet — wallet-level and per-order.

Funds in a user's manager always come back to that user's own external wallet — the manager's policy allows nothing else. There are two ways to move them, both following the same intent → sign → confirm pattern as order creation.

Wallet-level withdrawals

These pull a chosen token out of the manager regardless of any order. Use them to power a "withdraw available balance" action.

Check what's available first:

const { data } = await callTitanDca('/me/balance?hideZero=true', { sub });
// each balance row: totalBalance, lockedForFutureTxns, withdrawalPending, availableToWithdraw

availableToWithdraw is max(total − locked − withdrawalPending, 0) — the source of truth for a "withdraw max" button. Active orders lock their unspent input, so it won't be available until the order ends: for a DCA order or a running Slice Order that's totalAmount − amountSpent, and for a trigger order it's the full unspent amount while it's armed. Output mints are not locked, with one exception: tokens bought by a conditional entry stay locked from the moment its swap is sent until they become the bracket's amount.

lockedBreakdown on each row attributes the lock to specific orders, with orderType being any of dca, stop_loss, take_profit, oco, or slice — enough to tell the user which order is holding their balance.

Build the withdrawal tx, omitting amount to withdraw the max:

const intent = await callTitanDca('/withdraw/transaction', {
  method: 'POST',
  sub,
  body: {
    userPubkey,                                            // fee payer & destination
    tokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
    amount: '400000',                                      // optional; omit for max
  },
});

// user signs intent.data.transaction, then:
await callTitanDca('/withdraw/confirm', {
  method: 'POST', sub,
  body: { signedTransaction },
});

Titan re-runs the lock-aware check at confirm time using the (mint, amount) from the unsigned transaction it returned — so if that transaction goes stale during its 5-minute window (e.g. another withdrawal landed in the meantime), it's rejected before broadcast rather than overdrawing.

HTTP

error.code

When

400

INSUFFICIENT_AVAILABLE

The whole balance is locked, or the wallet is empty.

400

FUNDS_LOCKED

requested > available — some balance is locked by orders or a pending withdrawal. details has the breakdown.

400

INSUFFICIENT_BALANCE

requested > chainBalance — the wallet genuinely doesn't hold that much.

409

ONBOARDING_INCOMPLETE

Manager not fully provisioned. Re-call onboard, then retry.

Order-level withdrawals

These return the funds tied to a single terminal order (completed / cancelled / failed / expired) back to the external wallet.

failed orders — and expired trigger orders — return automatically. Titan sends their unspent input back without any call from you (see Lifecycle), so calling withdraw on one usually reports NOTHING_TO_WITHDRAW or ALREADY_WITHDRAWN. Order-level withdrawal is for completed and cancelled orders — plus expired DCA orders, which are the one expiry case that isn't auto-returned.

A completed trigger order or Slice Order is also a no-op here: its fills consumed the whole deposit and the output was delivered at execution time — or belongs to a bracket order — so it returns 400 NOTHING_TO_WITHDRAW. For trigger orders, order-level withdrawal really only matters after a cancel. Slice Orders cannot be cancelled, so it matters only in the brief window before a failed one is auto-returned.

To exit a bracket early, cancel the bracket order with withdraw: true — its withdrawal returns the bought tokens. See Conditional Entry & Bracket.

POST /orders/{orderId}/withdraw is safe to retry — each call rebuilds a fresh tx (so the user can re-prompt their wallet) and resets the inactivity timer on the automatic recovery process. withdrawalAmounts enumerates the per-mint amounts the transaction will move, one entry per non-zero mint, which is what you show the user before they sign.

HTTP

error.code

When

400

NOTHING_TO_WITHDRAW

No order-owned funds remain (the user likely drained them via a wallet-level withdrawal).

400

INVALID_STATE

Order isn't completed / cancelled / failed / expired.

400

ALREADY_WITHDRAWN

A prior withdrawal already completed — including the automatic return on a failed order or an expired trigger order.

409

EXECUTION_IN_FLIGHT

A swap attempt is still being reconciled. Self-resolving — retry shortly.

503

RPC_UNAVAILABLE

Couldn't fetch on-chain balance. Retry shortly.

Releasing a stuck withdrawal lock

If the user dismisses the wallet prompt from POST /orders/{orderId}/withdraw, the order sits in withdrawalStatus: pending. Call POST /orders/{orderId}/withdraw/abandon to make the "Withdraw" button usable again immediately — it's idempotent. Without it the lock clears on its own, but not quickly — expect up to 8 minutes. That's long enough for a user to notice, which is why abandon exists.

HTTP

error.code

When

400

ALREADY_WITHDRAWN

The withdrawal already completed — nothing to abandon.

404

NOT_FOUND

Order missing or not yours.

409

WITHDRAWAL_IN_FLIGHT

Lost the race to /withdraw/confirm. Retry briefly.

Last updated