Pool backing — what the exchange owes against what it holds
Why a Spot balance and a Funding balance are backed by different things, how a transfer between them opens a gap, what the pool-backing ledger records, what the reconciliation measures every fifteen minutes, how the settlement engine and its treasury close the gap in Auto mode, and the doors the console gives an operator.
A Spot balance is a number in your database backed by one pooled account on your exchange provider. A Funding (ECO) balance is a number backed by on-chain custody your platform holds — the customer's own deposit address, your master wallet. Most things that change one of those numbers move the asset behind it at the same time: a verified exchange deposit, a broadcast withdrawal. A few move the number and nothing else:
- an ECO → Spot transfer leaves the coins at the customer's deposit address while the exchange now owes them;
- a Spot → ECO transfer leaves the coins on the exchange while the customer's Funding wallet is owed them;
- a FIAT → Spot transfer creates a Spot balance backed by fiat sitting at your PSP or bank;
- an admin credit creates a balance backed by nothing at all.
Spot orders go to the exchange from that one pooled account, so none of this fails the next trade. It quietly makes the pool under-collateralised: trades keep succeeding out of other customers' money until the day the account runs short, and then somebody's withdrawal is refused by the exchange while your screen says they have the funds. Pool backing exists so that gap is explained row by row instead of discovered as one unexplained number.
Only two things on this page move money, and both are explicit: Settle now (you, one currency, watching) and mode Auto (the engine, every five minutes, within the bounds below). Everything else records, measures, alerts, and lets you say what you did by hand.
What the ledger records
Every one of the moves above writes one obligation row, in the same database transaction as the balance change, so a crash cannot leave a transfer the ledger never heard of. The row carries a signed amount:
- positive — the exchange pool is short by this much (an ECO → Spot or FIAT → Spot transfer, an admin credit on Spot);
- negative — the exchange holds this much more than it owes (a Spot → ECO transfer, an admin debit).
The amount is what the receiving wallet was credited. The transfer fee stays on the paying side and is credited to your Super Admin wallet there, so the paying pool's liability falls by exactly what the receiving pool's rises.
Each row also records its source, and the source decides what may close it:
| Source | Written by | Closed by |
|---|---|---|
transfer |
ECO ↔ Spot, same currency | a settlement movement — the engine's (Auto, or Settle now) or one you record |
conversion |
ECO USDT → Spot BTC and the like: two rows, one per asset | the source-asset leg by a movement; the target-asset leg by you trading on the exchange and recording it |
fiat_transfer |
FIAT ↔ Spot, FIAT ↔ ECO | only by you: buy the coins and record the movement |
admin |
a manual credit or debit; a deposit approved by hand that the exchange did not confirm | recording a movement, or waiving as recognised loss |
An admin approval of a Spot deposit is checked against the exchange before it is classified: if the exchange lists that deposit as accepted, the row is written already settled, with the exchange's evidence on it. If it does not — the customer typed a hash the exchange never saw — the row stays open.
What the reconciliation measures
Every fifteen minutes (Admin → System → Cron → Pool Backing Reconciliation, or the page's Run reconciliation button), for each currency:
- Liabilities — every Spot wallet's balance and held amount, your Super Admin's included and shown as its own line, plus withdrawals that were debited but not yet sent to the exchange, plus the parked principals: money that left a Spot wallet into another store while the coins stayed on the exchange — copy-trading binary allocations, active investments and AI investments, synthetic staking positions, live forex accounts and their investments, and fx-trading balances derived from the ledger. Each is counted in the Spot currency it left and shown as its own line with a breakdown; a store whose extension is not installed counts 0 with a note.
- Exchange holds — the exchange account's total, summed across every account type the provider splits its balance across: KuCoin's main and trade accounts, Binance's spot and funding, OKX's trading and funding. A KuCoin deposit awaiting verification sits in main; a reading of trade alone would report it as a hole. If any account cannot be read, holdings are unknown for that run — no gap is computed, no drift is recorded, no alert is raised for that run, and the page says so. An unreadable exchange is not a shortfall; only once it has been unreadable for more than three runs in a row (the fourth) does a stale-exchange alert go out, once a day.
- Gap — liabilities minus holdings. A settlement in flight needs no adjustment: the rows it claimed stay among the open obligations until it is settled.
- Unexplained — the gap minus the open obligations, minus the waived ones (a waived obligation is a recognised loss that still explains the gap, shown as its own Waived line under the open obligations). The part no row accounts for, this run; the page shows beside it how many consecutive runs it has held ("n runs") toward the drift threshold.
- Drift — an unexplained residual that has stayed outside the currency's precision for three consecutive runs (the number is the Drift Confirmation Runs setting), whichever sign it shows on each run. A deposit the exchange credited minutes before your database did is a transient, not a finding, and never becomes drift; a residual that keeps flipping sign while staying outside precision is not a transient, and it is recorded like a stable one so that it can be acknowledged.
Drift is a measurement, not an obligation. It is never netted, never
settled, and the platform will never move coins to cover it. On this platform
it has real sources of its own — the exchange's trading and withdrawal fees,
deposit fees, and every balance the treasury pays out that no exchange
movement backs (investment returns, staking rewards, affiliate rewards, binary
wins) — and the honest treatment is to review it and acknowledge it. An
acknowledged drift that later grows past the acknowledged amount counts as
unacknowledged again. Alerts — new drift per currency, a USD gap above
poolBackingAlertUsd per currency, a settlement parked for review, the
stale exchange — go to the Admin and Super Admin users who hold
view.pool.backing (a Super Admin always), once per subject per day. New
drift is the exception: it is delivered once per time it is recorded, so a
drift that clears in the morning and is recorded again in the afternoon is a
second notification, not a duplicate of the first.
The settings
Admin → System → Settings → Wallet → Pool backing.
| Key | What it does | Default |
|---|---|---|
poolBackingMode |
off records only; monitor records, reconciles and shows the console; manual also refuses an ECO → Spot or FIAT → Spot transfer that would push a currency's open obligations past the cap, and opens the settle doors; auto lets the engine settle on its own (see The engine) |
monitor |
poolBackingCapUsd |
The most a currency's unsettled ECO → Spot and FIAT → Spot transfers may add up to, in USD, before further ones are refused (manual and auto only). Empty = no refusal; gap alerts come from poolBackingAlertUsd alone. A per-currency override is set from the page |
empty |
poolBackingThresholdUsd |
The smallest net obligation worth a settlement movement | 50 |
poolBackingMaxSettlementUsd |
Max Settlement (USD): the largest single movement the engine plans, whoever pressed the button — the cron in auto, Settle now and Convert now in manual; a bigger net is settled in instalments, one per cycle. Empty removes the bound. Only Record external and Mark arrived, which record a movement you made by hand, are unbounded |
10000 |
poolBackingAutoConvert |
Lets the engine buy the exchange-side asset of a conversion on the venue (see Conversions) | off |
poolBackingCustodyReadsPerRun |
How many customer addresses per chain the reconciliation reads on chain each run, in rotation | 50 |
poolBackingMasterReserve |
A JSON map keyed by ecosystem chain name (ETH, BSC, TRON, SOL, XMR, …, not the coin symbol) to the native units the master wallet must keep after a settlement draws on it — as the fallback source of a native settlement, or as the gas payer of a TRC20 or SPL settlement — e.g. {"ETH": 0.05, "TRON": 20}. A coin or rail spelling (TRX, TRC20, BEP20, MATIC) is folded onto its chain; absent chains use built-in floors |
{} |
poolBackingAlertUsd |
Notify admins when a currency's gap exceeds this, in USD | empty |
poolBackingDriftRuns |
Consecutive runs a residual must survive to become drift | 3 |
poolBackingPause |
The kill switch: while on, no settlement is started or marked settled; the ledger and the reconciliation keep running | off |
poolBackingMode, poolBackingCapUsd, poolBackingMaxSettlementUsd,
poolBackingAutoConvert, poolBackingMasterReserve and poolBackingPause
can be changed only by a Super Admin.
The refusal a customer sees at the cap names the currency and what is waiting to be settled, and points a crypto holder at the Spot deposit flow and a FIAT → Spot customer at the fiat deposit flow — a transfer refused at the cap loses nothing.
The console
Admin → Finance → Pool Backing — permission view.pool.backing to see it.
-
Run reconciliation — reads the exchange now rather than waiting for the next tick. One run at a time: a Redis lock lets one run through, and a run started while another is still going (the cron's, or another admin's) is skipped and says so. The lock is all the two share — the button's run executes in the web process and paces its chain reads with that process's own buckets, beside the cron process's scanner, so it is a second read budget against the same public endpoints, not a share of the cron's. If Redis cannot be reached the run proceeds unlocked and the log says so. Permission
edit.pool.backing. -
Record external movement — you moved coins between the pools yourself: sent custody coins to the exchange's deposit address, withdrew from the exchange to a custody address, or bought coins on the exchange to cover a fiat transfer. Record what arrived, in which direction, with its proof (a transaction hash, an exchange deposit id, a bank reference); the currency's open obligations of that direction are settled oldest first. A row the amount only partly covers is split: the covered part settles, the rest stays open. This is the only way a
fiat_transferoradminrow closes, and on an install without the Ecosystem addon the only settlement door there is. Permissionedit.pool.backing. -
Acknowledge drift — records who accepted the currency's persistent unexplained gap, and at what amount. Permission
edit.pool.backing. -
Waive — recognise an open obligation as a loss instead of settling it. It stays on the ledger and keeps explaining the gap (the Waived line; it is never in the cap). A reason of at least ten characters is required, the admin who caused an admin credit cannot waive it — another admin with
manage.pool.backingmust — and a fresh second factor is always required, whatever the withdrawal 2FA settings say: the dialog reads the policy, sends the code by email or SMS or asks for the authenticator's, verifies it, and only then waives. An admin with no enabled second factor is refused outright and told to enrol one; a platform with two-factor authentication switched off cannot waive at all. The code confirms that one obligation only — verifying to waive one row does not verify another — and is used once. Permissionmanage.pool.backing. -
Settle now — one currency, now, with you watching: the engine's own planning, claim and dispatch for that currency (direction and chain chosen for you, or picked in the dialog), in
manualorautomode. Bounded bypoolBackingMaxSettlementUsdlike the cron's own movements: a bigger net goes in instalments, one press per instalment. Permissionedit.pool.backing. -
The review doors on a parked settlement — attach hash (you found the transaction the platform lost track of), mark arrived (the receiving side shows it; the amount you enter is what it received, and the difference to what was requested is booked as fees), mark failed (nothing ever reached the chain or the exchange; the obligations reopen). A row that carries a hash, an exchange withdrawal the provider has queued, a dispatch that had started, or a conversion order the exchange accepted or may have accepted asks you to confirm that nothing reached the chain or the exchange — or that no order filled — before it can be failed, because FAILED reopens the obligations (they would be shipped, or bought, again) and releases the hash from the deposit guard; a row that is mid-dispatch cannot be failed at all. Mark failed needs
manage.pool.backing; the other twoedit.pool.backing.
Three permissions. A seeder grants two of them from keys your roles already
hold, on every seed run: view.pool.backing to every role holding
view.deposit, and edit.pool.backing to every role holding both
edit.deposit and edit.withdraw — a role that decides customer money in
one direction only does not acquire the other sideways. manage.pool.backing
(waive, mark failed) is granted to nobody: a Super Admin holds it by name and
grants it by hand. The seeder only inserts — it never revokes, and a grant
you made by hand stays — so a role holding neither witness is the only one
you grant view or edit to yourself.
The engine
The treasury
The platform's own reserve is a Funding wallet per currency under a system
account: pool-backing@treasury.invalid, a fixed id, no password (nobody can
sign in), the plain User role. It is created the first time the engine needs
it, through the same path a customer's Funding wallet takes, so it has real
per-chain addresses that the deposit monitors, the UTXO pool and the
withdrawal movers all see. That is deliberate: coins withdrawn from the
exchange land where the platform can watch them arrive, and a customer's
withdrawal may be paid from the treasury's address like from any other custody
address — the ledger records that it lent them. The master wallet is not the
treasury (nothing watches it for arrivals; it is the gas payer), and neither
is the Super Admin's wallet (that is fee revenue).
The treasury card on the page lists, per currency and chain, the address, the
exchange network it maps to, whether the engine can send on it and receive on
it, and what is wrong when it cannot: an unmapped chain, a network the
exchange has disabled for deposits or withdrawals, a network whose deposit
address needs a memo on any chain but TON (only the TON sender carries one;
every other chain is refused at planning), a chain with no platform signer.
The card also flags any poolBackingMasterReserve key that names no
ecosystem chain once a coin or rail spelling has been folded onto its chain:
the entry is kept, but no signer will ever look it up.
The treasury account, and what is closed
The treasury is a user row, and so is the AI market maker's pool account
(ai-market-maker@pool.invalid, which holds futures margin). Both are listed
in the CRM with a "System account" badge, and both refuse every door that
would let a person operate them, whoever is asking — a Super Admin included:
- Signing in, by password, Google, wallet, one-time code or mobile app, answers the same "Incorrect email or password" a wrong password gets. Password reset accepts the address and sends nothing; email verification mints no code for it; a session or API key naming it is refused on arrival.
- The CRM editor will not change its email, password, role, status or verification flags, and the row cannot be deleted, restored, purged, blocked, suspended, banned, or have its 2FA or Transfer PIN reset. The refusal says which account it is and why.
- Its wallets cannot be deleted (even at zero balance), frozen or edited by hand, and no withdrawal or transfer naming it is approved, rejected or settled by hand. A customer cannot transfer to it by id, and the transfer form says so.
- Nothing automatic touches it: no mail (the mailer drops the address before a connection is opened), no in-app or push notification, no daily wallet PnL row, no KYC decision.
- Registration, the admin user editor and the CSV import refuse any
.invalidaddress, and the two usernames are reserved, so nobody can hold the row's place before the engine creates it. If a row from before this release does, the engine names it in a 409 with the hard-delete to run.
What an admin can still do: read the row and its wallets, and adjust a wallet balance — that door stays open because the pool-backing ledger records every adjustment as an admin obligation, and the treasury has to be fundable by hand when a reconciliation calls for it. The response says the wallet belongs to a system account and that nothing was mailed or notified.
The network map
The exchange names networks its own way (TRC20, BEP20, ERC20); the
ecosystem names chains its way (TRON, BSC, ETH). The built-in table
covers KuCoin, Binance, OKX, XT and Bybit for the common rails; the
per-currency network map on the page (chain → the exchange's network id)
overrides it and is what you set for anything the table does not know. A
leg whose network is unmapped, disabled, below the exchange's withdrawal
minimum, or needs a memo on a chain that cannot carry one (every chain but
TON) is refused at planning, before anything is claimed, with the reason on
the currency row.
What a cycle does
Every five minutes (Pool Backing Settlement), and on Settle now:
- Verify every movement in flight, in every mode above
off. An outbound movement is looked for in the exchange's deposit history by hash, and by address and tag when the exchange reports them; when the exchange lists it as accepted, the settlement is marked settled with what arrived. An inbound movement is followed through the exchange's withdrawal status to its hash, then the treasury wallet is scanned for the credited deposit row with that hash — the same credit path a customer deposit takes, so the treasury's balance is right by construction. A movement that cannot be proven either way after seventy-two hours is parked for review. - Plan, in
automode only (or for the one currency you pressed Settle now on, inmanual): net the currency's open nettable obligations — the transfer rows, and the Funding leg of a conversion — and refuse when the net is under the threshold, the latest reconciliation is over an hour old or could not read the exchange, the currency's drift is unacknowledged or its residual is outside tolerance but has not yet survived enough runs to be drift ("drift not yet confirmed" — both Auto only; you may still settle by hand), the network is not usable, or the pause is on. - Claim the rows under the currency's anchor row lock and write the settlement with its in-flight key. That key is UNIQUE per currency and direction: the web process and the cron cannot both send.
- Dispatch. Spot short → send custody coins to the exchange's deposit address for the mapped network: EVM tokens through the same EIP-7702, permit and just-in-time-gas movers a customer withdrawal uses, from the treasury's own address, then any customer address holding surplus (the address a customer's Funding → Spot transfer left the coins at), then the master wallet; UTXO coins from the pooled inputs, the expected hash reserved before broadcast. Exchange over-holding → an exchange withdrawal to the treasury address on the mapped network, after moving funds into the account the provider withdraws from (KuCoin main, OKX funding). Native coin, SOL and SPL tokens, TRX and TRC20, TON (with the memo the exchange asks for) and Monero are sent from the treasury's own address first and, for native coin, SOL, TRX and Monero, from the master wallet as a fallback that never takes it below the reserve you set per chain. A customer's address is never spent on those chains, so a treasury that holds none of the asset parks the leg for review: fund the treasury (an exchange withdrawal to it does that) or move it by hand and record it.
What can never happen
- A settlement is never marked settled at broadcast — only on the receiving side's own evidence.
- A settlement's hash can never be credited to a customer: the spot deposit
claim refuses it, both verifiers check again before crediting, and the
Funding deposit path refuses a settlement's own change output. A customer
who withdraws from their own Funding wallet to the Spot deposit address is
not caught by this: their own withdrawal row vouches for the hash — and so
is the sweep the
ecosystem_custodyspot deposit mode makes from a customer's address, for the same reason. - A movement that had started when the process died is parked, never resent; an exchange withdrawal the provider answered indeterminately is never re-issued — the engine looks for it by address, amount and time.
- Nothing is sent and nothing is marked settled while the pause is on.
- No settlement changes a wallet balance, except the treasury's own mirror
when it pays from its own address and the recognised-loss bookings, made
on the pool that received less than was sent: the exchange's deposit fee
against the Super Admin's Spot wallet, an exchange withdrawal's fee against
its Funding wallet on that chain, gas against its Funding wallet, under the
adminProfittypePOOL_BACKING, inside the settlement's own transaction — which rolls back if a booking cannot be written, so nothing is marked settled with an unbooked fee. - A movement is never below the exchange's minimum deposit: a net that
would ship dust the exchange will not credit is refused, and so is an
instalment the per-movement cap (
poolBackingMaxSettlementUsd) would cut below the threshold.
Conversions
A Funding USDT → Spot BTC transfer leaves two rows: the Funding side holds
USDT it owes nobody, and the exchange owes BTC it never received. The
engine ships the USDT like any transfer leg. The BTC leg stays a Record
external door unless poolBackingAutoConvert is on; then, once the USDT
leg is settled, the engine buys the BTC on the venue — the BTC/USDT market
sized in BTC to the venue's own precision and minimums, funds moved into the
trading account first on KuCoin and OKX, one conversion per currency in
flight, capped by the same per-movement cap (poolBackingMaxSettlementUsd).
The fill is read back from
the venue; the venue's fee is booked as recognised loss; an order not filled
in thirty minutes is cancelled and parked for review with what filled. The
Convert now door runs it for one currency in manual.
The ecosystem side
For every currency and chain that has a treasury address or an open
Funding-side obligation, the reconciliation now measures the Funding pool
the way it measures the exchange: Le, what the Funding wallets say sits
at customers' addresses on that chain — the treasury's own figure and the
part no address accounts for are shown beside it and are not in it, so a
treasury reserve owed to nobody reads as a surplus, never as a hole —
against He, what the chain says. The
treasury, the master wallets and the custodial contracts are read every run;
customer addresses are read in a rotating slice (poolBackingCustodyReadsPerRun
per chain per run, least recently attempted first, so an address that fails
every run takes one turn and moves on) under the deposit scanner's own
per-chain rate limits, and every figure is cached with its read time and its
error. An address that leaves the platform's inventory (a replaced master, a
custodial contract set inactive, a deleted wallet) leaves the cache on the
next run of its chain. He is a number only when every address has a figure
(ok); it is partial while some never have, and unknown when the
treasury or a master could not be read this run — a failed read, or a read
the run did not get to make — an RPC failure is an error, never a zero, and
a cached figure never stands in for a read that did not happen. UTXO
customers are read from the platform's own unspent pool; Monero customers
only from the wallet mirror, and the card says so. A currency whose whole
ecosystem side could not be read shows as such on the card, with the reason,
rather than as a currency with no ecosystem side. The Funding-side residual
is shown on the Ecosystem side card — open and waived Funding-side
obligations explain it as they do on the exchange side — and is never
persisted as drift.
The nightly attribution
Pool Backing Attribution runs once a day. For every finished UTC day it
derives, from the transaction ledger, the Spot balance changes the platform
minted itself — investment and AI returns, staking and referral rewards,
binary outcomes, commerce, moves into and out of copy-trading and futures,
platform fees — as minted obligation rows per currency and family. They
explain the residual by category; the engine never nets or settles them and
the cap ignores them; they close only when you record a movement or waive
them. The first run attributes yesterday only: everything before the ledger
existed is the opening drift the first reconciliation showed you. Money
that moved into a parked store is no longer written as a minted row —
it is counted in the liabilities instead — and the rows written before this
change are cancelled by the next run.
The same job then reads the exchange's own trading fees from its trade
history, for every symbol the platform traded since the last run, and
writes one exchange_fee row per trade, booked as recognised loss against
the Super Admin's Spot wallet in the fee's asset: settled for the part the
wallet covered, open for the remainder. That was the last unexplained
figure of a healthy install.
After updating
- Check what the seeder granted:
view.pool.backingwent to every role holdingview.deposit,edit.pool.backingto every role holding bothedit.depositandedit.withdraw. Grant those by hand only to a role that holds neither witness. Grantmanage.pool.backingby hand to whoever may waive an obligation or mark a settlement failed — nobody has it until you do. Super Admin needs none. - Open the page and run a reconciliation. The first run's unexplained figure per currency is the gap your install has accumulated over its whole history of ledger-only transfers and treasury payouts; after three runs it is recorded as drift. Review it against your exchange statements and acknowledge it.
- Leave the mode on
monitoruntil the figures make sense to you. Set a cap and switch tomanualonly once you have a settlement routine — the cap refuses customers' transfers, and that is a promise you are making about how quickly you settle. - Open the treasury card and map every chain you want the engine to use to
the exchange's network id. Press Settle now on one currency in
manualand watch it through to settled. - Switch to
autowhen the manual runs behave. KeeppoolBackingMaxSettlementUsdat a figure you are comfortable seeing move without you, and remember the pause switch. - For the native chains (ETH and the other EVM coins, SOL, TRX, TON, XMR)
decide where the reserve comes from. The engine sends from the treasury's
own address first; an exchange withdrawal to the treasury funds it, and so
does any coin you send to the treasury address shown on the card. If you
want the master wallet as a fallback for native coin, SOL, TRX or XMR,
set
poolBackingMasterReserveto what it must keep per chain — the default floors are small, and gas for customers' withdrawals comes out of the same wallet. - Turn on
poolBackingAutoConvertonly if you want the engine to buy the exchange leg of conversions on the venue; otherwise those rows stay a Record external door, which is also fine.
What it does not do yet
- Spend a customer's address on a native chain, Solana, Tron, TON or Monero when the treasury holds none of the asset: the leg is parked for review. Fund the treasury, or move it by hand and record it.
- Read Monero customers' addresses on chain: the wallet mirror is shown, labelled as such.
- Persist the Funding-side residual as drift, or refuse anything on the Funding side. It is measured and shown.
- Run a conversion before its Funding leg is settled, or with the switch off.
- Adjust the gap for a deposit the exchange has credited but the platform has not yet: the gap is liabilities minus holdings, and such a deposit shows as a transient residual until the platform credits it — which is what the run count before drift is for.
- Reserve an EVM settlement's hash before broadcast: it is reserved from the deposit guard the instant the node returns it, which is the instant the transaction is public. UTXO and TRON hashes are reserved before broadcast.