The gateway dashboard and reconciling it against platform revenue
Every figure on /admin/gateway, which of them the LIVE/TEST switch actually filters, why net volume goes negative after a refund, and how the addon's fee total relates to GATEWAY_PAYMENT rows on the revenue screen.
/admin/gateway is the addon's landing screen and the first thing anybody sees
after Extensions → Payment Gateway. It is a real statistics surface — every
tile is a server-side aggregate over the whole table, not a page of rows — and it
is the screen an operator will reach for when reconciling a month.
It will not reconcile against Finance → Revenue Analytics
(/admin/finance/profit) on the first attempt, and the reasons are structural
rather than a bug in either screen. The second half of this page is about that.
access.gateway.merchant is the only key involved — the same one that opens the
merchant screen. There is no page-level permission on /admin/gateway, so a
staff member without that key sees the frame and an error rather than a 403.
The page re-fetches every 60 seconds, but only while the tab is actually
visible, so a dashboard left open in a background tab is as old as the last time
you looked at it. The provenance line at the top of the masthead states which
mode the figures were drawn from and the clock time they were computed —
generatedAt from the payload, not the browser's idea of now. A failed refresh
turns the live dot amber and says figures are stale while keeping the last good
numbers on screen; a failure with nothing loaded takes the whole page, because a
console of zeros is a claim the screen cannot support.
What the LIVE/TEST switch actually filters
The switch is in the gateway admin header. It is a browser preference stored in
localStorage under admin-gateway-mode and defaults to LIVE; it is not
attached to your account. A payment's mode comes from the prefix of the API key
the merchant used — see The payments desk.
It does not filter everything on this page, and the difference matters when you are reading it:
| Figure | Filtered by mode? |
|---|---|
| Payment counts (total, completed, pending, failed, refunded, partially refunded) | Yes |
| Volume, fees, the per-currency breakdown | Yes |
| Total refunded, net volume | Yes — via the payment each refund belongs to |
| Recent payment activity | Yes |
| Success-rate declines | Yes |
| Merchant counts (total, active, pending) | No — merchants are not per-mode |
| The payout queue, its aging and its currencies | No |
Every payment and volume figure on this page reads zero, or reads a test
merchant's play money, while the merchant and payout figures keep reporting the
real platform. That mixture is more confusing than an all-zero page, and the
only thing on screen saying which is which is the small TEST word in the
provenance line above the title.
The payout queue in particular is never filtered: gateway_payout has no
testMode column at all. If the meter shows nine overdue payouts while you are
in TEST mode, those nine are real money that real merchants are waiting for.
The masthead: the payout queue, aged
The masthead is three tiers, in this order: the provenance line, then the title,
then the payout queue meter. That meter — the band below the title — is the
one thing on the page that is meant to get somebody out of bed. It is the
PENDING payout queue split into three age bands
against a 168-hour (7-day) SLA, taken from the platform's shared SLA table —
the same budget the withdrawal queue uses, because a merchant payout is money
leaving your custody.
| Band | Age | Colour |
|---|---|---|
| On time | under 84 hours | success |
| Due soon | 84 hours or more | warning |
| Overdue | 168 hours or more | destructive |
The bar is weighted by payout count, not by amount — each payout is one decision somebody has to make — and the amounts are printed in the legend beside the counts. The last item in the legend is the age of the oldest waiting payout, which is the fact a count cannot carry: twelve payouts queued this morning and twelve queued nine days ago are the same number and completely different mornings.
Work the queue on Payouts and refunds.
The four tiles
Each one links through to the rows that produced it.
| Tile | Figure | Caption | Goes to |
|---|---|---|---|
| Pending payout value | payouts.pendingAmount |
count awaiting approval | Payouts |
| Total merchants | merchants.total |
active / pending split | Merchants |
| Total payments | payments.total |
completed / pending split | Payments |
| Net volume | payments.netVolume |
total fees | Payments |
The Pending payout tile is the only one allowed to turn amber, and only when the overdue band has something in it.
gateway_payment.currency is free-form per merchant, so a flat sum across the
table can be adding EUR to USDT. The payload therefore carries volume and fees
broken out by currency, and the page names a unit only when the breakdown has
exactly one currency in it. With several, the tile prints the bare figure and
the caption reads summed across N currencies instead of naming your fee take.
That caption is the honest answer, and it is also a sign that a single headline number is the wrong tool for your platform. Read the per-currency breakdown in the payload, or filter the payment list by currency.
Every figure the endpoint returns
| Field | Meaning |
|---|---|
merchants.total · .active · .pending |
Row counts on gateway_merchant. Never mode-filtered |
payments.total |
Every payment row in this mode, any status |
payments.completed · .pending · .failed |
Counts for COMPLETED, PENDING, FAILED |
payments.refunded · .partiallyRefunded |
Counts for those two statuses |
payments.currencies[] |
Per currency: volume, fees, count — COMPLETED payments only |
payments.totalVolume |
The bare sum of currencies[].volume |
payments.totalFees |
The bare sum of currencies[].fees |
payments.totalRefunded |
Sum of every COMPLETED refund whose payment is in this mode |
payments.netVolume |
totalVolume − totalRefunded |
payouts.pending · .pendingAmount |
Count and value of PENDING payouts, all modes |
payouts.slaHours · .oldestPendingAt · .aging · .currencies[] |
The masthead meter |
merchantHealth |
The success-rate comparison below |
recentPayments[] |
The ten newest payments in this mode |
generatedAt |
When the figures were computed, ISO |
totalVolume is summed over payments whose status is exactly COMPLETED. The
moment a payment is refunded its status becomes REFUNDED or
PARTIALLY_REFUNDED, so the whole payment drops out of volume and fees — and
then totalRefunded is subtracted from what is left.
A single 100 USD payment, fully refunded, therefore reports totalVolume 0,
totalRefunded 100 and netVolume −100. A partial refund is worse in a
quieter way: the payment's entire 100 leaves volume while only the refunded 30 is
subtracted, so the 70 the merchant kept vanishes from the figure altogether.
Do not use the Net volume tile as a revenue figure on a platform that issues
refunds. payments.currencies[].volume — completed, un-refunded volume per
currency — is the figure that means something, and refunds are their own line.
The same applies to totalFees: the fee on a refunded payment leaves the total
completely, while the fee you actually kept on a partial refund is somewhere
between zero and all of it.
Success-rate declines
The panel below the tiles is the one thing here that finds a problem rather than counting one. A gateway does not fail loudly — it fails as one merchant's checkout quietly dropping from 96% to 60% while every headline on the page keeps rising, because the other forty merchants are fine.
It compares the last 7 days against the 7 before, per merchant, in the current mode:
COMPLETED,REFUNDEDandPARTIALLY_REFUNDEDcount as succeeded — a later refund is a separate deliberate event, not a failed checkout.FAILED,CANCELLEDandEXPIREDcount as failed.PENDINGandPROCESSINGare excluded entirely; an in-flight payment has not failed.- A merchant needs 10 decided payments in both windows to be compared at all, and the drop must be at least 5 percentage points to be listed. At most five merchants are shown, worst first.
The empty state names its own threshold, and the distinction is worth reading: "N merchants compared" means the check ran and found nothing, while "no merchant had enough traffic" means it could not run. On a small platform the second is the normal answer and this panel will never say anything.
Reconciling against Finance → Revenue Analytics
This is where operators lose an afternoon. The addon's totalFees and the
GATEWAY_PAYMENT rows on /admin/finance/profit are measuring the same fees,
and they will not be the same number. Five reasons, all of them structural.
1. They are denominated differently. The dashboard's fees are
SUM(gateway_payment.feeAmount) grouped by the payment's invoice currency —
what the merchant priced the order in. The adminProfit rows are written one
per wallet allocation, in the currency of the wallet the buyer actually paid
from, because that is where the fee was taken. An order invoiced in USD and
paid from a USDT balance produces a USD figure on this dashboard and a USDT row
on the revenue screen. They agree only when the two currencies happen to match.
2. A refunded payment leaves this dashboard and stays on the revenue screen.
Refunding drops the payment out of totalFees entirely (see the callout above).
On the revenue side the original GATEWAY_PAYMENT row is never reversed — the
fee share is returned by debiting the Super Admin's wallet as an
OUTGOING_TRANSFER, with no negative adminProfit row written. So
adminProfit is a gross, cumulative record and the wallet balance is the net.
3. This dashboard has no date control at all, and the revenue screen's
summary is all-time too. Every figure on /admin/gateway is the whole table
for the current mode; there is no from/to picker anywhere on the page. On the
revenue screen the summary above the table — totals by type, totals by currency,
the admin wallet balances — is all-time as well, and its period tiles (today,
this week, this month, last month) are fixed windows you cannot change. What
can be windowed is the adminProfit rows in the table below: Created at
is a filterable date column, and the platform's date filter is a from/to range,
so you can pull GATEWAY_PAYMENT rows for exactly the month you are closing.
There is no equivalent on the gateway side, so the comparison is always a
windowed row set against an all-time total.
4. Test payments write nothing to adminProfit. A test-mode checkout
collects no fee and credits no balance, so there is no test pollution on the
revenue side — but it does produce a feeAmount on the payment row, which is
why a TEST dashboard shows fees that correspond to no money.
5. The revenue screen's "Top fee sources" card sums across currencies.
totalsByType["GATEWAY_PAYMENT"] is a single SUM(amount) grouped by type
only, so it adds every currency together. Its Top currencies card is the one
grouped by currency, and it spans every revenue type, not just the gateway.
The Super Admin wallets behind the number
Fees are not held in a treasury account. Every gateway fee is credited to a
wallet belonging to the oldest user holding the role named Super Admin, in
the same currency and wallet type the buyer paid from, as a PLATFORM_FEE
operation with reference <paymentId>_fee. The Admin wallet balances section
on the revenue screen is those wallets, read live.
Which means the balance you see there is GATEWAY_PAYMENT fees plus every
other fee the platform collects, minus every refunded fee share and
minus anything you have withdrawn or spent from that account. It is a
balance, not a revenue figure. The full behaviour — including what happens when
no Super Admin exists and fees are silently dropped — is in
Reading the revenue screen.
Merchant balances are not custodial liability
gateway_merchant_balance is a ledger table with pending, available and
reserved columns, and it is read by the gateway's own screens and jobs and
by nothing else in the platform. It is not a wallet row. It does not appear
under Finance → Wallets, it is not in any wallet total, and it is not in the
revenue screen.
That is the correct model, and it has one consequence you have to hold on to: a
merchant's pending balance is money you owe and cannot see on any core finance
screen. It becomes a real, visible, spendable wallet row only when a payout is
approved — that is the transaction that moves the amount from pending to
available and credits the merchant's wallet as an INCOMING_TRANSFER.
So the platform's custodial liability at any moment is its wallet balances
plus the sum of pending across gateway_merchant_balance, and only the
first half is on a screen.
A workable monthly reconciliation
-
Confirm the mode. LIVE, in the header, before you read a single figure.
-
Take the per-currency breakdown, not the headline.
payments.currencies[]gives completed volume, fees and count per currency. The Net volume tile is not usable where refunds exist. -
Take refunds separately.
payments.totalRefundedplus the refunded and partially-refunded counts. Do not net them against volume yourself either — the payments they belong to have already left the volume figure. -
Cross-check against the revenue screen by currency, not by total. Compare
GATEWAY_PAYMENTper currency on/admin/finance/profitagainst the allocation currencies your merchants are actually paid in, remembering that refunded fees are still counted there. -
Add the outstanding merchant balances. The sum of
pendingper merchant is on each merchant's detail screen. That is what you owe and have not paid. -
Check the payout queue's oldest item. A large
pendingtotal with a nine-day-old oldest payout is not a reconciliation problem; it is an unworked queue.
The three jobs behind these numbers
Every figure on this page is moved by the cron worker. With it stopped, the dashboard keeps rendering and quietly stops being true.
| Job | Interval | What it moves on this page |
|---|---|---|
processGatewayPayouts |
60 min | Creates the PENDING payouts the masthead meter counts. Skips its entire run while gatewayEnabled is off, so completed payments keep crediting pending and no payout is ever proposed |
processGatewayWebhookRetries |
60 s | Redelivers failed merchant webhooks. Nothing on this dashboard, but its absence is why merchants report missing notifications for payments that look fine here |
processGatewayPaymentExpiry |
5 min | Moves lapsed checkouts out of PENDING. Without it, payments.pending grows forever and the pending half of the Total payments caption becomes meaningless |
All three run under the gateway category. Verify them on Admin → System →
Cron, or see Scheduled jobs.
gatewayEnabled gates merchant registration and payment creation and makes the
payout job skip. It does not stop a checkout that is already in flight from
completing and crediting a merchant balance. Expect a backlog of unproposed
payouts when you turn it back on — and note that the hourly job will then
propose them all at once.
Next: The payments desk for the rows behind these totals, or Gateway settings for the switches that change them.