A transfer is accepted because it can be proven legal — not because someone saw it.
The problem · The solution · Who it's for · Features · Contracts · Stack · Architecture · Getting started · Docs
Android 8+ · arm64 · Stellar testnet — balances are test assets with no monetary value
|
|
|
Android will ask you to allow installing from outside the Play Store. Prova is not on the Play Store yet — a payments app has to clear their financial-services review first.
arm64 only, deliberately. The zero-knowledge prover that builds every spend proof is a native
Rust library with no 32-bit build, so a armeabi-v7a device would install the app, run it, and then
fail at the exact moment it tried to send. Shipping one architecture makes that impossible rather
than surprising. Every phone from roughly 2015 onward is arm64.
Balances are testnet assets with no monetary value, and the test network can be reset at any time. Try it freely; do not treat anything in it as savings.
Two phones, since a transfer needs someone to receive it. Everything runs on Stellar testnet — the balances are test assets with no monetary value.
- Install the app and allow installing from outside the Play Store.
- Sign in with your email. A 6-digit code arrives by email, then you choose a PIN.
- Verify your identity — your name, your phone, a photo of your ID and a selfie. It goes to a reviewer, who approves it at /ops. No vendor is connected yet, so this step is manual.
- Add money — free testnet funds, then "Make it private" to move them into the shielded pool.
- Add the person you are sending to — scan their QR from Profile → Account details → Receive privately, or paste the address they send you.
- Send. It appears in Activity on both phones.
A step-by-step version for people who are not developers lives on provapay.duckdns.org.
- Download the app
- Trying it
- Submission
- Screenshots
- User feedback
- On-chain activity
- Overview
- The problem
- The solution
- Who this is for
- Key features
- Smart contracts
- Technology stack
- Architecture
- How a transfer actually works
- Testing
- Repository layout
- Prerequisites
- Getting started
- Environment configuration
- Security & privacy model
- Troubleshooting
- Documentation map
- CI
- Roadmap
Everything a reviewer needs, in one place. Every link below was checked live at the time of writing.
| Item | Link |
|---|---|
| Live demo (website) | provapay.duckdns.org |
| Demo video | Watch the full walkthrough ↗ |
| Android APK | Download v1.2.7 · 86 MB ↗ |
| Operations console | provapay.duckdns.org/ops |
| API health | /healthz · /readyz · /pool/status |
| Pool contract | CBLLKIUUWPH4GCPL4NNK6S6NGDG4OEAX33TTYJ7RPO3SZU52FHYYJEVX |
| Verifier contract | CBQ2HVIYASMYNRIKWM54JUA3A4OGQOWRP42BLMRRB262YQINAA36GD5U |
| Feedback form | Submit feedback ↗ |
| Feedback responses | Response sheet ↗ |
| Requirement | Status | Where |
|---|---|---|
| Production-ready MVP | Done | Deployed end-to-end; a private transfer settles on testnet today |
| Stable frontend + contract architecture | Done | Architecture · Smart contracts |
| Mobile responsive UI | Done | Screenshots — native Android app; the marketing site is responsive |
| Loading states + error handling | Done | Payment states |
| Production deployment | Done | App, API, website and both contracts are live |
| Monitoring / analytics | Done | Monitoring |
| Project structure + documentation | Done | Repository layout · Documentation map |
| Contracts on Stellar testnet | Done | Both contract IDs above, verifiable on Stellar Expert |
| 15+ meaningful commits | Done — 93 | git rev-list --count HEAD |
| Public GitHub repository | Done | github.com/soumen0818/Prova |
| User feedback collection | Done | User feedback |
| Proof of wallet interactions | Partial | On-chain activity — see the note there |
Every screen below is the real app running against Stellar testnet.
Three outcomes, three different things to say. The rule throughout: never imply money moved when it did not, and never imply it is lost when it might still land.
![]() Landing page — provapay.duckdns.org |
![]() Get the app — download and onboarding steps |
Three public endpoints on the API at provapayment.duckdns.org. All three are live and need no
authentication, so a reviewer can confirm the backend is up without taking anyone's word for it.
| Endpoint | Purpose | Live response |
|---|---|---|
/healthz |
Liveness — the process is running, and which asset and environment it is configured for | {"anchorAsset":"XLM","env":"production","maintenance":false,"schemaVersion":"0.1.0","status":"ok"} |
/readyz |
Readiness — dependencies reachable, safe to route traffic to | {"status":"ready"} |
/pool/status |
Shielded-pool state — tree size, current root, queue depth, last fold | {"root":"52e2b300…","treeSize":14,"queueDepth":0,"batch":8,"lastFoldAt":"…"} |
Liveness and readiness are split on purpose: a process that is up but cannot reach Postgres should be live and not ready, so an orchestrator restarts nothing and simply stops sending it traffic.
queueDepth is the number to alert on. Notes are queued by shield/transact and only become
spendable once a permissionless folder batches them into the Merkle tree, so a queue that climbs and
stays up means the folder has stalled and new money is not becoming spendable — while everything else
still looks healthy. treeSize and lastFoldAt are the corroborating figures.
The operations console at /ops is where verifications are
reviewed and support conversations are answered. It shows queue state, per-submission status and
approval timestamps.
Note what is not on that screen: no name, no document, no amount. The app checks identity documents
on the device and never uploads them, so a reviewer decides on the record rather than on the
paperwork — and an operator with full console access still cannot see what anyone is worth or who
they paid. The API also exposes
/healthz,
/readyz and
/pool/status, the last of which reports tree size,
queue depth and the most recent fold — queue depth being the number to alert on, since a rising queue
means new notes are not becoming spendable.
Collected through a public feedback form; raw responses live in the response sheet.
| # | Date | Tester | Wallet | Overall | Reported | Action taken | Status | Commit |
|---|---|---|---|---|---|---|---|---|
| 1 | 22 Aug 2026 | Souvik Mandal | GAG3SU…3YLKR4 |
9 / 10 | No bug. Asked for a shorter pool address. | Receive address re-encoded with a CRC-32 checksum: 302 → 145 characters, and a corrupted paste is now rejected rather than silently accepted. | ✅ Shipped | 996f391 |
| 2 | 29 Aug 2026 | Suman Pradhan | GAGQNP…PXSD22 |
8 / 10 | Bug: balance showed zero right after a transfer. | The balance now shows the total (spendable + confirming) instead of only the spendable part. Spend checks still gate on spendable, so nothing unspendable is offered. | ✅ Shipped | f82f463 |
| 3 | 30 Aug 2026 | Nurudeen Adebayo | GC3TR6…D3FFQT |
9 / 10 | No bug. All aspects Excellent, recommend 5/5. | Nothing to fix — recorded as a post-fix baseline. | — | — |
| 4 | 30 Aug 2026 | Soumita Rana | GBDIL5…UQK2UM |
10 / 10 | No bug. No requests, recommend 5/5. | Nothing to fix. | — | — |
| 5 | 30 Aug 2026 | Arka Dash | GAN7XF…YDZONQ |
6 / 10 | Bug: verification would not submit — "check your connection", but it was fine. | The screen reported every failure as a connection error. It now names the actual cause — expired session, claimed wallet, service down, or real connection loss. | ✅ Shipped | 65d2b71 |
| 6 | 31 Aug 2026 | Soumen Mandal | GB25FQ…GOCVWH |
10 / 10 | No bug. All aspects Excellent, recommend 5/5. | Nothing to fix. | — | — |
| 7 | 31 Aug 2026 | Soumyajit Maity | GCMURV…LMFHKN |
8 / 10 | Bug: "Slow network issue" — on office WiFi. | Requests had a flat 15s timeout and no retry, so a congested network was cut off mid-flight. Waiting calls now get 45s, and idempotent ones retry — never anything that moves money. | ✅ Shipped | 52504b4 |
| 8 | 31 Aug 2026 | Shubhadeep Biswas | GB2TJ3…352RZI |
10 / 10 | No bug. All aspects Excellent, recommend 5/5. | Nothing to fix. | — | — |
| 9 | 31 Aug 2026 | Snigdha Mandal | GBUQV6…6IY5EK |
9 / 10 | Bug: "The app is not working on the wifi network" — on college WiFi. | Same cause and fix as 7. | ✅ Shipped | 52504b4 |
| 10 | 31 Aug 2026 | Abdul Rahman | GDX456…D3Y7HK |
9 / 10 | No bug reported. | Nothing to fix. | — | — |
Three of the five responses raised something actionable, and all three are fixed and committed. Each was picked up the day it arrived — the address request in 1 h 43 m (reported 22 Aug 23:02, committed 23 Aug 00:45), and the balance and verification bugs on the day each was filed.
Ten respondents to date. One skipped the per-aspect grid, so those rows count nine.
| Aspect | Responses | Mean |
|---|---|---|
| Overall satisfaction | 10 | 8.8 / 10 |
| Ease of navigation / UI | 9 | 3.8 / 4 |
| Speed and performance | 9 | 3.7 / 4 |
| Reliability of transactions | 9 | 3.6 / 4 |
| Security features | 9 | 3.3 / 4 |
| Customer support | 9 | 3.3 / 4 |
| Would recommend | 10 | 4.2 / 5 |
The lowest scores track specific defects rather than the product generally. Security features (3.3) and customer support (3.3) are the weakest, and both readings are fair: security is largely invisible in a wallet that hides its own workings, and support is one person answering an in-app inbox. The lowest overall score (6/10) came from the tester who could not submit a verification, and both 3/5 recommendation scores came from the pair on shared WiFi.
All four reported defects are fixed and committed. Ten responses is a small sample and is treated as one: enough to surface real bugs — it surfaced four — and not enough to read a trend into the averages. The value here is the report-to-fix loop, not the scores.
Wallet addresses are each tester's own Stellar account, as supplied on the form, and link to Stellar Expert. Not all resolve to funded accounts: an account is only created on-chain when someone taps Add money, so a tester can install, verify and give feedback without ever funding one. All addresses were checksum-validated before publishing.
Tester email addresses are collected by the form for follow-up and are deliberately not reproduced here — publishing a tester's contact details in a public repository would be a poor trade for a product whose entire argument is that it does not leak what it does not need.
Private transfers relayed through the pool contract on Stellar testnet. Each hash is independently verifiable — the contract, the ledger and the result are all public.
| # | Transaction | Date | Result |
|---|---|---|---|
| 1 | 328e56e8…3bf21c |
29 Aug 2026 05:44 UTC | ✅ Success · ledger 4391734 |
| 2 | ad8e61ee…6843b5 |
29 Aug 2026 06:10 UTC | ✅ Success · ledger 4392045 |
| 3 | a773f25d…6adcd2 |
29 Aug 2026 12:50 UTC | ✅ Success · ledger 4396854 |
| 4 | aee6ab3c…9f7aef |
31 Aug 2026 07:57 UTC | ✅ Success · ledger 4427891 |
All four are invoke_host_function calls against the pool contract CBLLKIUU…, submitted by the
relayer. Transfers 3 and 4 were supplied by testers through the feedback form — transfers made by
somebody other than the team.
Why every transfer comes from one account, on purpose. The proof already hides the amount and the parties — but somebody has to pay the fee and sign the submission, and if that were the sender's own Stellar account the chain would record "this account spent" next to the nullifier and the privacy would be gone in practice. So the backend relays instead, and every transfer arrives from the same account. What an observer learns is "Prova relayed a transfer", which is true of every transfer. The relayer cannot steal or redirect anything: the amount, both output notes, the payout destination and the encrypted payloads are all bound inside the proof. Its only powers are to refuse, and to see that a proof passed through it.
On the 10-user requirement. Onboarding is real and independently visible — the operations console screenshot above shows six approved Tier-2 verifications spanning 17–29 Aug 2026, each one a person who installed the app, submitted identity documents and was reviewed. What this table does not yet do is enumerate ten distinct wallet interactions, because a private transfer is deliberately unlinkable: the chain shows a nullifier and two commitments, never a sender. Additional transaction hashes are being added here as testers complete transfers.
Prova — from "proof." The name is the product: a transfer is accepted because it can be proven legal, not because a bank, a forex desk, and three correspondent banks all got to look at the amount and the identity behind it.
The mental model: a sealed letter with a notary stamp. The post office never opens the letter to know it's valid — it trusts the stamp. Here, the notary is math, it runs on the sender's phone, and the stamp carries zero personal information.
- First corridor: UAE → India.
- Rails: Stellar — it already solved speed, cost, and fiat on/off-ramps (anchors + SEPs). Prova adds the one layer that was missing: privacy in transit, with compliance intact.
- Status: deployed and verified end-to-end on Stellar testnet — see Smart contracts for live contract IDs you can check yourself.
- Scope today: transfers run Prova-to-Prova. Withdrawals to a bank account will follow once a licensed payout partner is connected — the private transfer is built; the last mile is a commercial arrangement, not a missing feature.
Meet Ravi. He works in Dubai; his mother lives in West Bengal. Every month he sends her ₹15,000 — groceries, medicine, the electricity bill. Millions of people do exactly this.
Here's what happens to that ₹15,000 today:
Ravi → UAE bank/exchange → SWIFT correspondent bank → forex desk → Indian bank → Amma
(sees amount) (sees amount) (sees amount) (sees amount)
Five different companies read his exact amount. He loses 5–7% to fees. It takes 2–5 days. And there's nothing he can do about it — that's simply how the system works. This isn't a UX inconvenience, it's structural:
- No privacy — salary, family budget, spending patterns: all visible to every intermediary, and sellable as data.
- Cost — every intermediary takes a cut. The ~$800B/year global remittance market loses tens of billions of dollars to the middle.
- Latency — correspondent banking settles in days, not seconds.
Crypto solved cost and speed years ago. So why hasn't this been fixed for Ravi? Because of the deeper problem: privacy and compliance are mathematical opposites, and nobody has made them work together on a live payment corridor at consumer scale. Every existing payment system is a transparent pipe — every node sees everything, because seeing is how it verifies. To check "does Ravi have enough money," the system reads his balance. To check "is this legal," it reads the amount. To check "is he KYC'd," it reads his identity. You cannot verify something you cannot see — that single constraint is what makes privacy and compliance enemies in every system that exists today.
The amount stays private while it travels, but a mathematical proof travels alongside it that says "trust me, this is legitimate" — and anyone can verify that proof without ever learning the actual number. That's what zero-knowledge means: proving a statement is true without revealing the secret behind it.
Concretely: when Ravi sends ₹15,000, his phone generates a proof that simultaneously asserts (a) the amount is within the legal limit, (b) he holds a valid KYC credential from a licensed anchor, and (c) this exact transfer/note has never been spent before. A Soroban smart contract on Stellar verifies that proof in milliseconds and accepts or rejects it. If accepted, only a commitment hash and a nullifier are written on-chain. The number ₹15,000 appears nowhere.
ZK is to Stellar's payment rails what HTTPS is to the internet. The internet could already move data; HTTPS added a privacy/security layer on top without replacing the pipes. Prova adds a privacy layer on top of Stellar's existing payment pipes — it doesn't replace them.
The regulator doesn't actually need to see the amount — they need to verify three facts (in range, KYC'd, not replayed), and a Groth16 proof verifies exactly those three facts and nothing else. That's the whole trick, and it's why privacy and compliance stop being enemies.
| Role | What they get | Where in the system |
|---|---|---|
| Sender (e.g. Ravi) | A wallet that generates its own keys on-device, verifies identity once, and sends privately — the amount never leaves the phone in the clear. | mobile/ |
| Recipient (e.g. Amma) | Cash-out through a licensed local anchor, same privacy guarantees on the sending leg. | mobile/ + anchor rails |
| Licensed anchors (UAE deposit-side, India payout-side) | Existing SEP-1/6/10/12/24/31 infrastructure they already run for other Stellar products — Prova adds a privacy layer, not a new integration model. | backend/internal/anchor/, Docs/deposit-flow.md |
| Pool operator (the folder) | A permissionless, low-trust role: batches queued notes into the Merkle tree. Can stall the queue, can never mint, steal, or spend. | backend/internal/pool/folder.go |
| Auditor / regulator | Every accepted transfer emits an on-chain event and every KYC decision is written to an append-only audit log — provable compliance without a data request. | Docs/kyc-verification.md |
Privacy
- On-device proof generation — the amount never leaves the phone in the clear, not even to Prova's own backend.
- Shielded-pool note model: on-chain, an observer sees only commitments, nullifiers, and proofs — never balances, never amounts, never who paid whom.
- Encrypted note discovery — incoming payments are findable only by their owner (Jubjub ECDH + Poseidon-derived masking), not by anyone watching the chain.
Compliance, without the surveillance
- KYC once: an anchor-signed credential, verified inside the ZK proof, proves "verified, unexpired, sufficient tier" without ever putting a passport number or a name on-chain.
- Every accepted transfer is an on-chain event; every KYC decision is an append-only audit record — auditable without being surveillable.
Wallet & security
- One master seed, generated on-device, stored only in the platform secure enclave (iOS Keychain / Android Keystore) — never uploaded anywhere in the clear.
- PIN + biometric step-up for every money-moving action.
- Encrypted cloud backup (iCloud / Google Drive) via envelope encryption — a lost phone doesn't mean a lost wallet.
- Real, rate-limited, hashed email one-time codes for sign-in (no dev-only shortcuts in production).
Speed & cost
- Stellar settlement: seconds, not days.
- A folded batch of up to 8 notes updates the entire pool's Merkle root in one on-chain transaction.
Two Soroban (Rust) contracts, both live and verified on Stellar testnet today.
Contract ID:
CBLLKIUUWPH4GCPL4NNK6S6NGDG4OEAX33TTYJ7RPO3SZU52FHYYJEVXNetwork: Stellar Testnet · Explorer: view on Stellar Expert ↗
Custodies real tokens and moves value privately between notes. Verified on-chain: admin matches
the deployed admin key, root matches the circuit's independently-computed empty-tree root,
is_paused is false, queue_depth is 0. Full deployment record, transaction hashes, and the
anchor-key rotation history: contracts/DEPLOYMENTS.md.
| Function | Access | Description |
|---|---|---|
initialize(admin, token, anchor_pk_x, anchor_pk_y) |
one-time | Binds the pool to its custodied token and trusted KYC anchor |
shield(from, amount, note, proof) |
public | Move real tokens in, queue the resulting note |
transact(proof, nullifier, merkle_root, outputs, current_time) |
public | Private transfer — 1 note in, 2 notes out, nothing revealed but a nullifier and two commitments |
unshield(proof, nullifier, merkle_root, outputs, amount, to, current_time) |
public | Withdraw real tokens to a public Stellar address — same circuit as transact, so on-chain shape never reveals which one happened |
update_root(proof, new_root, count) |
permissionless | Folds queued notes into the tree — the contract itself never hashes (see why below) |
set_paused(paused) |
admin | Halts deposits/transfers; withdrawals are never paused |
set_anchor, set_admin, upgrade |
admin | Break-glass operations — see Docs/deployment-and-keys.md §6 |
root(), queue_depth(), is_spent(nullifier), is_known_root(root) |
read-only | State queries — queue_depth is the number to watch operationally |
Why the pool never hashes on-chain: a measured Poseidon permutation costs ~10.97M CPU
instructions against Soroban's 100M-per-transaction budget — a depth-20 Merkle append needs 20 of
them and simply cannot fit. So tree maintenance is deferred and batched: shield/transact/
unshield only ever verify a proof and queue a commitment; a permissionless off-chain folder
periodically proves a batch tree-append and calls update_root. The fold proof enforces
correctness, so a folder can neither mint nor steal — only stall.
Contract ID:
CBQ2HVIYASMYNRIKWM54JUA3A4OGQOWRP42BLMRRB262YQINAA36GD5UNetwork: Stellar Testnet · Explorer: view on Stellar Expert ↗
The earlier design: verifies a KYC-inclusive Groth16 proof (range + commitment + nullifier +
in-circuit anchor signature) without custodying any tokens itself. Verified live on testnet:
verify → true, submit → success + transfer event, ~49.0M CPU per verification.
| Function | Access | Description |
|---|---|---|
verify(proof_a, proof_b, proof_c, commitment, nullifier, anchor_pk_x, anchor_pk_y, current_time) |
public | Pure Groth16 check — no state change |
submit(...) |
public | Verifies, rejects an already-used nullifier, records + emits a transfer event |
is_spent(nullifier), is_committed(commitment) |
read-only | State queries |
Both contracts verify BLS12-381 Groth16 proofs using Soroban's native pairing_check host
function, against a verifying key embedded at compile time — never computed on-chain, always
generated by the prova-prover CLI in circuits/. Full contract-level detail, types, and the
security model behind every entrypoint: contracts/README.md.
| Layer | Technology | Why |
|---|---|---|
| Mobile app | React Native + Expo SDK 56, TypeScript, expo-router | One codebase; native module support for the on-device prover and the platform secure enclave |
| State / data | TanStack Query | The sole state/data-fetching library — no Redux/Zustand |
| On-device crypto (JS) | @noble/curves, @noble/hashes, @noble/ciphers, @scure/base |
Audited pure-JS primitives for everything that isn't Groth16/Poseidon/Jubjub |
| On-device crypto (native) | Rust, ark-groth16, ark-bls12-381, ark-ed-on-bls12-381 |
Groth16 proving is infeasible in JS at usable speed; one Rust implementation shared by mobile, backend, and contracts so nothing can silently drift |
| Backend | Go 1.25, stellar/go SDK, net/smtp |
First-class Stellar SDK; goroutines + strong typing fit a money system's concurrent, must-not-lose-it work |
| Database | PostgreSQL | ACID guarantees for financial state — holds no amounts or PII, only commitments/status/timestamps |
| Cache / rate limiting | Redis | Shared OTP + rate-limit state across API replicas (falls back to per-instance counters if unset) |
| Smart contracts | Rust + Soroban SDK 22 | The only language for Soroban; native BLS12-381 pairing host functions |
| ZK circuits | arkworks (Rust): ark-groth16, ark-crypto-primitives (Poseidon) |
An active, audited Rust Groth16 stack over the one curve Soroban actually supports |
| Blockchain | Stellar Testnet · Soroban RPC · Horizon | Settlement, contract calls, existing SEP/anchor network |
| Shared contracts | TypeScript (shared/src) + Go (shared/go/schema) |
Hand-mirrored, not generated — every cross-repo shape has tests on both sides |
| CI/CD | GitHub Actions, one path-filtered workflow per component | Only the changed component's pipeline runs |
Three trust boundaries, drawn from where secrets and computation actually live — not from which repo a file happens to sit in:
flowchart TB
subgraph phone["Phone — secrets + proving"]
seed["Master seed\n(secure enclave)"]
prover["Rust prover\n(arkworks, on-device)"]
wallet["Wallet UI\n(Expo / React Native)"]
end
subgraph chain["Soroban — verification + anti-replay"]
verifier["verifier contract\n(circuit v2)"]
pool["pool contract\n(circuit v3, custodies tokens)"]
end
subgraph server["Go backend — coordinator, never a viewer"]
api["API"]
indexer["indexer / folder"]
anchors["anchor + KYC orchestration"]
end
subgraph anchor["Licensed anchors"]
uae["UAE anchor"]
india["India anchor"]
end
wallet -->|build proof| prover
prover -->|proof + commitment + nullifier| api
api -->|relay| verifier
api -->|relay| pool
api <-->|SEP-10/24/12, Travel Rule| anchors
indexer -->|read events| pool
indexer -->|read events| verifier
wallet <-->|status, history, Merkle paths| api
style phone fill:#0E0E11,color:#fff,stroke:#E6F94E
style chain fill:#0E0E11,color:#fff,stroke:#E6F94E
style server fill:#0E0E11,color:#fff,stroke:#DCCBF7
style anchor fill:#0E0E11,color:#fff,stroke:#DCCBF7
The rule that makes this work: secrets and proving live on the phone. Verification and anti-replay live on Soroban. Orchestration, anchors, Travel Rule, and history live in the Go backend — which never sees an amount or a raw identity either. It's a coordinator, not a viewer.
sequenceDiagram
participant Wallet as Phone (wallet + prover)
participant Backend as Go backend
participant Pool as Soroban pool contract
participant Folder as Folder (permissionless)
Wallet->>Backend: GET /pool/path/{commitment} (Merkle path for my note)
Wallet->>Wallet: build Groth16 proof on-device\n(membership + nullifier + conservation + KYC)
Wallet->>Backend: POST /pool/spend (proof, nullifier, outputs)
Backend->>Pool: transact(proof, nullifier, root, outputs)
Pool-->>Backend: verified · nullifier recorded · notes queued
Folder->>Pool: update_root(fold proof, new_root, count)
Pool-->>Folder: root advanced — notes now spendable
Wallet->>Backend: GET /pool/notes (scan + trial-decrypt)
Backend-->>Wallet: candidate notes
Wallet->>Wallet: trial-decrypt natively — find what's mine
- Sign up — the app generates a master seed on-device (secure enclave), creates a backend account keyed by email, and signs in with an emailed one-time code.
- Verify once (KYC) — identity documents go from the phone to the verification provider, never through Prova's servers. On approval, the anchor signs a credential the phone stores and never uploads anywhere.
- Add money — deposit into the shielded pool via a real anchor rail (SEP-24) or, in dev, a simulated instant credit.
- Send — the phone selects a note, fetches its Merkle membership path, and generates a Groth16 proof on-device: ownership, a fresh nullifier, value conservation across two outputs, and a valid KYC credential — all without revealing the amount to anyone, including Prova's own servers.
- Submit — the proof goes to the Soroban pool contract (directly, or relayed by the backend). One BLS12-381 pairing check, replay rejection, and the new notes are queued.
- Fold — a permissionless off-chain folder batches queued notes into the Merkle tree with its own proof, making them spendable.
- Payout + Travel Rule — for a cash-out, the two anchors exchange the required data as a sealed, encrypted envelope decryptable only by the receiving anchor, never on-chain.
- History — the backend's indexer reads on-chain events to build a private history the wallet can display; nothing PII- or amount-bearing is ever stored server-side.
| Component | What's covered |
|---|---|
circuits/prover |
~45 black-box shield/spend/fold integration tests where the must-fail cases are the point — every assertion maps to a way money could be stolen, minted, or lost. Plus unit tests per circuit (v2 transfer, KYC credential, FFI round-trips). |
contracts/pool, contracts/verifier |
Contract tests build real Groth16 proofs via prova-prover as a dev-dependency rather than replaying fixtures, so a circuit/contract disagreement fails a contract test, not just a circuit test. Includes an executable CPU-cost gate proving the on-chain-hashing constraint (gate_onchain_merkle_does_not_fit_cpu_budget). |
backend |
Unit + handler tests for OTP (rate limiting, hashing, expiry), the SMTP mailer, rate limiting, KYC provider parsing, pool events, the folder, the prover shell-out, and pool spend handlers. |
mobile |
tsc --noEmit, expo lint, Prettier — enforced in CI; validation logic mirrors and is tested against the same cases as the backend's Go validators. |
shared |
validation_test.go and validation.test.ts assert the same cases on both sides of the TS/Go mirror. |
Run everything locally: see each component's own README for the exact commands
(cargo test, go test ./..., npm run typecheck && npm run lint && npm run format:check).
A single git repository, one folder per component, each with its own toolchain, tests, and CI
workflow. Every component below has its own detailed README.md — this file is the map, not the
whole manual.
| Folder | Stack | What it is |
|---|---|---|
mobile/ |
React Native + Expo (TS) | The consumer app: wallet, KYC, send flow, cloud backup, the native ZK prover bridge |
backend/ |
Go | API, sign-in, SEP/anchor orchestration, KYC state machine, the shielded pool's off-chain half (indexer + folder + relayer) |
contracts/ |
Rust + Soroban | Two on-chain programs: the per-transfer verifier and the shielded pool (real token custody) |
circuits/ |
Rust + arkworks | The ZK circuits (BLS12-381 Groth16) and the on-device prover, shared by mobile, backend, and contracts |
shared/ |
TypeScript + Go | Cross-component schemas — proof format, IVMS101, API types, error codes, the pool/note format, the legal text — mirrored, not generated, in both languages |
web/ |
Next.js (TS) | The public site and, at /ops, the operator console: KYC review queue and support inbox |
Prova/
├── mobile/ Expo app (React Native, TypeScript)
│ ├── src/
│ │ ├── app/ expo-router screens (sign-in, KYC, send, deposit, settings, …)
│ │ ├── features/ tab implementations (home, activity, profile, KYC identity step)
│ │ ├── components/ design-system primitives + app-level components
│ │ ├── lib/ keys, vault, pool, prover bridge, API client, validation, …
│ │ ├── hooks/, constants/, config/
│ ├── modules/prova-prover/ Expo native module → JNI → the Rust prover
│ └── assets/ brand images, fonts, icons
│
├── backend/ Go API service
│ ├── cmd/api/ entrypoint (RUN_MODE selects API / indexer / both)
│ ├── cmd/verifyproof/ dev CLI: submit a proof, print accept/reject
│ ├── internal/
│ │ ├── server/ HTTP router + every handler
│ │ ├── transfers/, chain/, indexer/, anchor/ legacy per-transfer relay + Soroban + SEP rails
│ │ ├── pool/ the shielded pool's off-chain half (service, indexer, folder, relayer)
│ │ ├── kyc/, otp/, mailer/, ratelimit/ identity, sign-in, and abuse controls
│ │ ├── store/ Postgres persistence (PII-free, amount-free)
│ │ └── config/
│ └── migrations/ versioned SQL, embedded + boot-applied
│
├── contracts/ Soroban (Rust) — Cargo workspace
│ ├── verifier/ circuit-v2 per-transfer proof verifier
│ ├── pool/ circuit-v3 shielded pool (token custody, notes, Merkle root)
│ ├── scripts/ deploy_testnet.sh, deploy_pool_testnet.sh
│ └── DEPLOYMENTS.md live contract IDs, tx hashes, verification checks
│
├── circuits/
│ └── prover/ the real crate — everything else in circuits/ is a retired
│ ├── src/lib.rs Circom/BN254 prototype
│ ├── src/credential.rs KYC credential: anchor-signed Jubjub EdDSA
│ ├── src/pool/ shield / spend / fold circuits + Merkle tree + note encryption
│ ├── src/ffi.rs, jni_bridge.rs the mobile native-module bridge
│ └── src/bin/prova_prover.rs the CLI: setup, proving, artifact generation, dev tools
│
├── shared/ cross-component schemas (mirrored, not generated)
│ ├── src/ TypeScript — consumed by mobile/ and web/
│ │ └── legal.ts Privacy Policy + Terms, so app and site publish one wording
│ └── go/schema/ Go — consumed by backend/
│
├── web/ Next.js — marketing site + operator console
│ └── src/
│ ├── app/ public pages (/, /privacy, /terms) and /ops (staff only)
│ ├── components/ site chrome, scroll reveal, shared legal renderer
│ └── lib/ server-only session + backend client (COMPLIANCE_TOKEN never
│ reaches the browser)
│
├── Docs/ product, architecture, and phase-by-phase design docs
├── .github/workflows/ one path-filtered CI workflow per component
└── README.md this file
| Tool | Version | Used by |
|---|---|---|
| Node | 22 LTS (nvm use 22) |
mobile, shared |
| Go | ≥ 1.25 | backend |
| Rust + wasm32 target | stable (see contracts/rust-toolchain.toml) |
contracts, circuits |
| Stellar CLI | ≥ 27 | contracts (deploy), circuits (dev tools) |
| Docker + Compose | recent | backend (Postgres + Redis) |
| Expo dev client | — | mobile (Expo Go cannot load the native prover module) |
Build order matters: shared and circuits/prover are dependencies of the others.
# 1. shared — build first; mobile and backend both depend on it
cd shared && npm install && npm run build
cd shared/go && go build ./...
# 2. circuits — build the prover; backend and contracts both depend on the binary/artifacts it produces
cd circuits/prover && cargo build --release
# 3. contracts — optional unless you're redeploying or changing contract code
cd contracts && cargo test && stellar contract build --optimize
# 4. backend
cd backend
cp .env.example .env # see .env.example for LOCAL DEV vs PRODUCTION values
docker compose up -d postgres redis
set -a && source .env && set +a # bare `go run` does NOT auto-load .env
go run ./cmd/api
curl localhost:8080/healthz
# 5. mobile — needs a development build, not Expo Go (the native prover module won't load in Expo Go)
cd mobile
nvm use 22 && npm install
cp .env.example .env
npm startDeploying the contracts to testnet yourself, generating keys, and understanding which secret goes
where (and which one never touches a server at all) is a full step-by-step in
Docs/deployment-and-keys.md — read §1 first, since two of Prova's
keys are far more dangerous than the rest and the difference isn't obvious from their names.
Every component ships a .env.example labeled by LOCAL DEV vs PRODUCTION value, so there's
one place to look, not a scavenger hunt across scripts:
| Component | File | Notable values |
|---|---|---|
backend/ |
.env.example |
DATABASE_URL, REDIS_URL, POOL_CONTRACT_ID, CONTRACT_ID, RELAYER_KEY, ANCHOR_SEED, SMTP_* (Gmail App Password compatible), AUTH_MODE |
mobile/ |
.env.example |
EXPO_PUBLIC_API_BASE_URL, EXPO_PUBLIC_STELLAR_NETWORK, EXPO_PUBLIC_AUTH_MODE, EXPO_PUBLIC_DEPOSIT_MODE, EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID (cloud backup) |
web/ |
.env.example |
PROVA_API_URL, OPS_PASSWORD, OPS_SESSION_SECRET, COMPLIANCE_TOKEN (must equal the backend's) — the marketing pages need none of these |
No secret is ever required to run the app locally — AUTH_MODE=development accepts a fixed dev OTP
and DEPOSIT_MODE=simulated credits a local counter with no chain or anchor involved. The one key
that must never appear in any .env file, on any server, or in git is the pool admin secret —
see the danger-ranked key table in Docs/deployment-and-keys.md §1.
| Layer | Sees amounts? | Sees identity? | Holds custody? |
|---|---|---|---|
| Phone (secure enclave) | Yes — that's where it's computed | Yes — that's where credentials live | No — never on-chain balances of its own |
| Soroban contracts | No — only commitments/nullifiers | No | Yes — the pool contract custodies real tokens |
| Go backend | No | No — only an opaque userId hash |
No |
| Licensed anchors | Only their own leg (deposit/payout) | Yes — that's their regulatory role | Only during on/off-ramp |
If you take one thing from this table: the backend is the least trusted-with-secrets component in the whole system, on purpose. It coordinates a lot and stores none of what would matter if it were breached.
Concretely, on the code level:
- The master seed and every key derived from it never leave
expo-secure-store(iOS Keychain / Android Keystore) in the clear. - Postgres holds commitments, nullifiers, status, and timestamps — never an amount, never a name.
- The KYC pipeline carries no PII across the wire it doesn't have to: documents go device → provider
directly; the backend only ever sees an opaque
userId = Poseidon(secret, domain). - The pool admin key — the one secret that can replace contract code — is never written to a
.env, a server, or git; only its public address is. See the full danger-ranked key table inDocs/deployment-and-keys.md§1. - Every unauthenticated endpoint (there's no session before sign-in) sits behind rate limiting, so a script can't burn an SMS/email budget or brute-force a six-digit code.
| Symptom | Likely cause |
|---|---|
go run ./cmd/api ignores your .env values |
Bare go run does not auto-load .env — source .env (with set -a/set +a) first, or use docker compose up which loads it automatically |
| Postgres/Redis connection refused | Check for a port collision with another local project; docker-compose.override.yml supports POSTGRES_PORT/REDIS_PORT overrides |
| Every fold rejected | POOL_SETUP_SEED doesn't match the seed the contract's embedded verifying keys were built with |
| Every spend rejected | KYC credential bound to an old identity — re-verify |
queueDepth climbing and not draining |
The folder has stalled, or its relayer key is unfunded — no funds at risk, but nothing new becomes spendable until it resumes |
/pool/* returns 503 |
POOL_CONTRACT_ID is unset, or Postgres is unreachable |
| Mobile app can't find the native prover | You're running Expo Go — the prover is a native module; use a development build (eas build --profile development) |
initialize fails on the pool contract |
Already initialized — it's one-shot; redeploy under a new contract ID if the admin address was wrong |
The full, longer list (with exact commands) lives in
Docs/deployment-and-keys.md §9.
Docs/ is the authoritative source for anything architectural — this repo's standing rule is to
read it before starting any non-trivial change, since Prova is a multi-repo system where the
circuit, contract, backend, and app must agree on shared formats.
| Doc | Covers |
|---|---|
proposal .md |
The product case: the problem, the persona, why ZK + Stellar, why it's defensible |
tech-stack.md |
Stack choices and why, the polyrepo split, the end-to-end technical workflow |
implementation-guide.md |
The phase-by-phase build plan and exit criteria — the roadmap below is generated from this |
shielded-pool.md |
The note/UTXO design, the Merkle-fold architecture, the full must-not-break invariant list |
kyc-verification.md |
The verification state machine, credential issuance rules, tiers |
deposit-flow.md |
How money enters a Prova wallet (simulated vs. real anchor rails) |
account-recovery.md |
Cloud backup, envelope encryption, the restore flow |
signup-and-validation.md |
Sign-up, field validation (client + server), rate limiting, email delivery |
deployment-and-keys.md |
Every key, what it can do, where it goes, step-by-step contract deployment |
environments.md |
Environment matrix and secrets handling |
design-system.md |
The UI style guide — dark theme, chartreuse accent, rounded glassy fintech look |
branding-assets.md |
Every brand/marketing image, spec, and generation prompt |
All workflows live in .github/workflows/ and are path-filtered — each
runs only when something it actually depends on changes.
| Workflow | Runs on changes to | Checks |
|---|---|---|
web-ci.yml |
web/, shared/src/ |
typecheck, Prettier, next build |
mobile-ci.yml |
mobile/, shared/src/ |
typecheck, expo lint, Prettier |
shared-ci.yml |
shared/src/ |
typecheck, build |
backend-ci.yml |
backend/, shared/go/ |
gofmt, go vet, build, tests |
contracts-ci.yml |
contracts/, circuits/ |
fmt, clippy, wasm build, tests |
circuits-ci.yml |
circuits/ |
fmt, clippy, tests |
docker-ci.yml |
backend/, shared/go/, .dockerignore |
image build + compose validation |
The filters follow the real dependency graph, not the folder names, because the two disagree in three places:
- The Go backend consumes
shared/gothrough areplacedirective, so a change there compiles into it —backend-ciwatchesshared/go/**for that reason. contracts/poolbuilds real Groth16 proofs in its tests viaprova-proveras a path dev-dependency, socontracts-ciwatchescircuits/**.- The Node pipelines watch
shared/src/**rather than all ofshared/**, so a Go-only edit does not run the mobile and web jobs for nothing.
A web-only change therefore runs web-ci and nothing else.
| Phase | Ships | Status |
|---|---|---|
| 0 — Foundations | 5-component scaffold, CI, environments, shared schemas | Done |
| 1 — Core ZK on testnet | A Groth16 proof verifies on Soroban | Done — pivoted BN254→BLS12-381 after discovering Soroban has no BN254 host functions |
| 2 — Stellar rails | Commitment/nullifier store, testnet anchor deposit flow | Done |
| 3 — KYC attestation | In-circuit anchor-signed credential check | Done |
| 4 — Mobile prover UX | On-device proving, honest progress, the shielded pool | Core done — on-device latency benchmarking on real low-end hardware is the one remaining manual step |
| 5 — Real corridor | Licensed anchors, Travel Rule, public trusted-setup ceremony, audit | Not started |
| 6 — Extraordinary | Selective disclosure, proof aggregation, compliance-proof-as-an-API | Not started |
Full detail, exit criteria, and risks per phase: Docs/implementation-guide.md.

















