Withdrawals and fees

What signs a Solana withdrawal, how the platform fee is calculated, the network-fee reservation that can shrink a full-balance send, and why a broadcast transaction that cannot be verified is parked rather than refunded.

7 min readUpdated 3 August 2026withdrawals, fees, spl, timeout, master-wallet

A withdrawal is a queue, not a request-response. The user's balance is debited durably before anything is broadcast, and the queue then moves the row through PENDINGPROCESSINGCOMPLETED. Everything below happens after the debit has already been committed, which is why the failure handling matters as much as the happy path.

Before the debit

  1. Address validation. The destination is parsed as a Solana public key. Anything that is not valid base58 is rejected with 400 Invalid Solana address: …. Nothing else about the destination is checked — see Wallets and key custody.

  2. Precision. The amount may not carry more decimal places than the token's configured precision, falling back to its decimals. Native SOL carries 9 decimals in the chain registry.

  3. Withdrawal 2FA, if the administrator has enabled per-withdrawal verification. The endpoint expects a single-use token from the verification flow when the policy demands one.

  4. The internal short-circuit. If the destination address belongs to another user on this platform, the withdrawal is processed as an internal transfer with no on-chain transaction. Withdrawing to your own address is rejected outright.

  5. The debit. The wallet row is locked FOR UPDATE, the balance is checked, and amount + platform fee is deducted in the same database transaction that creates the PENDING row.

Submits a withdrawal to an external address. Rate-limited per user as a money-movement endpoint.

The platform fee

The fee comes from the token's fee object and is max(amount × percentage, min). It is always denominated in the withdrawn currency — a USDC withdrawal is charged in USDC, a SOL withdrawal in SOL.

Network cost is not added to the user's debit on any chain, Solana included. The platform pays the network fee out of the master wallet or out of the balance being sent, and recovers it through the token fee you configure. An "estimated network fee" quoted anywhere in the interface is informational.

That means the token fee is your only cost recovery. Set it on the Solana tokens you list, or every SPL withdrawal is a small subsidy paid out of your master wallet's SOL.

Native SOL withdrawals

Signed by the customer's own Solana keypair, decrypted from their wallet_data row. The master wallet is not involved and does not need a balance.

The network fee is therefore paid out of the very balance being withdrawn, and the code reserves it explicitly:

  1. A probe transfer is compiled and priced with getFeeForMessage. If that call fails, 5,000 lamports is used as the fee.

  2. The address's on-chain balance is read at processed commitment — the freshest available view, because a staler one can pass a check that simulation then rejects.

  3. The maximum sendable amount is balance − fee. If that is zero or negative the withdrawal fails with 400 and a message naming both figures.

  4. If the requested amount exceeds the maximum sendable, the send is reduced to the maximum and a warning is logged. The transaction is then built, signed and broadcast with the reduced figure.

The platform debits amount + platform fee from the recorded balance. If that leaves nothing on-chain for the network fee, the broadcast amount is silently reduced to cover it, and the customer receives a few thousand lamports less than the amount on their transaction record.

It is a small discrepancy and it only bites on a max-out withdrawal, but it is a discrepancy, and support will be asked about it. Configuring a non-zero minimum fee on the SOL token, so a full-balance withdrawal always leaves headroom, is the practical mitigation.

SPL token withdrawals

Two keys sign every SPL withdrawal:

  • the customer's keypair authorises the token transfer out of their account;
  • the master wallet is the fee payer, and partially signs.

Before the transfer, the platform ensures an associated token account exists for the mint on both sides — the sender's and the recipient's — creating either if it is absent. The master wallet pays for both the network fee and any rent-exempt lamports that account creation costs. Sending an SPL token to a recipient who has never held it therefore costs you more than sending it to one who has.

The sender's token balance is checked before the transfer and a shortfall fails with "Insufficient SPL token balance". A missing or unusable master wallet fails with "Master wallet not found or invalid", and a missing customer key with "Sender wallet data not found".

This is the signature of an empty or disabled master wallet. Native SOL never touches it; every SPL withdrawal does. If SOL withdrawals are clearing and USDC withdrawals are not, top up the master wallet before you look anywhere else.

Confirmation, and the status that protects your float

Both paths broadcast, then confirm, then verify — and the verification is deliberately reluctant to declare failure.

  1. Confirm against the blockhash and last valid block height the transaction was built with.

  2. Verify by re-fetching the signature, up to ten times, backing off 2, 4, 6 … seconds to a 10-second cap. A transaction found with no error is a success and the row is completed with its signature.

  3. A transaction found with an on-chain error is a definitive failure. The row is marked FAILED with the error attached.

  4. Confirmed but never verifiable is treated as a success — it was confirmed, the verification calls were what failed.

  5. Neither confirmed nor verified raises WITHDRAWAL_STATUS_UNKNOWN and the row is set to TIMEOUT, not FAILED.

Once a transaction is broadcast it may land regardless of whether the platform managed to observe it. Marking that FAILED would trigger a refund for money that has already left, so it is parked as TIMEOUT instead.

TIMEOUT rows do not resolve themselves. Take the signature from the row, check it on explorer.solana.com, and settle the row by hand. A growing TIMEOUT queue on Solana is the clearest signal that the public cluster is no longer keeping up with your volume — see Network and RPC.

The queue and its recovery jobs

Solana withdrawals are serialised behind a 5-second per-chain cooldown, applied specifically to keep the platform under the cluster's rate limits. Queue depth therefore translates directly into wait time: fifty pending Solana withdrawals is a four-minute tail even if every one of them succeeds first time.

Because the queue is in-memory and the debit is already durable, a restart between the two would strand a debited row. Three jobs prevent that, all inherited from Ecosystem:

  • a boot-time sweep that re-enqueues every PENDING row before the queue starts taking new work;
  • a watchdog every five minutes for rows older than three minutes;
  • a legacy 30-minute pass running the same recovery.

Recovery is careful about the case that costs money. A PROCESSING row that already carries a signature is never re-broadcast — it is promoted to COMPLETED, because the funds have irreversibly left. A PROCESSING row with no signature and no on-chain match is reverted to PENDING for retry. Anything it cannot classify is left alone for a human.

Prerequisites checklist

Withdrawal readiness on the diagnostics page fails unless all of these hold:

    • The cluster RPC answers getHealth
    • The Solana licence is valid and the chain row is enabled
    • backend/src/blockchains/sol.ts is present
    • The Ecosystem vault is unlocked in the process serving the request
    • A Solana master wallet exists and is not disabled — required for SPL
    • That master wallet holds SOL — required for SPL

A locked vault is the one that produces the most confusing symptom: withdrawals are accepted, debited and queued, and then never sign. Check /admin/ecosystem for the Vault Active badge on every process, not just one.