Moderating offers and trades
The P2P admin screens — the offer approval queue, flagging, pausing and disabling, the trade list, cancelling and force-resolving a trade, and the activity log that records all of it.
Six screens live under /admin/p2p. This page covers four of them — Overview,
Offers, Trades and Activity Log. Disputes has its own page, and
so do Settings.
Every screen is gated on its own permission key, so a role that can reach the admin panel still sees nothing here until you grant them.
| Screen | Path | Read | Write |
|---|---|---|---|
| Overview | /admin/p2p |
access.p2p |
— |
| Trades | /admin/p2p/trade |
view.p2p.trade |
edit.p2p.trade |
| Offers | /admin/p2p/offer |
view.p2p.offer |
edit.p2p.offer |
| Disputes | /admin/p2p/dispute |
view.p2p.dispute |
edit.p2p.dispute |
| Activity Log | /admin/p2p/activity |
view.p2p.activity |
— |
| Payment Methods | /admin/p2p/payment-method |
view.p2p.payment_method |
create / edit / delete.p2p.payment_method |
| Payment Rails | /admin/p2p/payment-rail |
view.p2p.payment_method |
create / edit / delete.p2p.payment_method |
| Method setup | /admin/p2p/payment-method/setup |
view.p2p.payment_method |
create.p2p.payment_method |
The last two are new in 6.2.5 and carry the shared half of a payment method — see Payment methods. No role holds their permissions at install, so the setup console refuses everyone except a Super Admin until you grant them in Admin → Roles.
It is the permission behind force-resolving a trade, which credits a buyer or refunds a seller directly. Treat it like a withdrawal-approval permission, not like a moderation one.
Overview
Totals, growth, distributions and a recent-activity panel drawn from the P2P activity log. It is a health read, not a work queue — the two queues that need daily attention are the offer approval list and the dispute list.
Offers
/admin/p2p/offer lists every offer with its owner, type, currency, price
model, amounts, status and flag state.
The approval queue
When p2pAutoApproveOffers is off — the default, and the right default — every
new offer and every edit to a live offer lands in PENDING_APPROVAL and waits
here.
PENDING_APPROVAL is a collateralized status. A SELL offer took its full
advertised total out of the maker's spendable balance the moment it was
submitted, and it stays there while the offer sits in your queue.
This is deliberate — approval must never have to find the funds a second time, because an approval that could fail on insufficient balance is a queue that randomly rejects. But it means a slow queue is a queue that freezes customers' money. Work it daily, or turn auto-approve on and moderate reactively instead.
Approve validates that the offer has an amount and at least one payment
method, sets it ACTIVE, and emails the owner.
Reject sets it REJECTED and releases any locked funds back to the
owner, with your reason attached to the notification. REJECTED is terminal —
the maker posts a new offer rather than fixing this one.
The other actions
| Action | Status after | Escrow effect | Owner notified |
|---|---|---|---|
| Approve | ACTIVE |
unchanged (already held) | yes |
| Reject | REJECTED |
released | yes, with reason |
| Pause | PAUSED |
released | yes |
| Activate | ACTIVE |
topped up to the advertised total | yes |
| Disable | CANCELLED |
released | yes, with reason |
| Flag | unchanged | none | yes |
| Note | unchanged | none | no — internal |
| Edit | per the change | recomputed | per the change |
Three of these need explaining.
Activate is the counterpart to pause and reject: it can bring a paused, disabled, rejected or cancelled offer back. Because those statuses hold no escrow, activating a SELL offer tops the escrow back up to the advertised total, drawing on the owner's current balance. If they have spent that money in the meantime, activation fails — that is correct, since a published SELL offer must always be fully collateralized.
Flag does not change the offer's status. It writes a flag row with your reason and notifies the owner. Use it for "this needs a second look" and for things you want visible on the offer without taking a live offer off the board in front of its counterparties. Unflagging revives the same row rather than creating a second one.
Note appends a timestamped internal note to adminNotes and to the offer's
activity log. Users never see it. This is where the reasoning behind a decision
should go, because the notification the user gets is short and the audit log
entry is structured rather than narrative.
Editing an offer as an admin
The admin edit route can do things the owner's route cannot, including status transitions the owner is not allowed to make. It manages locked funds for SELL offers the same way, and the platform minimum and maximum trade amounts are still enforced — as dollar figures converted into the offer's pricing currency, the same as at creation.
The same blocker applies as for owners: an offer with a trade in PENDING,
PAYMENT_SENT or DISPUTED cannot have its terms changed underneath a live
counterparty.
Trades
/admin/p2p/trade lists every trade with both parties, amount, price, total,
status, escrow status and timestamps. Opening one shows the full timeline —
every status change, every chat message, every admin action — plus the payment
details snapshot and the dispute if there is one.
Adding a note or a message
One endpoint, two behaviours, decided by a flag:
- Note (default) — internal. Recorded against the trade for other admins.
- Message — posted into the trade chat as an admin message, broadcast over the WebSocket, and both parties are notified.
Use messages sparingly and use them to state facts: what you have decided and what you need from each side. Everything else belongs in a note.
Both are sanitised on the same terms as a trader's message. A broadcast is capped at 1,000 characters, the same limit the trade chat enforces — it lands in both traders' chat panels and is rendered by the same component. An internal note is escaped the same way but allows 4,000, because it is staff prose recording why a decision was made rather than a chat message.
Cancelling a trade
Cancels the trade with your reason, settles the escrow back and restores the offer's advertised capacity. It exists for trades that are stuck for reasons that are not a dispute — a payment method that no longer exists, a counterparty whose account has been blocked, a test trade left open on a live board.
If the two parties disagree about money, this is the wrong tool. Use resolve.
Cancelling a disputed trade closes its dispute. Cancelling settles the
escrow, so there is nothing left to rule on — the case leaves your queue with
the outcome recorded as CANCELLED, and stops ageing against the response
target on the dashboard.
It is recorded as CANCELLED rather than as either side winning because nobody
won: the trade was unwound and the money went back where it came from. Anyone
auditing the case later sees a cancellation, not a ruling that was never made.
Force-resolving a trade
/admin/p2p/trade/<id> offers the same four outcomes as dispute resolution, and
accepts a trade in DISPUTED, PAYMENT_SENT or PENDING. That is wider
than the dispute door on purpose: it lets you settle a trade that never had a
dispute filed.
| Outcome | Trade ends | Money |
|---|---|---|
BUYER_WINS |
COMPLETED |
escrow to the buyer, less the platform fee |
SPLIT |
COMPLETED |
divided by buyerPercentage (0–100) or buyerShare; 50 % if neither is given |
SELLER_WINS |
CANCELLED |
escrow back to the seller, no fee, offer capacity restored |
CANCELLED |
CANCELLED |
identical to SELLER_WINS |
Both doors route through the same escrow authority, so SPLIT means the same
thing in each. If a dispute exists on the trade, resolving here marks it
RESOLVED with the same outcome.
fundsReleased is true only if a credit, a refund or a fee was genuinely
booked — not merely that a branch ran. A trade whose escrow was already settled
by another door returns "Trade resolved, but no escrow was moved", and the
warning is worth reading rather than dismissing: it usually means the seller
already released, or the expiry cron got there first.
Running the timeout handler by hand
There is a manual trigger for the expiry job on the trades screen. It runs the
same handler the cron runs every minute: expire overdue PENDING trades,
auto-dispute PAYMENT_SENT trades older than 24 hours, expire dead offers.
Use it when a payment window has clearly lapsed and nothing has happened. If it fixes things, your cron process is not running — fix that, because the manual button is not a substitute for it.
Activity log
/admin/p2p/activity is the P2P event stream: trades initiated, payments
confirmed, trades completed, cancelled and expired, disputes created and updated,
offers expired, payment methods created, reputation milestones, and every admin
action.
Admin actions are additionally written to a dedicated admin-activity table inside the same transaction as the change they describe, so a rolled-back resolution does not leave a log entry claiming it happened.
The hourly reputation job writes a log row per user. It now writes one only when a score actually moves — previously it wrote one per active user per hour forever, and because the dashboard's recent panel takes the five newest rows with no type filter, every hour those five were reputation noise hiding the trades and disputes the panel exists to show.
Rate limits on admin actions
Fixed in code, per admin:
| Door | Limit |
|---|---|
| Offer actions (approve, reject, pause, activate, disable, flag, edit) | 50 per hour |
| Trade actions (resolve, cancel, note) | 100 per hour |
| Dispute actions | 100 per hour |
A bulk cleanup of a stale board will hit the offer limit. Spread it, or do it against the database with the escrow consequences in mind.