Curating the market page — lenses, suggested counterparties and locale detection

The four UI & Display settings that shape what a visitor sees first on /p2p/market — the default lens, the traders view, the picks shelf and visitor-locale detection — and what each one actually changes.

9 min readUpdated 6 August 2026market, settings, discovery, locale, picks

/p2p/market is the single front door to your marketplace. It absorbed the old /p2p/offer table and the separate /p2p/guided-matching wizard, so it is the page every visitor lands on and the only place their first impression is formed.

Four settings on Admin → P2P → Settings → UI & Display shape that first impression. All four are read. They were stored-but-unread for a period and the settings page still carried a "NOT YET READ" note against them; that is no longer true, and this page is the current description.

Key Type Default Decides
p2pDefaultMarketLens select match which of three views the market opens on
p2pShowTradersLens switch on whether the third view exists at all
p2pFeaturedPicksCount number 3 how many suggested counterparties the shelf carries
p2pDetectVisitorLocale switch on whether the board opens on the visitor's own currency

Every value here arrives at the market page as a string, or as undefined on an install where nobody has touched the control. Each of the four is parsed defensively, and the paragraphs below say what an unparseable value falls back to — because a hand-edited row is a real thing that happens.

p2pDefaultMarketLens — which view opens

The market has three lenses, switched by a tab strip at the top of the page:

Value Tab Shows
match Find me someone a shelf of suggested counterparties, with the full board underneath
offers All offers the offer board directly
traders Traders people rather than offers

The setting decides which one an unqualified arrival sees.

A ?lens= in the URL always outranks it. The lens is part of what somebody sent you, so a shared link opens the view it names regardless of this setting. The three accepted URL values are the same three above.

Anything else — a typo, an empty row, a value from an older release — falls back to match. The same fallback applies if you select traders here while p2pShowTradersLens is off: the market resolves the active lens to match rather than rendering a tab that does not exist.

The lens is synchronised into the URL as you switch tabs, because it changes what is on the page. The filter style is deliberately not — see below.

p2pShowTradersLens — whether the Traders view exists

Adds the third lens, which lists counterparties ranked by completed trades and how fast they release, rather than listing offers.

Turn it off on a young marketplace. A short list of people tells every visitor exactly how thin the book is, in a way a short list of offers does not.

Switching it off does two things, and the second is the one that matters:

  1. The tab is removed from the strip.
  2. A bookmarked ?lens=traders is redirected back to match. Switching a view off has to mean switching it off — otherwise the one visitor most likely to hold that link is the only one who still sees it.

The settings page saves changed keys only. An operator who never touches this switch leaves no row in the settings table at all, and getBooleanSetting(undefined) is Boolean(undefined) — false. Reading it that way would ship every fresh install with the Traders view off, which is the inverse of the documented default.

So the market treats absent, null and empty string as on. Only an explicit stored "false" turns it off. If you need the view gone, use the switch — do not delete the row.

p2pFeaturedPicksCount — the suggested-counterparty shelf

Range 1–12, default 3. This is how many cards the "Find me someone" lens puts in front of a visitor.

Three sit side by side. Above three, the row becomes a horizontal slider — a native snap-scrolled shelf with arrow buttons — so extra suggestions cost a swipe instead of a taller page. Set it to 1 if you want the market to make a single recommendation.

The value does two jobs and both are required:

  • It lays the shelf out, including the skeleton drawn before the picks land. The skeleton keys off the setting rather than off how many picks came back, because a skeleton that guesses a grid where a slider will render is exactly the page shift a skeleton exists to prevent.
  • It is passed as ?count= to the picks endpoint.
The suggested counterparties

The endpoint clamps count to 1–12 and defaults to 3; anything else is treated as a hand-edited settings row. It used to stop at a hard-coded three, which made this setting change the skeleton and nothing else — raising it drew extra placeholder cards that then resolved to nothing.

How a pick is chosen and labelled

The endpoint delegates to the same board query the offer list uses, so a recommendation can never vanish when the visitor opens the full list. It fills three named slots first, then tops up by score:

Label Headline
best Best overall
cheapest Cheapest (buy side) / Highest price (sell side)
safest Safest
alternative Also available

On a thin board one offer can be best, cheapest and safest at once, in which case the shelf is shorter than the count rather than repeating a card.

Every reason on a card is a number this platform measured, rendered as a sentence — "Releases in about 40 seconds", "1,204 completed trades at 99%", "You've traded with them 3 times before". There is no "highly rated" and no "trusted seller", because those are claims your platform would be making on a stranger's behalf.

Cautions sit in the same list as the positives, on the same card — "New trader — no completed trades yet", "Usually takes 22 minutes to release". A downside on a separate screen is a downside nobody reads.

The endpoint is public but knows who is asking

GET /api/p2p/market/picks is unauthenticated — a signed-out visitor gets picks — but it is declared optionalAuth, so a valid session is read when there is one. That changes two things:

  • The viewer's own offers are dropped from the picks. Every card here is a recommendation to open a trade, and a trade against your own offer 404s — so a pick nobody can act on is worse than one fewer pick. This is picks only: the All offers board keeps a maker's own rows, marked and ranked, because a position in a price-ordered list is the thing a maker comes to it for.
  • Prior counterparties are surfaced, and the card says so. A repeat counterparty also scores higher.

