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