A privacy-conscious physical shop takes payment, trusts the numbers, keeps clean books, and pays no processor cut — across Bitcoin, Ethereum/Polygon, Solana, Monero and Tari. The app holds no keys: it only watches the chain for settlement.
No custody → no processor cut, no chargebacks. The customer pays the network fee directly; the merchant keeps the sale. Tari L2 settles at ~0 fee.
Privacy coins (Monero, Tari) + observe-only design + a reorg watchdog. The terminal can be stolen and still hold no spendable funds.
A 90-second walkthrough
The whole job is the cashier loop — run a hundred times a day. Step through it below; the terminal on the right updates as you go. Press play, or click any step.
The full picture
An honest, exhaustive outline — grouped the way an owner thinks about the shop. Tags mark what's real on a test network and what's mechanically enforced by the test suite.
Six payment rails
Two detection models. Watch (pull): a keyless on-chain watcher polls the merchant's receiving address. Push / view-key (config-gated): EVM is customer-pays via WalletConnect; Monero, Tari Ootle L2 and Minotari L1 detect from the merchant's own view-key wallet or indexer. A rail with no config falls back to a clearly-marked demo stub and never lights the LIVE badge. Pick a rail:
The product
Real screenshots from the running app (Galaxy-class emulator, light and dark). The terminal runs dim all day, so the dark “Terminal Slate” theme is a first-class surface.







