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.

10 min readUpdated 6 August 2026admin, dashboard, revenue, reconciliation, cron

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

Everything on the dashboard, in one call

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, countCOMPLETED 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, REFUNDED and PARTIALLY_REFUNDED count as succeeded — a later refund is a separate deliberate event, not a failed checkout.
  • FAILED, CANCELLED and EXPIRED count as failed.
  • PENDING and PROCESSING are 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

  1. Confirm the mode. LIVE, in the header, before you read a single figure.

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

  3. Take refunds separately. payments.totalRefunded plus 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.

  4. Cross-check against the revenue screen by currency, not by total. Compare GATEWAY_PAYMENT per currency on /admin/finance/profit against the allocation currencies your merchants are actually paid in, remembering that refunded fees are still counted there.

  5. Add the outstanding merchant balances. The sum of pending per merchant is on each merchant's detail screen. That is what you owe and have not paid.

  6. Check the payout queue's oldest item. A large pending total 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.