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.
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/settings → Fees & 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:
- 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>. - Writes the ledger row. For SPOT and FIAT the
transactionrow carriesreferenceId = p2p_fee_<tradeId>_fee; the operation typePLATFORM_FEEmakes it aPLATFORM_FEEtransaction. For ECO the credit goes throughecoCreditwith operation typeECO_FEE, which maps to the samePLATFORM_FEEtransaction type — but noreferenceIdis threaded through, so an ECO fee row has none. - Writes an
admin_profitrow withtype = '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.tradeIdThis 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 recordedWriting 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 syncedThe 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 carrydeletedAt IS NULL.admin_profitis not paranoid and has nodeletedAtfilter to apply.
Related
- P2P settings — the fee field and its neighbours.
- Reconciling P2P escrow — the other half of the money question: what the platform is holding, rather than what it earned.
- The trade lifecycle and escrow — where the fee sits in the settlement.