P2P settings
Every field on the P2P settings screen — what it does, which code reads it, which ones are enforced server-side, which only persuade the browser, and which four are stored but read by nothing.
/admin/p2p/settings has four tabs: Trading, Fees & Limits,
Platform and UI & Display. Values are written through the core settings
endpoint into the settings table, and the backend reads them through the
cached settings manager.
false is the string "false", which is truthy. Code that reads a P2P
switch has to compare explicitly, and the ones that matter do. If you add a
consumer of your own, do the same.
The page keeps to one rule: every field on it has a reader. Fourteen controls were removed on 2026-08-03 after grepping the repository for each key — a maker fee that charged nobody, a "Max Active Trades" that capped nothing, an "Enable Dispute System" switch that could not stop a single dispute. The rows survive in the table, so nothing anyone typed was destroyed; the values were simply never read. Four remaining fields are documented below as having no consumer yet, and are marked as such rather than quietly implied to work.
Trading
Minutes, 1–1440. It is step 3 of the payment-window resolution — an offer's own
tradeSettings.autoCancel wins, then its legacy paymentWindow alias, then
this, then a hard-coded 30 minutes.
Read by the timeout cron, the trade detail endpoint, the trade WebSocket, the payment-confirmation endpoint and the trade dashboard. All five must agree; a mismatch means a customer sees a countdown that does not match the deadline actually being enforced.
With it off, an unpaid trade stays open until somebody cancels it by hand, and the seller's escrow stays locked the whole time. It does not disable the 24-hour stale-payment auto-dispute, which is a different safety net.
The trade room counts up from the moment a dispute is opened and shows both traders how long it has been and how much of this window is left. It answers "has anyone even looked at this?" on the page, which is otherwise a support ticket — a second queue, staffed by the same people, asking about the first one.
Set it to what you can actually meet. A target you miss is worse than no target, because the screen tells the trader you missed it. Set it to 0 and the room shows the elapsed time and promises nothing, which is the honest setting for a desk that cannot commit to a turnaround.
Nothing resolves a dispute when this elapses, and nothing should. An escrow decided by a timer rather than by a person would pay somebody out on silence alone, which is the one outcome a dispute system exists to prevent. Passing the target makes the room say the case is overdue. That is all it does.
Leave it on where your traders pay by bank transfer. The reference is what lets a seller match an incoming credit to a trade, and its absence is the most common cause of a P2P dispute.
Turn it off where your traders do not get one. Cash, some wallet transfers and
several mobile-money flows give the payer nothing to quote, and on those the
field is an unanswerable question standing between a buyer who really has paid
and the confirmation that protects them — a trade that expires while its buyer
hunts for a reference number that does not exist is the worst outcome in the
product, because the money is gone, the escrow has returned to the seller, and
an EXPIRED trade cannot be disputed.
The switch moves three things together: the trade room's reference row states
whether it is a rule or a courtesy, the confirm form marks the field required or
optional, and POST /api/p2p/trade/{id}/confirm refuses or accepts an empty
one. That last part is new — the requirement used to live only in the browser,
so anything that was not the platform's own form could confirm a payment with no
reference at all.
The platform's other concurrency rule — one live trade per offer — stops a trader opening the same offer twice and stops nothing else. This is the platform-wide count, and it is what limits somebody tying up a dozen sellers at once with no intention of paying any of them.
Two things it deliberately does not do:
- It counts only the trades a trader opened themselves, against other people's offers. A maker is a participant in every trade taken against their offer, and counting those would cap your best market makers for being popular — fill five of their offers and they could no longer trade anywhere.
- A disputed trade does not count. A trader cannot close a dispute; that needs one of your staff. Counting it would mean raising a dispute costs the trader their ability to trade until somebody gets to the case, which turns a limit into a punishment for using the dispute system.
PENDING and awaiting-payment trades are what count. Refusals arrive as a 409
naming the number they are holding.
Both are enforced in three places: offer creation, trade initiation, and the admin offer edit. An offer whose limits fall outside them is rejected at creation; a trade whose value falls outside them is refused when opened.
Both figures are dollars, and the platform converts them per offer. A 100000 maximum means one hundred thousand US dollars, whatever currency an offer is quoted in — on a naira marketplace it permits roughly ₦136,500,000, at the same fiat rates the rest of the platform uses. The offer form and the offer page both state the bound in the offer's own currency rather than in dollars, so the number a maker reads is the number they can type.
If you have set either bound and a currency has no usable rate in Finance → Currencies, offers priced in it are refused rather than quietly exempted, and the refusal names the currency. A risk limit that silently stops applying is worse than one that stops you — that is exactly how the old dollar minimum came to permit trades worth fractions of a cent.
An install that has set neither bound never needs a rate and is never blocked by this.
Set the maximum at the point where you would rather a counterparty split the order than carry it in one escrow — a single dispute over that amount is a single decision you have to get right.
Fees & Limits
There is no maker fee, no taker fee and no dispute fee. One function computes the charge, at trade initiation, and stores it on the trade.
Three behaviours to know:
- The fee comes out of the buyer's proceeds and is capped at them.
- It is never charged on a refund.
- A floor of
0.0001of the traded currency applies, but only while it stays under 5 % of the trade amount. Without that cap the absolute floor was ten dollars' worth of BTC — enough to equal a minimum-size BTC trade, produce a zero buyer credit, be rejected by the wallet service, and leave the escrow permanently stuck. - A seller who holds Super Admin is exempt entirely.
The fee calculator looks for a settings row keyed p2p first and reads
EscrowFeeRate out of it. Only if that is absent does it fall back to the
p2pEscrowFeeRate key this screen writes.
On a fresh install there is no such blob and this field is authoritative. On an install carried forward from an older release there may be, and then moving the slider changes nothing. If the fee on a completed trade does not match what this screen says, check for that row first.
A currency listed here overrides the dollar minimum above. This is the only way to stop dust trades in an asset whose unit price is nothing like a dollar — a ten-dollar minimum is meaningless on DOGE and larger than a viable trade on BTC.
Enforced at offer creation (against the converted minimum) and at trade
initiation (against the requested amount). The same legacy p2p blob caveat
applies: a MinimumTradeAmounts key inside it wins over this field.
Platform
This is the one switch on the Platform tab that is enforced server-side. Off means every offer and every edit waits in the admin offer list until somebody approves it. It is the strongest lever you have against a bad offer reaching the board, and the one that costs a person's time.
Remember that a pending SELL offer is already holding the maker's collateral. See Moderating offers.
The switches that only hide UI
No P2P API route consults p2pEnabled, p2pMaintenanceMode or
p2pAllowGuestBrowsing. The market board endpoint is deliberately
unauthenticated and answers anyone; the offer and trade endpoints never look
them up.
They stop a customer. They do not stop a script. Server-side enforcement is a backend change and is deliberately not faked here — a switch that claims to lock the door when it only takes the sign down is worse than no switch.
Guest browsing shapes the funnel rather than hiding your book — the board endpoint answers unauthenticated requests either way.
Also browser-side, and genuinely useful before maintenance when you want the book to drain rather than grow.
Two-factor on escrow release
Two controls, both off by default so an existing install keeps behaving exactly as it did until you turn them on. They are a pair, and turning on only the second one is the mistake to avoid.
Turn on enrollment first, then the challenge
p2pTwoFactorChallenge demands a code at release. p2pTwoFactorRequired is
what guarantees the person who will have to answer that challenge actually
has a second factor.
Turn on the challenge alone and a seller who never enrolled reaches Release — already holding a buyer's paid escrow — and cannot proceed. Turn on enrollment first, give your sellers time to enrol, then add the challenge.
Why it is the seller, and why it is checked at trade start
Releasing escrow is a seller-only action, so the release challenge can only ever be answered by the seller. But which party that is depends on the offer:
| Offer type | The maker is | The taker is |
|---|---|---|
| BUY offer | the buyer | the seller |
| SELL offer | the seller | the buyer |
So gating whoever starts the trade would not work: on a SELL offer the person starting it is the buyer, and the seller is a maker who is not in the request at all. The enrollment rule is therefore applied to the resolved seller, whichever side that turns out to be — and SELL-offer creation is gated too, so a maker learns about the requirement on their own offer form rather than through a stranger's failed trade.
Release itself does not enforce enrollment, deliberately. A release refused purely for missing enrollment would demand no code, so it stops nobody holding a stolen session while stranding a seller who is holding a buyer's money. The enrollment check does its work before anyone's funds are committed.
UI & Display
Read by the market page, which resolves anything other than the literal
classic back to guided.
Four fields with no consumer yet
The remaining four are stored and will be honoured when the consumer lands, but nothing reads them today. They are listed here so nobody spends an afternoon wondering why moving them changes nothing.
There is no Security tab
It used to hold three switches — Enable Dispute System, Enable Rating System, Enable Chat System — and no code read any of them. Disputes opened, ratings posted and chat messages sent regardless of the position of all three.
A tab named Security whose every control is decorative is worse than no tab, because it answers "can I turn chat off?" with a switch instead of with the truth, which is that turning chat off requires a backend gate that does not exist.
What is not configurable
Worth knowing before somebody goes looking:
| Behaviour | Value | Where it lives |
|---|---|---|
| Stale-payment auto-dispute delay | 24 hours | code |
| Offer auto-expiry | 30 days inactive and zero remaining total | code |
| Post-completion dispute | not possible — COMPLETED is terminal |
the trade state machine |
| Payment window fallback | 30 minutes | code |
| Minimum payment window on edit | 5 minutes | offer validation |
| Margin price range | −50 % to +50 % | offer validation |
| Max payment methods per user | 20 | code |
| Chat attachment size | 5 MB | code |
| Dispute description length | 20–1000 characters | code |
| All rate limits | see Install | code |
After you change something
Settings are cached and invalidated over Redis, so a change propagates to the other processes without a restart. Two things do not update retroactively:
- The fee on a running trade. It is computed and stored at initiation. Changing the rate affects trades opened afterwards.
- The payment window on a running trade. It is measured from
createdAtagainst the offer's setting, so an offer that names its own window is unaffected by a change here at all.