Skip to main content
Three roles, three entry points, all SIWE-only. 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.
Locked in the 2026-07-01 redesign. Violating it produces the exact bug the redesign fixed: a pool-only merchant watching money arrive while their dashboard showed an empty table. The invariant applies to merchant surfaces only. Platform-admin surfaces still legitimately read 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_intents at all statuses, with a fees strip above it sourced from pool_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.
Invoices and Team were Phase-2 stubs and were deleted in the redesign.

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-nums and font-mono so 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

Partner earnings always display $0.00, and partner-visible merchants RLS is incomplete. Both are open from the 2026-07-28 audit. This matters beyond the dashboard: if a fee change accidentally reduces the partner’s 0.25%, nobody would notice, which is why the gas-reimbursement work carries an explicit regression test on the partner share.

Claim experience

The dashboard is moving to a single Claim button with a live gas estimate, hiding distribute() 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.