No billing history: a paying user cannot see what they bought, when, or why their limits dropped
The wallet can buy a subscription but never shows what was bought. Three questions a paying user will ask have no answer anywhere in the UI:
- "I paid — where is my order?" If the tab was closed and the order id is
gone, the only remaining trace is Paymento's email.
order-statusis keyed byorderIdalone and there is no list endpoint. - "What did I buy, and when?" Nothing records it. Settings → Subscription shows the CURRENT plan, its expiry and two usage bars — no history.
- "Why did my limits drop?" The gateway lazily demotes a lapsed paid
subscription to free with no error — silently lower limits.
PlanDowngradeWatchercatches it and opens the upgrade modal with the 'expired' banner, but nothing says which plan expired or when.
The data already exists
paymento_orders carries order_id, token, plan_id, email,
price_cents, status, status_name, api_key / upgrade_api_key,
key_revealed, settlement_json, fulfilled, awaiting_confirmation,
created_at, updated_at.
GET /api/paymento/order-status?orderId=… already returns orderId, status
(created | pending | paid | failed), statusName (raw Paymento, incl.
UserCanceled), fulfilled, confirming, settlement (on-chain details from
/verify), upgrade, maskedKey and planName. GET /api/paymento/key-info
(by x-api-key) returns maskedKey, planName, subscriptionState
(active | expired | inactive) and activeUntil.
So date, plan, amount, status, settlement and expiry are all available today. What is missing is a way to ask for them per wallet, and a surface to show them.
The purchase IS linkable to the wallet on the normal path
PlanScreen.startCheckout always asks for an in-place upgrade of the key the
wallet already holds ("same key, new plan, fresh 30 days"); a fresh key is
minted only against a pre-upgrade gateway or when the user explicitly picks
forceNewKey. That existing key is the identity-bound one provisioned at
onboarding through /auth/verify, which carries
api_keys.owner_id = "<network>:<pubkey>".
So for a normal purchase the chain already closes server-side:
paymento_orders.upgrade_api_key → api_keys.owner_id → network:pubkeyNo schema change is needed to answer "the orders belonging to this wallet" for
the common case. Only a fresh-key purchase stays unlinked (api_keys rows
created outside getOrCreateOwnerKey leave owner_id NULL).
Options
A. Local ledger (wallet only). Stop deleting the PendingOrderRecord
(src/sdk/subscription/pendingOrder.ts, added by #504) at terminal status;
fold it into a compact completed entry — order id, plan name, price, created,
settled, status, key mask, never the key — and render Billing from it. No
gateway change, works for fresh-key purchases too. Cost: the history lives in
one browser and cannot be recovered after a wipe or on another device.
B. Server-side history endpoint (gateway). A read endpoint listing orders
whose upgrade_api_key belongs to the caller's owner key. Covers the normal
path with no schema change; survives reinstalls and works across devices.
Authentication must be the pubkey challenge (/auth/challenge +
/auth/verify), NOT x-api-key. Keys are deliberately portable — the wallet
offers "Use a different key" and the timeout path offers "I have a key" — so a
key-authenticated endpoint would hand anyone holding a pasted key the buyer's
order history and their email. Signature auth exposes nothing new: only the
holder of the private key can read, and they already know both halves.
C. Bind fresh-key purchases too. The wallet already calls
order-key-ack?orderId=… after storing a purchased key durably. Signing that
call would let the gateway set owner_id on the purchased key. Note two costs:
it ends the key's portability, and api_keys has a UNIQUE partial index on
owner_id, so a wallet cannot own two keys as the schema stands.
Recommended: A + B — the ledger for the surface and for offline/fresh-key coverage, the endpoint so the history survives a reinstall. C only if key portability is being dropped anyway.
Two related facts the page should state honestly
- There is no cancellation and no refund. The Paymento store has no cancel
endpoint;
payment_sessions/refund_amountbelong to the legacy/api/payment/*flow, not to this one. "Cancel this payment and start over" sets a localabandonedAtonly — the order lives on and can still settle (which is #508). Nothing to cancel in the subscription sense either: it is not recurring, it simply expires after 30 days. - A downgrade costs the remaining days.
isPlanSelectableblocks only re-buying the CURRENT active plan, so a cheaper plan is purchasable; on fulfilmentchangePlan(key, planId, expiry)sets the new plan with a fresh 30 days and no proration. Buying down silently discards whatever was left. Either warn in the UI or refuse the move while a paid plan is active.
Bonus: this gives #508's orphaned key a home
#508 describes a key delivered by a cancelled-but-settled order that has nowhere to land: the toast points at Settings → Subscription, which cannot reach it, so a paid key is announced and then lost. A Billing list is exactly that missing home — the order appears as a row with "key delivered" and an "install this key" action.
Out of scope, worth filing separately
The browser learns about payment only by polling: the gateway has an IPN callback from Paymento and its own 5-minute re-verification for 24h, but no SSE or WebSocket anywhere, so the page updates on the next poll tick rather than when the payment confirms.
Related: #501, #504, #508.
Where the gateway-side half is tracked
Everything this page needs from the SGW — the per-wallet order listing, a push so the screen reacts when a payment confirms, cancellation, proration and an expiry signal — is collected in unicitynetwork/aggregator-subscription#83. Options B and C above are items 1 and 6 there; option A (the local ledger) needs nothing from the gateway and can ship on its own.
The downgrade warning this page would have to explain is filed separately as #510.
Source: unicity-sphere/sphere