Reading the revenue screen
Why platform revenue is the Super Admin's own wallet balances joined to adminProfit rows, what each tile on /admin/finance/profit measures, and the log lines that mean fees are being dropped or a payout went unfunded.
Finance → Revenue Analytics (/admin/finance/profit) is the platform's P&L.
It is not a report computed over transactions. It is two different things joined
on one screen:
- What you hold — the balances on the Super Admin's own wallets, read live, per wallet type and per currency.
- What you booked — rows in the
admin_profittable, one per fee the engine captured or payout it funded.
Everything on the page follows from that. Revenue is zero on a busy platform whenever the first half has nobody to point at, and the screen will not tell you so — it renders a clean, empty dashboard.
The page needs access.admin.profit; the data behind it needs
view.admin.profit.
Where revenue is actually held
There is no treasury account, no house ledger and no separate revenue table of balances. When the fee engine collects, it credits a wallet belonging to a real user account — the Super Admin.
That account is resolved in two steps, and both can fail:
- Find the role whose
nameis exactlySuper Admin. - Find the oldest user (
createdAtascending) holding that role.
If either step comes back empty, collectPlatformFee logs and returns. It
never throws — a fee-collection failure must not roll back a customer's
deposit — so the customer's money moves correctly and yours simply does not
arrive. Nothing on the admin panel goes red.
Two lines in the backend log under the PLATFORM_FEE module are the only
warning you get, and they are logged at error level for that reason:
[CRITICAL] No Super Admin role configured — platform fees are being dropped!
[CRITICAL] No Super Admin user configured — platform fees are being dropped!The second one is the trap. The role existing is not enough — a role with no user on it drops fees exactly as thoroughly as no role at all. Renaming the role, deleting the original superadmin account, or migrating that account onto a different role each produce this state.
Every dropped fee is also logged individually with its amount, currency, type and reference, so a window of lost revenue can be reconciled from the log after you fix the cause.
The lookup is cached for five minutes. A role change, a new Super Admin account or a repaired configuration is not reflected until that cache expires, so do not judge a fix by the next fee — wait, or restart the backend.
One deliberate exclusion: when the acting user is the Super Admin, fee collection is skipped entirely. Charging yourself a fee and crediting it back is a circular no-op that inflates the report. Your own test trades therefore book no revenue.
The wallet balances — totalsByWalletType
The Admin wallet balances section reads the Super Admin's wallet rows
directly, grouped by type and then by currency. Under the platform's HOLD model
each currency line carries three figures:
| Figure | Formula | What it is |
|---|---|---|
available |
balance (floored at zero) |
Free — you can withdraw or spend this |
total |
balance + inOrder |
Everything the wallet holds |
walletBalance |
balance |
The raw free figure, unfloored |
The card prints available / total and shows the second half only when they
differ, so a line with one number is a wallet with nothing on hold. inOrder
is money held against an open order or an escrow — it is yours, it is not
spendable yet.
Wallet types whose every currency is empty are dropped from the section rather than rendered as a row of zeroes, so a type disappearing from this list means it holds nothing, not that it broke.
The Super Admin's wallets appear on the wallet screen like anyone else's. Freezing one makes every fee credit into it fail. Deleting one is refused while it holds funds, and would be catastrophic if it were not.
The booked figures — the three breakdowns
All three come from admin_profit, all three are all-time with no date
filter, and none of them is a wallet balance.
| Key | Grouped by | Rendered as |
|---|---|---|
totalsByType |
type |
Top fee sources — one ranked bar per revenue line, with a row count in brackets |
totalsByCurrency |
currency |
Top currencies |
recentFees |
— | The 20 newest rows, returned by the API; the table below the summary is where you read them |
Both breakdown cards rank and scale on magnitude, not signed value. A large payout is a large flow and belongs at the top; sorting by the signed number would bury the only rows that need attention. A negative row draws in the loss colour and always carries its minus sign.
trackingSince is the createdAt of the earliest admin_profit row and is
printed as the footnote under the summary. On an install that has never booked a
fee there is no such row, and the footnote is absent.
Totals are per currency, and are never added together
This is the single most important thing to know about the page.
admin_profit.currency is a per-row column. A platform running USD, NGN and BTC
fees has rows in all three, and adding them produces a number that means nothing:
0.0004 + 5 + 12000 is not 12005.0004 of anything.
So periodComparison — the Today, This week, This month and Last
month tiles — is grouped by currency by construction. Each tile shows the
currency with the largest magnitude as its headline figure, with the rest listed
beneath it and a Show all (n) disclosure when there are more than three.
The % vs last month chip under the This month tile compares that tile's lead currency only, against the same currency last month, and names the currency beside the percentage. It is not a platform-wide growth figure. If your lead currency changes between months, the comparison changes what it is comparing — read the currency label, not just the arrow.
Period boundaries are computed in the server's local time, and the week
starts on Sunday. A currency whose total for a window is exactly zero is
omitted from that window rather than shown as 0.00.
The tab switcher in the page header swaps the summary for a chart view. The summary panel is all-time and per-currency; the analytics cards are scoped to the selected date window and price each row into USD at its own rate. Both are correct; they answer "what have we earned" and "what earned this period". Where the analytics tab shows a single ranked bar across currencies, its title says so.
The revenue-line vocabulary
Every row carries a type from a fixed list. This is what you will see on the
badges, in the filter dropdown and in the Top fee sources card.
| Type | Where it comes from |
|---|---|
DEPOSIT |
Fee on a deposit, booked at approval |
WITHDRAW |
Fee on a withdrawal, booked at settlement |
TRANSFER |
Wallet-transfer fee — see Internal transfers |
BINARY_ORDER |
The house's side of a binary option |
EXCHANGE_ORDER |
Spot order fee |
TRADE |
Generic trade fee |
INVESTMENT · AI_INVESTMENT |
The house's side of an investment settlement |
FOREX_DEPOSIT · FOREX_WITHDRAW · FOREX_INVESTMENT |
Forex addon |
ICO_CONTRIBUTION |
Token-sale contribution fee |
STAKING |
Staking fee |
P2P_TRADE |
P2P trade fee |
GATEWAY_PAYMENT |
Merchant gateway checkout |
NFT_SALE · NFT_AUCTION · NFT_OFFER |
NFT marketplace |
REFERRAL_REWARD |
A platform payout — always negative |
DEX_SWAP |
Swap fee, credited only by the on-chain fee sweep |
DEX_LP_FEE |
Realised liquidity-provider fees from a V3 collect() |
DEX_LISTING |
A listing or promotion fee invoiced to a project |
Two of these behave differently from the rest.
REFERRAL_REWARD is negative. It is written by the platform-loss path, not
the fee path, so the report nets out instead of presenting gross fees captured as
if they were profit. The same mechanism records the house's losses on binary
payouts and winning investments. A negative row is money you paid; the view
dialog labels it Platform payout rather than Fee collected.
DEX_LP_FEE is yield on your own capital, not a fee charged to a customer.
It sits beside an impermanent loss that is never in this table. Do not read it
as revenue from trading activity.
When the platform pays out, and the treasury is short
The loss path is not the fee path run backwards. It debits the Super Admin's wallet best effort, capped at what that wallet actually holds — the debit is the smaller of the payout and the wallet's free balance in that currency — and it never blocks the payout. By the time it runs the customer has already been credited, and a house with no reserves must not be the reason their winnings fail.
The negative admin_profit row is written anyway, and it carries the full
amount rather than the part that was funded. That is the point: an unfunded
payout appears here as a platform loss instead of being recorded nowhere, which
would leave the matching fee row on the report looking like pure profit. When
nothing was debited the row has no transactionId — there is no transaction to
open from the view dialog, and the row itself is the only evidence the payout
happened.
Three lines under the same PLATFORM_FEE module report it. They are logged at
warn, not [CRITICAL], because nothing was lost and nothing was
mis-recorded — the customer was paid and the books net out. It is the treasury
that is behind.
Treasury short on <TYPE> payout: owed <amount> <currency>, debited <debited>. The full amount is still recorded against admin profit. referenceId=<ref>
Recorded <TYPE> loss of <amount> <currency> with NO treasury debit (treasury empty). referenceId=<ref>
Treasury debit REFUSED on <TYPE> payout of <amount> <currency>: <reason>. Booking the loss unfunded. referenceId=<ref>The first two are the same condition at two depths. Treasury short means the wallet held less than the payout and was drained to whatever it had; the second line appears when it held nothing at all and no debit was attempted. Neither needs anything doing to the books — fund the Super Admin's wallet in that currency and wallet type, and the next payout debits normally. The gap is visible on the page itself: the loss rows are there while the wallet balance above them is not falling.
Treasury debit REFUSED is different, and it is the one to read the reason on. The balance was there when the amount was decided and the debit was still turned away; the line quotes why. Most often the wallet was drained between the two reads, or it is frozen — and a frozen Super Admin wallet refuses every fee credit into it as well, so this line is often the first symptom of a much wider problem. Check it on the wallet screen.
A fourth line is logged at error level, and it is the exception to everything above:
[CRITICAL] Unrecorded platform loss — no Super Admin configured. type=<TYPE> amount=<amount> currency=<currency> walletType=<type> referenceId=<ref>This is the same missing-Super-Admin fault as above, on the payout side. There is no wallet to debit and no row is written — so the fee legs stay on the report with nothing offsetting them and the platform reads as more profitable than it is, in the exact way the loss path exists to prevent. The line carries the amount, currency, type and reference of every payout it lost, so the window can be reconciled by hand once you have fixed the role or the account.
Accrued on chain, and why it is fenced off
If the Swap addon is installed, a warning-toned card appears above the wallet balances headed Accrued on-chain (not yet swept).
That money is not yours yet. A swap fee lands at a fee-recipient address the platform holds no key for; it becomes platform money only when you move it and the sweep observes the transfer and credits a wallet. Until then it is a claim, not a balance, which is why it is deliberately kept out of every figure above and rendered separately.
The card sums dexFeeAccrual rows whose sweepStatus is ACCRUED or
SWEEP_SUBMITTED, grouped by token and chain. A token the platform could not
price shows an em dash in the USD column rather than $0.00 — an unpriceable
token is not a worthless one.
Installs without the Swap addon still receive the key. The summary route always
returns pendingOnchainRevenue — an empty array when the addon's model is not
present — because an absent key renders as a permanently loading card on the
client. The card itself draws only when the array has rows, so those installs
never see it.
Editing the books by hand
Three permissions exist beyond reading, and the screen exposes only one of them.
The table on /admin/finance/profit is configured create-off, edit-off,
delete-on. So deletion is a click; creating or editing a row means calling the
API. All four are written to the admin audit trail under the module ADMIN_FIN.
adminProfit is not a soft-delete model — there is no deletedAt column and
the table is configured non-paranoid. Delete is permanent, and there is no
restore. The table's own delete action is the destructive one.
The manual-entry form's type dropdown offers fourteen of the model's
twenty-two types: DEPOSIT, WITHDRAW, TRANSFER, BINARY_ORDER,
EXCHANGE_ORDER, INVESTMENT, AI_INVESTMENT, FOREX_DEPOSIT,
FOREX_WITHDRAW, FOREX_INVESTMENT, ICO_CONTRIBUTION, STAKING,
P2P_TRADE and DEX_LISTING.
The omissions are the point. Of the three DEX types, only DEX_LISTING is
offered:
- A listing fee is legitimately hand-entered — you invoiced a project and booked the charge. Nothing observed it on chain and nothing needed to.
DEX_SWAPandDEX_LP_FEEare excluded and must not be added. Each is an observation of an on-chain fact — a swept fee transfer, or the fee half of a V3collect()receipt. A hand-entered one is a fabricated realisation with no accrual and no pool event behind it, which makes the books unreconcilable against the chain.
Nothing is required by the create route itself: POST validates against a store
schema that declares no required list at all. The model is the only gate —
type, amount and currency are NOT NULL, and transactionId is
explicitly nullable, widened so the platform-loss path can book a loss with
no transaction to name. (The required list naming all four fields belongs to
the update schema, and binds only PUT.)
Fill transactionId anyway on any row you enter by hand. It is the link back to
the PLATFORM_FEE transaction the fee was taken from — the view dialog turns it
into a link to /admin/finance/transaction/<id>, and it is the only field an
operator ever navigates from.
When the screen says zero
Work down this list in order.
-
Is there a Super Admin user? Check the backend log for the two
[CRITICAL]lines above. If either appears, no fee since that moment was collected. Fix the role or the account, then wait five minutes for the cached lookup to expire. -
Are the fees configured at all? A fee of zero collects nothing and logs nothing —
collectPlatformFeereturns early on a non-positive amount. Check the fee settings for the flow you expected revenue from. -
Was the operation performed by the Super Admin? Fees are skipped for that account by design.
-
Is the flow one that books at settlement? Deposits book their fee at approval, withdrawals at settlement. A queue full of
PENDINGrows has earned you nothing yet, and rejecting them never will. -
Are you reading the right half of the screen? The wallet cards are live balances; the breakdowns are booked history. A wallet you withdrew from shows a smaller balance while the booked total is unchanged — that is correct, not a discrepancy.
If revenue is present but the wallet is empty, you have simply spent it: the
admin_profit row records that a fee was captured, and the wallet records what
is left of it. They are not supposed to agree forever.