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.

10 min readUpdated 6 August 2026float, reconciliation, treasury, balances, shortfall

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

  1. Read the pot. Open /admin/finance/exchange/balance (Quick Actions → Balances on the exchange hub, or the URL directly). It calls fetchBalance on every load, so it is live rather than cached.

    Live balances on your Binance account, filtered to non-zero holdings.

    Record available and inOrder for each asset. inOrder here 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.

  2. Read the claims. Open /admin/finance/wallet and filter Type to SPOT. Each row carries currency, balance and inOrder. What you owe in an asset is the sum of balance + inOrder across 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.

  3. 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 - claims

    A negative cover is 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.

  4. Check what is spendable, not just what exists. A payout can only use available. An asset where total covers the claims but available does 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.

  5. 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 no modelConfig. So the bars mix SPOT, ECO, FIAT and FUTURES rows of the same currency code.
  • It sums balance only. A ranked breakdown carries one aggregate, so the inOrder leg 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 > 0

An 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 inOrder on 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.

Percentage added to every spot withdrawal, on top of the currency's own fee. Debited from the customer's wallet and booked to the platform at settlement. The settings UI is a 0-10% range.
True makes your Binance account absorb the network fee and sends the customer the full requested amount. False subtracts it from what is sent. There is NO admin field for this key.

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

  1. Open /admin/finance/exchange/balance. Confirm the assets you list are present and that available is not near zero for any of them.
  2. Open /admin/finance/withdraw/log. It defaults to PENDING, oldest first — the age of the oldest row is your queue health.
  3. Check Admin → System → Cron that reconcileSpotWithdrawals and processPendingSpotOrders have 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

  1. 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.

Toggles one spot currency on or off.

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 SPOT wallet 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.