Float, treasury and reconciling the ledger against the account
The per-asset procedure for checking that your Binance account still covers what customers are owed, why the balance screen under-reports, and the only lever that stops the gap growing.
Every SPOT balance on your platform is a ledger row. The asset behind it is a
number in one Binance account. Nothing in the product compares the two, and the
first time the difference matters is the first withdrawal you cannot pay.
This page is the procedure that stops that being a surprise. It is manual, it is per-asset, and there is no screen that does it for you — the platform deliberately refuses to add BTC to USDT to produce a confident wrong number.
A shortfall does not announce itself. Deposits keep crediting, orders keep filling and the trade screen keeps working while the account drains. The only symptom is a failed payout, and by then the customer is already debited.
The two numbers
| Side | Where it comes from | What it is |
|---|---|---|
| The pot | /admin/finance/exchange/balance — a live fetchBalance on your Binance account |
asset, available, inOrder, total |
| The claims | wallet rows where type = 'SPOT', grouped by currency |
balance (spendable) plus inOrder (held behind open orders) |
They are only comparable per asset. A dollar total of either side needs a price for every asset, and this is the one place a rounded conversion turns a real shortfall into a comfortable-looking number.
The reconciliation, per asset
-
Read the pot. Open
/admin/finance/exchange/balance(Quick Actions → Balances on the exchange hub, or the URL directly). It callsfetchBalanceon every load, so it is live rather than cached.Live balances on your Binance account, filtered to non-zero holdings.Record
availableandinOrderfor each asset.inOrderhere is Binance's own hold — funds locked behind orders you have resting on Binance, which for a platform install is the customer orders your integration forwarded. -
Read the claims. Open
/admin/finance/walletand filter Type toSPOT. Each row carriescurrency,balanceandinOrder. What you owe in an asset is the sum ofbalance + inOrderacross every SPOT row in that currency.The Analytics tab has a Custodial balance by currency ranking, and it is the closest thing to a report — but read the caveats below before you use it as the answer.
-
Compare, asset by asset. For each currency your customers hold:
claims = SUM(wallet.balance + wallet.inOrder) WHERE type = 'SPOT' pot = exchange balance `total` for that asset cover = pot - claimsA negative
coveris a shortfall in that asset. It does not become less of a shortfall because another asset is in surplus — you cannot pay a BTC withdrawal with USDT. -
Check what is spendable, not just what exists. A payout can only use
available. An asset wheretotalcovers the claims butavailabledoes not is a payout failure waiting to happen, and the fix is different: you do not need more coins, you need the ones you have to stop being locked. -
Write the numbers down. There is no history anywhere in the product. Last week's figures exist only if you kept them, and the trend is what tells you whether the gap is drifting or was always there.
The wallet analytics card, honestly
Custodial balance by currency groups the wallet table by currency, sums
balance, ranks the top 8 and drops the date window entirely. Three things
follow:
- It does not respect the table's Type filter. The analytics request sends
the model and the page's
modelConfig, not the active column filters, and the wallet page sets nomodelConfig. So the bars mixSPOT,ECO,FIATandFUTURESrows of the same currency code. - It sums
balanceonly. A ranked breakdown carries one aggregate, so theinOrderleg is missing. Read a bar as "free balance", not as total liability. - It is a top 8. A long tail of small currencies is not shown at all, and a small currency is exactly where an undetected shortfall hides.
Use it to spot a concentration, not to close a reconciliation. For the real figure, filter the table to SPOT and read the rows, or query the database.
Why the balance screen under-reports
The balance endpoint builds its list from fetchBalance and then applies this
filter:
keep the row only if available > 0 OR inOrder > 0An asset where both are zero is removed from the response. It does not render as a zero row.
So the single most dangerous case — customers hold 4 BTC in the ledger and the account holds none — looks identical to "we have never listed BTC". The screen is silent about exactly the asset you most need it to shout about.
The defence is to drive the comparison from the claims side. Take the list of
currencies your customers actually hold from /admin/finance/wallet, and look
each one up on the balance screen. An asset that is missing there is a 100%
shortfall, not an absence of data.
What a payout can actually reach
The withdrawal path reads balance.free[currency] — the free spot balance.
That is the only pool it can see and the only pool it can spend.
Invisible and unusable:
- Earn, in every form — flexible, locked, staked.
- Funding, and anything held for Binance Pay or P2P.
- Sub-accounts. The credentials belong to one account.
- Funds locked by your own open orders. Those show as
inOrderon the balance screen, which is exactly why that column is on it.
The KuCoin branch of the same handler transfers from main to trade before it
withdraws. The Binance branch does not — it calls withdraw directly. What is in
spot is what can leave.
If you park the float in Earn for yield, you have to move it back to spot yourself, before the payout, by hand. Nothing in the platform will do it and nothing will warn you that it needs doing.
The shortfall failure surface
There are two payout paths and they fail differently. Which one you are on is
decided by the withdrawAutoApprove setting (Admin → System → Settings →
Wallet → Security, Auto-Approve Withdrawals; legacy installs may still be
reading withdrawApproval).
With auto-approval on, the customer's own request calls Binance immediately. It first reads the free balance and compares it to the amount being sent, and the message it prepares is:
Insufficient exchange balance. Available: X BTC, Required: Y BTC. Please contact support to refill the exchange account.
That check sits inside the same try block that catches "could not fetch the
balance", and the catch logs a warning and continues with the withdrawal
attempt, deliberately leaving the balance decision to Binance. So the message
above is not reliably what the customer sees. What they usually get is Binance's
own refusal, mapped to:
| Binance says | Customer sees |
|---|---|
error -4026 |
Insufficient funds available for withdrawal. Please try a smaller amount or contact support for assistance. |
any message containing insufficient |
Insufficient funds available for withdrawal. Please contact support for assistance. |
| anything else | Withdrawal request failed. Please try again or contact support. |
With auto-approval off — the default, and the safer one — there is no
exchange-balance pre-flight at all on the admin approval path. It checks the
customer's wallet balance, claims the row, and calls withdraw. A shortfall
surfaces as Binance's error wrapped in Withdrawal failed: <message> with a 500.
The wallet debit commits when the customer submits the request. Everything after that — approval, the exchange call, the result — happens against money that has already left their balance.
That is why every failure path credits it back: no withdrawal id, a FAILED
status or a CANCELLED status all trigger a refund of the full debited amount
under the idempotency key withdraw_approve_refund_<transactionId>, and the
transaction is marked REJECTED with a failureReason in its metadata.
The consequence for you is that a shortfall reads as a transient failure. The customer is made whole and is invited to try again, and nothing in the panel says "the account is empty". Only this reconciliation says that.
The two fee streams that move the float
Both are configured in different places and they pull in opposite directions.
The percentage fee grows the float. It is exchangeCurrency.fee for that
currency plus spotWithdrawFee, applied to the requested amount and added on top
of the debit. The customer pays it; you keep it. It is booked on settlement,
not at request time, so a rejected withdrawal refunds the whole debit and never
credits the fee — that ordering exists because booking it early minted money on
every rejection. Super Admin withdrawals skip it entirely.
The network fee drains the float, or does not, depending on one setting. With
withdrawChainFee enabled the customer receives the full requested amount and
your Binance account pays the chain fee on every single payout. With it disabled
— the default — the fee is subtracted from what is sent. On a chain with an
expensive withdrawal fee and a lot of small payouts, that switch is the
difference between a float that holds and one that bleeds.
Deposit fees are not revenue. When Binance keeps a cut of an incoming
transfer, that amount is subtracted before the wallet is credited and recorded as
a fee with no platform profit entry, because the platform never received it. The
fee is clamped to the deposit amount, and a deposit that nets nothing is marked
FAILED with an explanation instead of retrying forever.
A cadence, and a stop rule
Nothing here is enforced by the product. It is the routine that keeps the routine from being an incident.
Daily
- Open
/admin/finance/exchange/balance. Confirm the assets you list are present and thatavailableis not near zero for any of them. - Open
/admin/finance/withdraw/log. It defaults toPENDING, oldest first — the age of the oldest row is your queue health. - Check Admin → System → Cron that
reconcileSpotWithdrawalsandprocessPendingSpotOrdershave run recently and without an error. While the Redis ban key is set they report success and do nothing; see Permissions, background jobs and Redis keys.
Weekly, and after any unusual volume
- Run the full per-asset comparison above and record it.
A top-up threshold worth setting
Pick a per-asset cover ratio and act on it rather than on a feeling. A workable
starting point is: top the account up when available falls below the larger
of (a) 120% of the claims in that asset, or (b) enough to cover your largest
single historical payout in that asset three times over. The second half matters
more than the first — a float that covers the total but not the biggest single
request still fails visibly.
Whatever you choose, the trigger has to be a number you check, because there is no alert.
The stop rule
When cover in an asset is gone, there is exactly one lever in the product that stops new claims accruing in it: disable the currency.
Admin → Finance → Currency Management → Cryptocurrencies
(/admin/finance/currency/spot), then toggle status off for that currency.
What that actually does, precisely:
- The currency listing endpoint filters on
status: true, so the currency disappears from the deposit and withdraw selectors. Customers can no longer start either. - It does not delete anything. Existing
SPOTwallet rows and their balances are untouched, and the customer still sees the balance. - The deposit-create and withdraw-create routes look the currency up by code and
do not re-check
status. Removing it from the picker is what stops new requests, not a server-side refusal. - The currency import never overwrites
status. Re-importing will not switch it back on.
Two broader levers, in escalating order:
| Lever | Where | Effect |
|---|---|---|
| Turn auto-approval off | Admin → System → Settings → Wallet → Security, Auto-Approve Withdrawals | Every payout waits for a human. It does not stop the debit — customers are still debited on request |
| Turn spot trading off | Admin → Trading → Settings → General → Features, Enable Spot Trading (spotWallets) |
The SPOT wallet, deposit and withdraw screens stop appearing at all |
Disabling markets on /admin/finance/exchange/market removes pairs from the
market list and the ticker stream, but the spot order-create route never reads
market.status — so it stops new customers finding a pair, not a client that
already holds the symbol. Treat it as presentation, not enforcement.
What we could not determine
- There is no stored history of exchange balances. Every reading is as-of now, and comparing two dates means having recorded the first one yourself.
- No screen, endpoint or export produces the per-currency SPOT claim total directly. The wallet table and its by-currency ranking are the closest, with the caveats above; anything exact comes from the database.
- Nothing anywhere records which Binance account or sub-account a given ledger row was backed by. After a credential change or a provider switch there is no audit trail tying a balance to the account that held it.
Related
- Deposits and withdrawals — how coins get in and out, and every way a payout can fail
- The spot desk — the queues and screens named above
- Switching the active exchange provider — moving the float between accounts, which is the same problem at ten times the size
- Permissions, background jobs and Redis keys