@nelo/attest is a native module, and Expo Go has no way to bundle one.
apps/merchant — the till
The screen a shopkeeper actually lives in. Nothing on it says the word crypto.
What it does
Amount entry
A keypad, in the merchant’s own currency. Held as minor units in a
bigint, so no float ever touches a price.Solana Pay
A QR any wallet can pay. No Nelo app on the customer side.
Day-book
Grouped by the merchant’s own calendar day, with close-of-day totals.
Balance
Held in dollars, shown in local currency — both numbers on screen.
Two ways in, and the till does not care which
@nelo/onboard’s flow machine, under test. This file calls the SDK and reports back, and that split is the only reason any of onboarding is verifiable here at all.
create() is not idempotent, so a wallet is never treated as created because a promise resolved. wallet-ready is raised by observing the SDK’s own state, an existing wallet short-circuits the call, and the flow machine refuses a second create. A duplicate leaves the merchant holding an address the day-book has never seen, and there is no undo.What onboarding asks for, in order
1
Market
Nigeria or the Philippines, and it is asked rather than inferred:
0917… is a real prefix in both, so the same digits are a different person depending on the answer. Switching market discards the number already typed.2
Phone number
Normalised to E.164 before anything is sent, so the code goes to the number Privy will actually recognise rather than to what was typed.
3
The code
Six digits. Checked for shape locally first — a four-digit entry never reaches the API — and a resend inside 30 seconds is refused here rather than spent against Privy’s rate limit.
4
The wallet
No question asked. That question is the thing this step exists to remove.
5
Payout account
Bank or mobile money, validated by
@nelo/onboard and stored in the canonical form @nelo/settle takes. The NUBAN check digit warns and never blocks.The balance row
Held in dollars, shown in naira, and the till says both.That is the product decision, not a formatting one: a trader in a devaluing currency who holds overnight is better off in a dollar asset converted at payout, and they should be able to see that is what is happening. Showing only the local figure would be the same trick as showing a made-up exchange rate.
- Conversion rounds down — the figure on screen is never larger than what is held
- A merchant who has never been paid has no token account at all, and that reads as zero, not an error
- A malformed RPC entry is skipped rather than fatal
- An RPC failure leaves the last known figure up: a confident zero is worse than a stale number
- Refreshed on connect and after each settled sale, never on a timer — background polling spends a prepaid data bundle on a figure nobody is reading
- Reads “at a fixed rate” instead of “held in dollars” while the oracle is not live
The rate
Seven Pyth feed ids are wired and each was verified against the Hermes API — one of them was wrong on first pass, reconstructed from a truncated listing, which is why they are checked rather than transcribed.apps/payer — the probe
This is not the product yet, and its own header says so. It exists to answer the questions only a real handset can, and the real payer UI is week-3 work.There is no vault funding and no voucher emission to a merchant in it — it exercises the codec and the secure element as checks.
@nelo/attest. The round trip it runs — keygen in the secure element, sign the 105 bytes, verify with the merchant’s own code — is a check, not a payment flow.
The probe
The screen that answers “does this handset actually work?” — and the one that has to run before anything offline can be trusted:
Check a handset without building anything: