Where P2P revenue lands and how to reconcile it

What the escrow fee actually books — the Super Admin wallet, an admin_profit row and a p2p_commissions row — why the commission table has no currency, and the SQL to reconcile a period.

7 min readUpdated 12 August 2026fees, revenue, commissions, reconciliation, accounting

P2P charges one fee, and it produces three records: a wallet credit, an admin_profit row and a p2p_commissions row. Any of the three can be present without the others, and the reasons are worth knowing before you try to reconcile a month.

The fee, end to end

The rate is p2pEscrowFeeRate/admin/p2p/settingsFees & Limits → "Escrow (Platform) Fee". A slider, 0 to 5 %, step 0.01, default 0.2 %. It is the only fee P2P charges: no maker fee, no taker fee, no dispute fee.

calculateEscrowFee runs when the trade is created and the result is written to p2p_trades.escrowFee. Settlement reads that stored number.

So moving the slider never affects a trade that is already open. A trade started this morning settles tonight at this morning's rate. If you are comparing collected revenue against the slider for a period, remember that the period's trades were priced when they opened.

calculateEscrowFee looks for a settings row keyed p2p first and reads EscrowFeeRate (or escrowFeeRate) out of the JSON. Only if that is absent — strictly, only if the rate is still at its 0.2 built-in default after that lookup — does it fall back to the p2pEscrowFeeRate key the screen writes.

On a fresh install there is no such blob and the slider is authoritative. On an install carried forward from an older release there may be, and then moving the slider changes nothing. If collected revenue does not match the slider, check for that row before anything else:

SELECT `key`, `value` FROM settings WHERE `key` IN ('p2p', 'p2pEscrowFeeRate');

The floor and the cap

A floor of 0.0001 of the traded currency applies, but only while it stays under 5 % of the trade amount.

The cap is not decoration. The floor is an absolute quantity of whatever is being traded, so its worth depends entirely on the asset: 0.0001 is dust in DOGE and roughly ten dollars in BTC — enough to equal or exceed a legal minimum-size BTC trade. That produced a fee greater than or equal to the escrow, a zero buyer credit, a wallet-service rejection, and an escrow that could never be released. Making the floor conditional means it can never approach the value of the trade.

A capped fee logs under P2P_FEES:

Escrow fee capped for <amount> <currency>: <computed> -> <final> (max 5% of amount)

What settlement does with it

At settlement, the escrow authority applies four rules before it books anything:

Rule Effect
Fee is charged only on value delivered to the buyer a refund to the seller is never taxed, and a dispute won by the seller costs nothing
Fee is capped at the buyer's gross share it can never exceed the proceeds it is taken out of
A SPLIT uses the same stored fee, capped the same way it is not scaled down in proportion to the buyer's share, so a small buyer share can be charged the whole fee up to its own value
A buyer holding Super Admin is exempt entirely no fee, and therefore no commission row

The fee is paid by the buyer, so the exemption follows the buyer

The fee comes out of the buyer's proceeds and out of nothing else. The seller escrows the full amount and gives up the full amount whatever the fee is; what the fee changes is how much of it reaches the buyer. So the buyer is the party paying it, and the buyer is the party the exemption is about.

The exemption is checked twice — once in the escrow authority, once inside the fee collector — because charging the Super Admin and crediting them straight back is a circular no-op that inflates the profit report. Both checks read the same account for that reason: a settlement that took a fee the collector then declined to bank would leave the difference outside every wallet.

The exemption used to be keyed off the seller, so every trade the platform itself sold quoted an escrow fee on the trade screen and then credited the buyer the gross. Those trades book normally now. A quiet commissions table is no longer explained by house-owned offers — check the dropped-fee log lines under It never throws instead.

Trades bought from the Super Admin account still produce no revenue records, because the fee would be paid to that same account. Nothing quotes one either: the offer page and the market board resolve the terms for the signed-in reader, so an admin sees no fee line at all rather than a deduction that never happens. Signed-out visitors always see the ordinary terms — nobody anonymous is the house.

What collectPlatformFee actually books

The escrow authority hands the fee to the shared platform fee collector with:

Parameter Value
type P2P_TRADE
referenceId p2p_fee_<tradeId>
description P2P escrow fee for trade #<first 8 chars of the trade id>
walletType the offer's wallet type — FIAT, SPOT or ECO
chain for ECO only, resolved from the seller's walletData row

It then does three things, all inside the settlement transaction:

  1. Credits the Super Admin's wallet for that wallet type and currency, creating the wallet if it does not exist. Idempotency key platform_fee_P2P_TRADE_p2p_fee_<tradeId>.
  2. Writes the ledger row. For SPOT and FIAT the transaction row carries referenceId = p2p_fee_<tradeId>_fee; the operation type PLATFORM_FEE makes it a PLATFORM_FEE transaction. For ECO the credit goes through ecoCredit with operation type ECO_FEE, which maps to the same PLATFORM_FEE transaction type — but no referenceId is threaded through, so an ECO fee row has none.
  3. Writes an admin_profit row with type = 'P2P_TRADE', the amount, the currency, the chain and a link to the transaction.

The column carries a unique index. p2p_fee_<tradeId>_fee is therefore one row per trade, forever — which is the property that makes it safe to use as a join key when you are tracing a fee back to its trade, and the reason a second collection attempt on the same trade cannot quietly duplicate the row.

It never throws

collectPlatformFee catches everything and returns null. That is deliberate: a bookkeeping failure must not roll back a settled escrow. The consequence is that a dropped fee is only visible in the log.

Log line (module PLATFORM_FEE) Means
[CRITICAL] No Super Admin role configured — platform fees are being dropped! no role named Super Admin exists
[CRITICAL] No Super Admin user configured — platform fees are being dropped! the role exists but no user holds it
[CRITICAL] Dropped platform fee — no Super Admin configured. type=P2P_TRADE amount=… referenceId=p2p_fee_… the individual fee that was lost, with everything needed to reconstruct it
Failed to collect fee: P2P_TRADE … ref=p2p_fee_… anything else — a wallet failure, a constraint

The Super Admin lookup is cached for five minutes, so a freshly created Super Admin does not take effect immediately after a period with none.

The commission row

p2p_commissions is P2P's own revenue table. Its columns:

Column Holds
id uuid
adminId the oldest Super Admin account — see below
amount the fee, as a DOUBLE
description P2P escrow fee for trade #<8 chars> - <escrow> <currency>
tradeId the trade
offerId nullable, and nothing ever writes it

Two properties matter for accounting.

It is set to the oldest user holding the Super Admin role — the same account collectPlatformFee credits. There is no configured revenue recipient anywhere in P2P.

So the column identifies which system account the platform books revenue against, not who was paid, and not the admin who resolved the trade. If your oldest Super Admin is a decommissioned installer account, every commission row still names it. Do not read a payee out of this column.

A fee of 0.5 in that table could be half a dollar or half a Bitcoin. The currency comes from the joined trade, and only from there:

LEFT JOIN p2p_trades t ON t.id = c.tradeId

This is why every revenue figure on the P2P admin dashboard is reported per currency and never summed. The "Platform Revenue" card at /admin/p2p shows one figure per currency, and its growth percentage is only populated when exactly one currency exists — a cross-currency total would be a number with no meaning.

A commission whose trade row has been hard-deleted joins to nothing and lands in the UNKNOWN currency bucket.

The row is written only when the fee was genuinely collected

The commission row is written after collectPlatformFee returns, and only when it returned successfully. If collection failed or was skipped, no commission is recorded, and the escrow authority logs:

P2P_ESCROW  Platform fee of <amount> <currency> for trade <id> was not collected;
            no commission recorded

Writing it unconditionally used to record revenue the platform never received. Grep P2P_ESCROW for was not collected when your commission count is lower than your completed-trade count.

