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.

9 min readUpdated 6 August 2026admin, settings, fees, limits

/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

How long a buyer has to pay before the trade cancels itself and the crypto goes back to the seller. Offers may set their own window; this applies to every trade that does not.

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.

Cancels a trade the moment its payment window runs out and returns the crypto to the seller.

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.

How long you undertake to take over a dispute. The trade room counts against it. 0 states no target.

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.

Makes a buyer type the reference their own bank gave them before they can confirm they have paid.

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.

How many trades one trader may have running at the same time. 0 means no limit.

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.

The smallest trade the platform accepts, in US dollars. A real floor, not a hint.
The largest trade the platform accepts, in US dollars, enforced the same way as the minimum.

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

The platform's cut of every P2P trade, taken out of the crypto delivered to the buyer. This is the only fee P2P charges.

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.0001 of 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.

Per-currency floors for trade size, in the traded currency itself.

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

Publishes new and edited offers the moment they are submitted.

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.

Turns the P2P section off for everyone using the site: the market, the offer form and the trade room all show a closed-platform notice.
Shows a maintenance banner across the P2P pages and stops the UI from opening new trades, while leaving trades already running visible.
Lets a signed-out visitor see the market before creating an account.

Guest browsing shapes the funnel rather than hiding your book — the board endpoint answers unauthenticated requests either way.

Turn this off to freeze the book: the offer form refuses to open, so no new liquidity arrives while existing offers keep trading.

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.

The SELLER of a trade must have a second factor enrolled and enabled before the trade can start. Checked at trade start and again at SELL-offer creation.
Every escrow release must be approved with a freshly verified one-time code, proving the person pressing Release still holds the second factor rather than a stolen session.
Allow an authenticator app to satisfy the requirement.
Allow an emailed code to satisfy the requirement.
Allow an SMS code to satisfy the requirement.

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

Choose how visitors filter the P2P market: a plain-English sentence for newcomers, or the standard exchange filter row for frequent traders.

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.

Which view the market opens on for a visitor who arrives without a link that names one. NOT YET READ — the market page currently hard-codes the initial view.
Adds a third market view listing counterparties rather than offers. NOT YET READ — the view is unconditional today.
How many suggested counterparties the "Find me someone" view shows. NOT YET READ — the count is fixed at three, in both the backend picks endpoint and the frontend grid.
Uses the visitor's connection to open the market on their own country and local currency. NOT YET READ — the detection endpoint exists, but the market page does not yet skip it when this is off.

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 createdAt against the offer's setting, so an offer that names its own window is unaffected by a change here at all.