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.
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 → tradetransfer 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
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:
-
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.
-
Debit and commit. The user's balance is reduced and the transaction row is written in one database transaction.
-
Mark PROCESSING with no reference. This is the crash marker: a row that is
PROCESSINGwith a nullreferenceIdmeans money left the wallet but KuCoin was never asked to send it. -
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.
-
Transfer
main → trade. Abort if this does not return an id. -
Withdraw, passing the address, the memo and the mapped network.
-
Resolve the outcome.
fetchWithdrawalsis 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.
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.