Architecture
The core invariant: the app never signs, never custodies, never moves funds. It builds a payment request for the merchant's address and watches the chain for settlement. Every rail fails closed on an unreachable or unparseable endpoint, and settles each invoice exactly once.
CUSTOMER CRYPTO-POS TERMINAL MERCHANT WALLET
(their wallet) (this app — holds NO keys) (merchant controls keys)
| | |
| cashier rings up a sale | |
| <---- scans payment QR -------- | builds a standards URI |
| BIP-21 / Solana Pay / | for the MERCHANT address |
| EIP-681 | |
| | |
| -------- pays on-chain ------------------------------------> |
| | |
| | WATCHES the chain (read-only) |
| | keyless watcher / view-key |
| | <----- settlement confirmed ----|
| | |
| | books the sale + prints receipt
| | (fail-closed, idempotent) |
The app cannot spend, refund, or move funds. It only observes.
+--------+ generate QR +-------------------+
| IDLE | ---------------> | AWAITING PAYMENT |
+--------+ +---------+---------+
^ | tx seen in mempool
| cancel / new sale v
| +-------------------+
| | MEMPOOL DETECTED |
| +---------+---------+
| | included in a block
| v
| +-------------------+
| | CONFIRMING | count confs vs a trusted tip
| +----+---------+----+
| enough confs | | unreachable / wrong amount / expiry
| +-----------+ +-----------+
| v v
| +--------------+ +--------------+
+----- | CONFIRMED | | FAILED |
settle | settle ONCE, | | fail closed, |
then idle | write 1 row | | no ledger row|
+--------------+ +--------------+
+------------------------------+
| initiateCheckout (router) |
+--------------+---------------+
+-----------+----------+-------+-------+----------+-----------+
v v v v v v
+---------+ +----------+ +---------+ +----------+ +---------+ +-----------+
| BTC/SOL | | EVM | | Monero | | Ootle L2 | |Minotari | | (demo |
| keyless | | Wallet- | | view-key| | indexer | | L1 view-| | stub if |
| watcher | | Connect | | subaddr | | receipt | | key gRPC| | unconfig)|
+----+----+ +----+-----+ +----+----+ +----+-----+ +----+----+ +-----+-----+
+----------+---------+--------------+----------+-----------+
|
every rail fails closed; settles ONCE
v
settleActiveInvoice -> SettlementMath -> Room ledger (+ CSV)
tap MAINNET --> GO-LIVE CHECKLIST --> all accepted rails READY? --> type "MAINNET" --> LIVE
per-rail status: | no (typed confirm,
READY / exact blocker v unchanged hard gate)
switch off rails you Continue DISABLED
don't sell yet (fails closed)
Generated from the on-chain template design docs. The app only reconciles these — refunds pay the buyer directly; the terminal never moves funds.
Package map, the full settle-gate rules per rail, and the test layout live in the engineering docs: ARCHITECTURE.md · RAILS.md · INVARIANTS.md.
Field notes · being upfront
Integrating a real wallet, an L2 indexer, and on-chain templates is where the interesting problems live. In the project's honest spirit, here are the moments worth knowing before you build on Ootle — the gotchas, the engine surprises, and what we proved on-chain. Filter by kind:
The full blow-by-blow lives in the repo's dated journals and the Ootle template docs — this is the upfront summary, not a substitute for reading the source.
For blockchain developers
The Tari Ootle L2 work isn't just a payment detector; it's a set of on-chain retail templates a merchant or developer can adopt. Each keeps the terminal observe-only — the app reconciles and prepares, the owner signs. Every capability below carries an honest status, never inflated past what's been proven.
The "cheaper" claim, made legible
Card acceptance skims a percentage off every in-person sale, then adds chargeback risk on top. A keyless crypto terminal flips that: the customer pays the network fee directly and the merchant keeps the sale. Move the sliders — the blended card rate is grounded in the research below.
A rough, honest estimate — not a quote.
Card cost = volume × rate × 12 + chargebacks × $25 fee × 12.
Merchant-side crypto cost on keyless/L2 rails is approximately zero — the payer covers the network fee, and
there are no chargebacks on confirmed on-chain payments. Default rate and chargeback fee are drawn from the
cited research.
Charter & safety
The app holds no spending or admin key, anywhere. The single bounded exception is a store-scoped loyalty key — testnet + loopback only. Any change that makes the app hold a spending key is out of scope, full stop. Grep tripwires enforce it.
Every rail returns “not settled” on an unreachable, unparseable, or ambiguous result — never a fabricated receipt. A write error fails the settle and leaves no row, rather than booking income that didn't arrive.
No analytics, crash-reporting, or measurement SDK of any kind — not disabled, absent. A NoTelemetryTripwireTest scans the dependency catalog, build scripts, source and manifest on every test run, so such a dependency cannot land quietly.
Mainnet is armed only behind a green Go-Live checklist and a typed “MAINNET” confirmation. The default is always testnet; a fresh or unreadable install comes up safe.
Administrative actions (e.g. Ootle merchant withdrawals) are prepared in-app and signed by the owner in their own wallet. The owner console is observe-only.
Each sale freezes its quote, network and USD value at charge time. The on-screen total, the ledger row, and the exported CSV can never tell different stories — all rendered from one locale-independent money formatter.
Reproducible proofs
Not unit tests — actual on-chain settlements, booked through the app's normal checkout path. Six rails have each settled real money on public mainnet; the two Tari rails are proven on a self-hosted LocalNet — Tari L2 (Ootle) has no public network yet, and Tari L1 (XTM) has a mainnet but its Tor-only P2P wouldn't sync reliably in this build environment (an honest gap, below). Recipes and full write-ups are in the repo's journals and results docs.
| Rail | Network | Amount | Settled | Reference |
|---|---|---|---|---|
| Solana | mainnet | $0.25 SOL | finalized, exact-credit | tx 5B9Myhj…E9vWFX |
| Solana | mainnet | $0.25 USDC | SPL token, exact-credit | tx 2iTHS86m…Teev4 |
| Bitcoin | mainnet | $0.50 BTC | confirmed @ $63,823.75 | tx 9eeec6c8…d433483f |
| Ethereum | mainnet | $0.10 / 0.00005974 ETH | WalletConnect, confirmed | tx 0x1c09b53a… |
| Polygon | mainnet | $0.25 USDC | WalletConnect, confirmed | tx 0x6bf62515… |
| Monero | mainnet | $0.10 XMR | 10+ confs, view-key detect | tx 02b1fd21… (#Tx9) |
| Rail | Network | Amount | Settled | Reference |
|---|---|---|---|---|
| Tari L1 (Minotari) | LocalNet | $5.00 / 9087.27 XTM | ONE_SIDED_CONFIRMED, view-key gRPC | tx 6152428636… |
| Tari Ootle L2 | LocalNet | $1.20 / 10 XTR | Commit&Accept | rcpt_72f1… / 5a9f2c5f… |
| Bitcoin | testnet4 | $1.00 / 1592 sat | block 139028 | tx 1425ca93…43c6 |
| Solana | devnet | $0.10 SOL | slot 468632915 | tx hRHeiYw1…eg344 |
| Monero | stagenet | $0.10 XMR | 10+ confs, unlocked | tx 636f9bd2…f8957bf |
| EVM (Ethereum) | Sepolia | $0.10 USDC | confirm + credit check | tx 0x4182bb0f… |
For developers — AI and human alike
Single Gradle :app module, hand-rolled DI, ~940 host tests. Start at
AUTOPSY.md — a lean router into
docs/autopsy/ with an honest,
current breakdown of what's real vs simulated, the invariants, and the roadmap.
# Build the debug APK (AGP needs JDK 21 — the bundled Android Studio JBR) JAVA_HOME=~/Apps/android-studio/jbr ./gradlew :app:assembleDebug # Host test suite (Robolectric / JUnit) ./gradlew :app:testDebugUnitTest # EVM (WalletConnect) needs a Reown project id in local.properties reown.projectId=YOUR_ID
ui/PosViewModel.kt is the source of truth for the session/cart state and the PaymentSessionState machine; initiateCheckout routes to each rail. The settlement chokepoint is settleActiveInvoice → chain/SettlementMath.kt.
Each rail is a pure parser + a flow under chain/, wc/, monero/, tari/ or minotari/. Parsers are host-tested independent of the network. Honor the settle-gate rules in INVARIANTS.md §1.
No spending/admin key in the app; fail closed; no telemetry. These are mechanically enforced by grep tripwires and NoTelemetryTripwireTest — a PR that crosses them turns the suite red.
Conventions (the green-marker push gate, the test-integrity rules, the modular-docs discipline) are in CONVENTIONS.md. New work is ranked against the mission tier-stack — bookkeeping correctness first.
The research
Loading the cited research…
For blockchain developers · ideas → code → usage
Each idea below is a real Tari Ootle L2 template, compiled against
tari_template_lib rev b1e0308 — most round-tripped on a self-hosted LocalNet, paired with
the verbatim source and how to use it (one is a superseded design draft). Each card's
status badge states its exact proof level. This is the code-and-usage companion to the
capability overview above; the terminal stays observe-only the whole way
(it reconciles and prepares, the owner signs).