Deposits and withdrawals

How SPOT money moves through KuCoin — the Main and Trade account split, network-name mapping, deposit address generation and detection, the withdrawal sequence, and the reconciliation jobs behind both.

7 min readUpdated 3 August 2026deposits, withdrawals, kucoin, networks, reconciliation

With KuCoin active, your users' SPOT balances are numbers in your database backed by coins in your KuCoin account. Nothing is held on-chain by the platform, and no keys are derived. If you want per-user on-chain custody, that is Ecosystem, a different product with a different model.

Everything on this page therefore reduces to one question: is the right amount in the right KuCoin account at the moment the platform asks for it.

Main and Trade

KuCoin splits an account into a Main (funding) account and a Trade account. The platform crosses that boundary twice, and both crossings are explicit calls in the code:

  • After a deposit is credited, it calls transfer(currency, amount, "main", "trade"). This runs after the user's balance has already been committed, and a failure is logged rather than raised — the customer keeps their credit even if the internal transfer fails.
  • Before a withdrawal is sent, it calls the same main → trade transfer and will not proceed unless the call returns an id. A failure here aborts the whole withdrawal with "Transfer to trade account failed".

The withdrawal path always tries to move funds out of Main first. If your working balance has all been swept into Trade — including by the platform's own post-deposit transfers — that transfer has nothing to move and every withdrawal fails at step one, before KuCoin is ever asked to send anything.

Keep a working balance in the Main account, and treat "Transfer to trade account failed" as a funding problem rather than a credential problem.

The key you use needs KuCoin's Transfer permission for either crossing to work. See API keys and permissions.

Network names

KuCoin identifies chains by token standard — ERC20, BEP20, TRC20, POLYGON, AVAX — where the platform and most other exchanges use chain names. Both the deposit-address path and the deposit-detection path translate before calling KuCoin:

Platform chain KuCoin network
ETH, ETHEREUM ERC20
BSC, BINANCE BEP20
TRX, TRON TRC20
POLYGON, MATIC POLYGON
AVALANCHE, AVAX AVAX
ARBITRUM, OPTIMISM, BASE unchanged

The mapping is a fallback, not the first move. The platform fetches the live network list for that currency from KuCoin and uses the requested name directly if it appears there; only then does it try the table above, then a case-insensitive match, then a partial match, then a scan of the network objects' own name fields. An unmapped chain is passed through as typed.

The consequence: the chain list a user sees on the deposit and withdraw screens is fetched from KuCoin per request, not read from the imported currency table. It reflects what KuCoin will actually accept right now — and it fails with a 503 the moment credentials stop working.

Deposit addresses

Returns a deposit address for one currency on one network. SPOT only; the chain list comes from the live exchange response.

Only networks the exchange currently reports as depositable are offered, so a chain KuCoin has paused does not appear as an option that then fails.

For KuCoin specifically the platform works down a cascade of five ccxt methods and takes the first that returns a usable address: fetchDepositAddressesByNetwork, fetchDepositAddresses, createDepositAddress, fetchDepositAddress with a network parameter, and finally fetchDepositAddress without one. Other providers use a single path; KuCoin's coverage varies by asset, and the cascade is what makes the whole listed set usable.

If every method fails, the user gets a 404 or 500 with the provider name scrubbed to ***. The real reason is in the backend log under EXCHANGE, one debug line per failed method.

Memos. Assets on memo-carrying chains need the memo as well as the address. The memo flag in the imported currency table is inferred from KuCoin's contract address field rather than from a memo field, so do not rely on it for a chain you have not checked. A deposit sent without a required memo is a support case with KuCoin, not something the platform can resolve.

How a deposit becomes a balance

Two mechanisms, one fast and one slow.

The live path. When a user opens the deposit screen, a WebSocket connection polls the exchange for their deposit every 15 seconds, for 30 minutes, then stops. For KuCoin the lookup itself is a three-attempt fallback: first fetchDeposits with the mapped chain, then without the chain parameter, and finally all deposits with client-side filtering — KuCoin's chain filter does not match consistently across assets.

The catch-up path. The processSpotPendingDeposits cron runs every 15 minutes and picks up anything the live path missed, including deposits that arrived after the user closed the tab.

Both apply the same guards: the deposit currency must match the wallet currency, and — if depositExpiration is enabled — a deposit whose exchange timestamp sits more than 15 minutes either side of the transaction, or is more than 45 minutes old, is marked TIMEOUT rather than credited. A deposit for which the exchange reports no timestamp is never expired.

The credit itself and the status flip happen inside one database transaction under a row lock, credit first, guarded by an idempotency key. Crediting twice is not possible; the main → trade transfer and the notification happen after the commit as best-effort side effects.

How a withdrawal leaves

The sequence, in order, because the order is what makes it recoverable:

  1. Validate. Precision, minimum and maximum are checked against the live KuCoin network configuration for that currency and chain, not against the imported table. An amount with more decimals than the network allows is rejected before anything is debited.

  2. Debit and commit. The user's balance is reduced and the transaction row is written in one database transaction.

  3. Mark PROCESSING with no reference. This is the crash marker: a row that is PROCESSING with a null referenceId means money left the wallet but KuCoin was never asked to send it.

  4. Check the exchange balance (advisory only). The balance read and the insufficient-funds comparison share one error handler, so an insufficient balance is warned about and the withdrawal continues. KuCoin is what actually rejects it.

  5. Transfer main → trade. Abort if this does not return an id.

  6. Withdraw, passing the address, the memo and the mapped network.

  7. Resolve the outcome. fetchWithdrawals is called to find the record and read its real fee and status. If the record is not found yet, the withdrawal is treated as completed with the fixed fee applied.

Two crons stand behind this. reconcileSpotWithdrawals runs every 5 minutes and exists solely to find rows stranded at step 3 by a process restart — confirming them with KuCoin if the send happened, refunding them if it did not. processPendingWithdrawals runs every 30 minutes over pending rows more generally.

The user-facing failure message is fixed text: "Withdrawal request failed. Please try again or contact support." The exchange's own words never reach them.

Who pays the network fee

Two settings interact.

Percentage fee added to every spot withdrawal, on top of the currency's own fee. Admin → Settings → Wallet → Fees.
When true, the platform absorbs the chain fee and sends the full requested amount. When false the chain fee is deducted from the amount the user receives. Stored in the settings table; there is no admin field for it.

At the default, a user asking to withdraw 100 USDT on TRC20 receives 100 minus KuCoin's published TRC20 fee, and the percentage fee is deducted from their wallet on top. Turning withdrawChainFee on sends the full 100 and books the chain fee against you.

The fixed chain fee is read live from KuCoin's network data for that asset. If KuCoin publishes none, it is zero — check before you enable an asset on a chain with an expensive fee.

Admin approval

If withdrawals are held for manual approval, approving one runs the same KuCoin sequence from the admin side: main → trade transfer, withdraw, then fetchWithdrawals to resolve fee and status. The chain name is converted through the platform's chain-id map (BEP20 → bsc, ERC20 → eth, TRC20 → trx and so on) and passed as the chain parameter, and the request carries a client order id of wd_<transaction id> so the send can be traced back from KuCoin's own records.

That client order id is the fastest way to answer "did this actually leave" when a customer disputes a withdrawal. Search KuCoin's withdrawal history for it before you touch the platform's records.