There is one more way a commission goes missing: the row is only created if a Super Admin user is found at that moment. Collection can succeed and the commission row still be skipped if that lookup returns nothing inside the transaction — rare, but it is why the two counts can differ by one or two.

The ECO wallet path

For an ECO offer the fee path additionally resolves the seller's walletData.chain and passes it to the collector, so the credit updates all three ECO layers and the admin_profit row carries the chain.

The related failure is on the settlement itself rather than the fee: if the seller's walletData row for that currency is missing, the main balances are still credited and the chain transfer is skipped, with an error logged rather than thrown:

P2P_ESCROW  ECO walletData missing for seller <id> (<currency>);
            main balances settled for trade <id> but chain ledger not synced

The buyer's balance looks right in the UI and their withdrawal will not work. This does not resolve itself — see the ECO divergence failure mode.

Reconciling a period

Three queries, in the order you should run them. Substitute your own dates.

1. What P2P thinks it earned, from its own table, per currency:

SELECT COALESCE(t.currency, 'UNKNOWN') AS currency,
       COUNT(*)                        AS commissionRows,
       SUM(c.amount)                   AS feeRecorded
  FROM p2p_commissions c
  LEFT JOIN p2p_trades t ON t.id = c.tradeId
 WHERE c.deletedAt IS NULL
   AND c.createdAt >= '2026-07-01 00:00:00'
   AND c.createdAt <  '2026-08-01 00:00:00'
 GROUP BY currency
 ORDER BY feeRecorded DESC;

This is exactly what the admin dashboard's Platform Revenue card aggregates, so a mismatch between this and the card is a caching or date-window question, not an accounting one.

2. What the platform actually booked, from the shared profit ledger:

SELECT currency,
       COUNT(*)    AS profitRows,
       SUM(amount) AS feeCollected
  FROM admin_profit
 WHERE type = 'P2P_TRADE'
   AND createdAt >= '2026-07-01 00:00:00'
   AND createdAt <  '2026-08-01 00:00:00'
 GROUP BY currency;

These two should agree. When they do not, the difference is one of the two skip paths above, and it is always in the same direction: admin_profit can have rows p2p_commissions does not, never the reverse.

3. The individual trades, when you need to find which ones diverged:

SELECT t.id,
       t.currency,
       t.escrowFee   AS feeQuotedAtInitiation,
       c.amount      AS feeRecorded,
       t.status,
       t.escrowStatus,
       t.completedAt
  FROM p2p_trades t
  LEFT JOIN p2p_commissions c
         ON c.tradeId = t.id AND c.deletedAt IS NULL
 WHERE t.deletedAt IS NULL
   AND t.status = 'COMPLETED'
   AND t.completedAt >= '2026-07-01 00:00:00'
   AND t.completedAt <  '2026-08-01 00:00:00'
 ORDER BY t.completedAt;

A completed trade with a non-zero escrowFee and no commission row is either a Super Admin sale, a fee capped to zero by a zero buyer credit, or a genuine collection failure. Check the seller's role first, then the log.

To trace a single fee to its wallet movement:

SELECT createdAt, type, amount, currency, referenceId, description
  FROM transaction
 WHERE referenceId = 'p2p_fee_<tradeId>_fee';

Remember that ECO fees have no referenceId — for those, search by idempotencyKey = 'platform_fee_P2P_TRADE_p2p_fee_<tradeId>' instead.

What is not here

  • No fee report screen. The dashboard card is the only surface, and it is all-time plus a 30-day growth figure. A period report is SQL.
  • No per-currency or per-offer fee override. One rate, platform-wide.
  • No revenue withdrawal path. The fee lands in the Super Admin's ordinary wallet for that currency and wallet type, and leaves it the same way any other balance does.
  • Nothing prunes p2p_commissions. The table is paranoid, so a delete soft deletes; every query above must carry deletedAt IS NULL. admin_profit is not paranoid and has no deletedAt filter to apply.