Platform admin is gated on membership in
platform_admins; AdminRoute renders a 403 on a miss rather than redirecting.
The pool-only invariant
Every merchant surface reads pool data only —
pool_deposit_intents, pool_distributions, merchant_pools, and on-chain state. paid_events and payment_intents are the legacy atomic rail and are dropped from all merchant reads.paid_events.
Two tabs regressed after the redesign and were fixed: Analytics merged paid_events with pool data, and Customers merged paid_events with pool_events while keeping a realtime subscription on the legacy table.
Merchant dashboard
Governing principle: the pool system is the only real checkout path. No merchant-facing copy uses the words “phase”, “v3”, “legacy”, or “atomic”.- Overview. The LIVE dot reflects service readiness — can this merchant accept money right now — not payment recency. It previously pulsed green only when a settlement had occurred within 60 seconds, which meant a perfectly healthy merchant with no recent orders looked offline. Totals show a single display currency with per-token chips beneath.
- Payments. One pool ledger over
pool_deposit_intentsat all statuses, with a fees strip above it sourced frompool_distributions, network and token and status filters, CSV export, and a per-order rail badge. - Tokens. Surfaces TRON alongside Available and Coming groups. Per-(token, chain) cells are individually enableable.
- Pool. A unified settlement card that absorbed six earlier cards, plus per-chain activation.
Non-happy-path visibility
The deposit rail has explicit per-invoice statuses and every one is surfaced:underpaid, overpaid, wrong_token, held_sanctioned, swept, expired, expired_paid_late, with confirmed_amount shown against expected_amount.
Before this, an operator could not see “customer paid 47 of 49 EURC” — money simply appeared to be missing with no explanation.
An “unsettled deposits” surface is designed but unbuilt: every case where money arrived but never settled, in one place. It pairs with the gas-debt ledger, since a sweep that produced nothing still cost real money.
Shared patterns
- Numerals use
tabular-numsandfont-monoso columns align. - Queries refetch every 30 seconds and subscribe to Supabase Realtime, so a new insert invalidates without polling.
- Any table over 50 rows gets CSV export via
src/lib/csv.ts. - Dashboard tabs are routable through
?tab=— before that, the browser Back button ejected a merchant to the landing page.
Known gaps
Claim experience
The dashboard is moving to a single Claim button with a live gas estimate, hidingdistribute() entirely as an implementation detail. A DistributeAndClaim helper contract makes it one signature.
The estimate is not cosmetic. On TRON a claim costs the merchant roughly $4.59 in TRX, so claim cadence is a real economic decision they need visibility into. See Distribution.