Reviewing one offer — the moderation doors and the admin edit

The offer case at /admin/p2p/offer/[id] — which doors a status permits, what each one does to the maker's collateral, the required reason, and what the admin edit screen can do that the owner's cannot.

7 min readUpdated 6 August 2026admin, offers, moderation, escrow, approval

/admin/p2p/offer/<id> is where one offer is decided. The list at /admin/p2p/offer finds it; this screen is where the money moves. Moderating offers and trades covers the list and the action table — this page covers the case screen and the admin edit form behind it.

What Permission
Read the offer case view.p2p.offer
Every door, the note box and the edit form edit.p2p.offer
Reads the offer case
Publishes it
Refuses it and releases collateral
Takes it off the board and releases collateral
Puts it back, topping the escrow up
Marks it for another moderator
Cancels it and releases collateral
Appends an internal note
The admin edit

A pending offer is already holding the maker's money

A SELL offer took its full advertised total out of the maker's spendable balance the moment it was submitted, and it stays there for as long as the offer sits in your queue.

This is deliberate — approval must never have to find the funds a second time, or approving becomes an action that randomly fails on insufficient balance. But it means a slow queue is a queue that freezes customers' money, which is exactly why the desk dashboard ages this queue against a 72-hour budget. See the dashboard's offer queue.

Only the doors the status permits are drawn

The Moderate panel derives six flags from the offer's status and renders only the doors whose flag is true. Every one of these mirrors the route's own guard, because all six used to be offered on every offer — "Pause" was live on a rejected offer and could only ever return a 400.

Door Drawn when the status is Result
Approve PENDING_APPROVAL ACTIVE
Reject PENDING_APPROVAL REJECTED
Put back on the board PAUSED ACTIVE
Pause ACTIVE PAUSED
Flag for review ACTIVE, PENDING_APPROVAL, PAUSED unchanged
Take it down ACTIVE, PENDING_APPROVAL, PAUSED CANCELLED

When none of the six applies — a completed, expired, rejected or already cancelled offer — the panel says so and offers only the internal note box.

activate accepts PAUSED, DISABLED, REJECTED and CANCELLED; the screen only draws it for PAUSED. disable has no status guard at all. So an API client can revive a rejected offer or cancel a completed one even though this screen will not. If you script against these routes, that is the difference to know about.

Each door also carries the sentence describing what it does, on the button itself, and the confirmation panel repeats it. The dialog this replaced said "Are you sure you want to {action} this offer?" — the raw action key spliced into a sentence, with no mention of the collateral that a take-down releases.

What each door does to the collateral

This is stated on screen, on the doors that have a collateral effect, with the actual amount and currency filled in.

Door Collateral
Approve normally unchanged — the hold was taken at creation
Reject released to the maker's spendable balance
Pause released
Put back on the board topped up to the advertised total
Flag none
Take it down released

Three of those need the detail.

Approve verifies the collateral and can refuse

Approving publishes the offer, so a SELL offer has to be fully collateralized before it goes ACTIVE. Normally the escrow was taken at creation and this is a no-op. Where there is a shortfall, approve draws the difference from the owner's current balance — and if their balance cannot cover it, the approval is refused with the missing amount named in the message. That is correct: a published SELL offer that is not fully collateralized is an offer that will fail the first trade opened against it.

Approve also validates that the offer has a non-zero amount and at least one payment method before anything else happens.

The approve route writes adminNotes from the optional notes field on the request. The Approve door on this screen sends no notes, so approving clears any internal notes already on the offer. The offer's own activityLog entry and the admin activity trail are unaffected, and so is the flag row — but if the reasoning behind a decision needs to survive, it has to be somewhere other than the notes field.

Put back on the board tops the escrow up

PAUSED, REJECTED and CANCELLED all hold nothing, so activating a SELL offer re-takes the whole advertised total from the owner's current balance. If they have spent that money in the meantime, activation fails with the available and required amounts in the message.

The top-up is computed as total − escrowAmount, not as total, so a partially consumed offer is not double-charged.

Pause and take-down release the attributed escrow

Both call the release helper against the offer's own escrow rather than against amountConfig.total. That is what stops a partially consumed offer from either stranding the remainder or releasing funds that belong to another offer.

Trades already open against the offer are settled on their own terms and are not affected by any of these doors.

The reason field is required, and it is shown to the maker

Three routes require a reason in the body and will 400 without one — reject, flag and take it down. The screen requires it on exactly those three, and the field is labelled with who reads it:

Door Label Who sees it
Reject "Why — the maker is shown this" the maker, in the notification
Take it down "Why — the maker is shown this" the maker, in the notification
Flag for review "What to look at — internal" the next moderator

The reason is sanitised, written into the offer record and carried in the notification. For reject and disable it is the only explanation the maker gets, so a reason that reads "no" is a support ticket you have chosen to open.

Approve, pause and activate take no reason field on this screen.

Flag does not change the status

Flagging writes a row to p2p_offer_flags carrying your reason, who flagged it and when, notifies the owner, and leaves the offer exactly where it is. Use it for "this needs a second look", and for anything you want visible on the offer without taking a live listing off the board in front of its counterparties.

The flag row is soft-deletable, so an unflag/reflag cycle revives the same row rather than creating a second one — the flag association is a hasOne, and a second row would collide with it. The dashboard's flagged count reads distinct flagged offers joined to a surviving offer, which is why it is a count of offers rather than of flag rows.

Note is internal, and it appends

The note box at the bottom right appends a line of the form [timestamp] Admin Name: your text to adminNotes, and records a NOTE_ADDED entry against the offer in the admin activity trail. Users never see either.

This is where the reasoning behind a decision belongs: the notification the maker receives is short, and the audit entry is structured rather than narrative. Ctrl/Cmd + Enter submits.

The admin edit screen

/admin/p2p/offer/<id>/edit is a full offer form — type, currency, wallet type, status, amounts, price model, trade settings, location, user requirements and payment methods — saving through PUT /api/admin/p2p/offer/{id}.

It can do one thing the owner's own edit form cannot: set the status directly. The Status select offers six values — ACTIVE, PENDING_APPROVAL, PAUSED, DISABLED, FLAGGED and REJECTED. The route's request schema lists four more (DRAFT, COMPLETED, CANCELLED, EXPIRED), but they are reachable only from an API client; this screen never offers them.

Two of the six cannot be saved. DISABLED and FLAGGED are not members of the offer's status ENUM, which is DRAFT, PENDING_APPROVAL, ACTIVE, PAUSED, COMPLETED, CANCELLED, REJECTED, EXPIRED. The route has a branch for each — that is where the "disabled" and "flagged" emails live — but the column cannot hold either value, so the database rejects the write and the save fails with a server error. It is a single UPDATE, so the whole edit is lost, not just the status, and because the update runs before the mail step no email goes out either. Use the Take it down door (which writes CANCELLED and releases the escrow) and the Flag for review door (which writes a flag row and leaves the status alone) instead.

A transition into ACTIVE from PENDING_APPROVAL sends the approval email. A transition into REJECTED sends the rejection email, but its reason comes from a rejectionReason field in the request body — and this form does not send one. So a rejection driven from this screen always emails the literal fallback "Does not meet platform requirements", never your notes, and it overwrites adminNotes with Rejected: No reason provided followed by whatever was in the notes box. To reject with a reason the maker can read, use the Reject door above, which requires one.

Platform limits are still enforced whenever the amount or price config changes:

  • p2pMinimumTradeAmount — the offer's own minimum may not fall below it.
  • p2pMaximumTradeAmount — the offer's maximum may not exceed it. An offer that states no maximum is measured on the whole offer's value instead.
  • The per-currency crypto floors, so an edit cannot create a dust offer in an asset whose unit price is nothing like a dollar.

The first two are US dollar figures and are converted into the offer's pricing currency before the comparison, so editing a naira offer is checked against the dollar value of what you typed, not against the number itself.

Both are described on P2P settings.

Two things this route does not do, and both matter:

It does not check for trades in flight. The owner's route (PUT /api/p2p/offer/{id}) refuses an edit while any trade against the offer is PENDING, PAYMENT_SENT or DISPUTED, precisely so terms cannot change underneath a live counterparty. The admin route carries no equivalent check, so editing here can change price, limits or payment methods while somebody is mid-trade. Check the offer's open trades before you save.

It does not move escrow. The route's own summary claims it "manages locked funds for SELL offers"; it imports no escrow helper and touches no wallet. So raising a SELL offer's total through this form advertises an amount the offer is not collateralized for, and flipping a status through this form does not release or take a hold the way the six doors above do.

Use the doors for anything with a collateral consequence. Use the edit form for terms, and prefer taking the offer off the board first.

Rate limit

50 admin offer actions per hour, per admin. That bucket covers approve, reject, pause, activate, flag, disable and the edit route. The note endpoint carries no rate-limit middleware at all, so internal notes are free — as is reading the case.

A bulk cleanup of a stale board will hit it. Spread the work, or do it against the database with the escrow consequences above firmly in mind. The trade and dispute buckets are separate and both allow 100 per hour.