A missing or invalid token simply leaves the caller anonymous; the route never refuses.

p2pDetectVisitorLocale — opening on money the visitor recognises

On, the market asks one endpoint where the visitor is and what money they think in, and pre-fills the currency and country filters from the answer.

The visitor's country and currency, best signal first

The precedence, strongest first:

# Signal Answers
0 an explicit ?fiat= / ?country= in the URL both — and the endpoint is not called at all
1 the caller's own P2P history currency only
2 the country on their profile country, and a currency inferred from it
3 a CDN geo header, then the cached IP lookup country, and a currency inferred from it
4 the busiest fiat on the board currency, client-side, as the last resort

History outranks the profile for the currency only, because the profile answers "where do you live" and the currency is then a second guess on top of the first. History answers "which money do you price in" directly, from rows this person created. History never answers the country: nothing in a trade states where the caller is, and the only country on the record is the maker's.

Turning the switch off skips the request entirely, and the board falls back to the busiest pair — which is also what happens whenever detection finds nothing.

Both the currency and the country slots are hard filters on the board query, not display preferences. Applying either blindly shows "no matches yet" to exactly the visitors a young marketplace most needs to keep.

So a detected currency is applied only when the endpoint reports currencySupported and the board actually has offers quoted in it. currencySupported means the platform knows the currency, not that anybody is quoting in it — only the facets know that.

A detected country is applied only when the board has offers from there.

Neither guard is tier-specific, and that is deliberate: a stronger signal is not a licence to skip the check. Somebody whose every trade was priced in ARS, on a board with no live ARS offers, gets the same empty market as a geolocated guess would produce. The signal decides what the page says; it never decides whether the value is worth filtering by.

Whichever way it resolves, the board states what it assumed and the visitor changes it in one click. When a country was assumed and then dropped for emptying the board, the page says that too rather than silently widening.

Detection is fired once per page load, and only after the facets have loaded — they are the authority the two guards consult, and asking earlier means the answer arrives with nothing to check it against. Nothing here restricts, prices or settles anything; it is separate from the geo-restriction pipeline, which decides who may trade, and it works with that feature switched off.

Filter style is no longer an operator setting

The guided sentence and the classic filter row are not two products. They are the same query written for two different readers — and which reader is at the keyboard is a fact you cannot know. The desk that opens this page forty times a day and the first-timer arriving from a share link are on the same install.

So /p2p/market carries a Guided / Classic switch above the filters, and remembers each browser's choice in localStorage under p2p.market.filterStyle. Per browser, not per account: somebody signed out has a preference too, and this decides nothing about anyone's money — it decides which of two controls writes the same six values.

The switch is not drawn on the Traders lens, which has no filters under it.

The control is gone from the settings page and nothing writes the key any more. An install that already set it still has the row, and it still does the one job it can do honestly: it decides what a browser that has never expressed a preference sees. The first click on the switch retires it for that browser forever.

Anything that is not exactly classic — including a typo, an empty row, or the key being absent — resolves to guided. A filter style nobody recognises must never render an empty page.

The style is deliberately not in the URL, in either direction. Every other param on this page changes the response; the filter style changes not one byte of it. Putting it in the query string would make a desk trader's shared link hand a first-time buyer the control row they came here to avoid.

The guided sentence, and the endpoint that is not behind it

The guided presentation is one sentence with editable slots. It collects exactly the same six values as the classic row, into the same intent object, which is synchronised into the URL and sent to the board:

Slot Intent field Board query
buy / sell side side
the asset currency currency
the amount amount amount
the money you pay in fiat fiatpriceCurrency
how you pay paymentMethodId paymentMethods
where country country

The payment-method slot is the one place the two names diverge: the page writes method into its own shareable URL, but the request sends paymentMethods, which is the only name board.get.ts declares and reads (comma-separated ids). A hand-built ?method= on the API is silently ignored.

Nothing below the filter line knows which presentation is on screen. The country slot renders a country name rather than the raw ISO alpha-2 code stored on the offer, and any is a real, selectable value on the payment-method and country slots — both presentations prepend it to the facet list. The asset and currency slots have no such entry; they take the board's facets unchanged. You widen those by clearing the slot, not by choosing any, and the "Any currency" the classic row shows is the placeholder for an unset slot rather than an option in the list.

The route still exists and still works. It requires authentication, and it takes a fixed criteria object — tradeType, cryptocurrency, amount, paymentMethods, pricePreference, traderPreference, location, all required — and runs its own query.

That is the API behind the old wizard, which was retired: /p2p/guided-matching now redirects to /p2p/market, and no page in the product calls the endpoint. It was retired precisely because it computed its results with different code against different filters from the offer table, so a recommendation could disappear when the visitor opened the full list. market/picks.get.ts delegates to market/board.get.ts so that cannot happen.

Treat it as a legacy integration surface, not as the thing your market page is built on.

What is not on this screen

Three switches on the Platform tab — p2pEnabled, p2pMaintenanceMode and p2pAllowGuestBrowsing — draw banners on this page but are not enforced by any P2P API route. The market board endpoint is deliberately unauthenticated and answers anyone. See P2P settings.