Zero-Knowledge Proofs for Private Payments
From RAILGUN-inspired shielded transactions to Proof of Innocence for sanctions compliance. How we built ZK privacy into a production wallet.
Why financial privacy matters for digital payments
When you pay with cash, nobody except you and the merchant knows what happened. No third party records the amount, the time, or what you bought. Digital payments reversed that entirely — every transaction creates a permanent record visible to payment processors, banks, and anyone with a subpoena.
Blockchain made this worse, not better. Public ledgers mean that anyone with your wallet address can see your entire transaction history. Every payment, every balance, every interaction, permanently visible on-chain. For businesses paying suppliers, for individuals buying everyday goods, and for anyone who values financial privacy, this is a problem.
Zero-knowledge proofs offer a way out. They let you prove something is true without revealing the underlying data. We implemented this in a production mobile wallet, a RAILGUN-inspired privacy layer that lets users transact in stablecoins without exposing their history to the world.
ZK-SNARKs: proving without revealing
A zero-knowledge proof lets one party (the prover) convince another party (the verifier) that a statement is true, without revealing anything beyond the truth of that statement. In cryptography, this isn't a metaphor. It's a mathematical protocol with formal guarantees.
ZK-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge) are the specific variant we use. "Succinct" means the proof is small and fast to verify, regardless of how complex the underlying computation is. "Non-interactive" means the prover generates the proof once and the verifier checks it, with no back-and-forth communication required.
So a user can prove they own enough USDC to make a payment, that their funds come from legitimate sources, and that the transaction follows the rules. Their balance, transaction history, identity? None of that gets revealed.
RAILGUN-inspired privacy: shield, unshield, transfer
Our privacy layer is inspired by the RAILGUN protocol, adapted for stablecoin commerce on Polygon, Base, and Arbitrum. It implements three core operations:
Shield deposits tokens from a public wallet into a private pool. The user's USDC moves into a RAILGUN-compatible privacy pool, a cryptographic commitment gets added to a Merkle tree accumulator, and from that point the funds are invisible on-chain.
Transfer is where it gets interesting. Funds move privately within the pool, and both sender and recipient remain invisible. The proof demonstrates sufficient shielded balance, correct amounts, and that no tokens were created out of thin air. No public address is ever linked to the transaction. Not pseudonymous. Actually private.
Unshield is the reverse: withdraw funds back to a public address with a ZK proof that includes a Merkle path proving the commitment exists in the pool. The recipient can be a different address than the original depositor (that's the whole point). The cryptographic link between deposit and withdrawal is broken.
Each operation requires a circuit-specific proof. We support four circuits: shield_1_1, unshield_2_1, transfer, and poi (Proof of Innocence). The circuit files — Structured Reference Strings (SRS) — are roughly 50MB each and are downloaded from a CDN with SHA-256 integrity verification.
Proof of Innocence: sanctions compliance with zero knowledge
Here's the feature that makes privacy practical outside of cypherpunk circles.
Regulators don't object to financial privacy in principle. They object to privacy that enables money laundering and sanctions evasion. The standard regulatory response is to ban privacy entirely, which is like banning curtains because criminals might hide behind them.
Proof of Innocence (POI) takes a different approach. It generates a ZK proof that the user's shielded funds are not derived from addresses on the OFAC, EU, or UN sanctions lists. The proof verifies the claim without revealing the user's transaction history, balances, or counterparties. The verifier learns one thing: "these funds are clean." Nothing else.
The mechanism works by proving set non-membership. The proof demonstrates that none of the commitment inputs in the user's transaction graph intersect with known sanctioned addresses, without revealing what those inputs actually are. The proof has a 24-hour validity window and can be regenerated on demand.
Genuinely a breakthrough, if regulators accept it. Instead of choosing between full transparency (current banking) and full opacity (mixers), you get a middle path: prove compliance without surrendering privacy. Whether the regulatory framework catches up is an open question. But the cryptography is sound.
The x402 micropayment protocol
Privacy isn't just about hiding large transactions. We also built a protocol for seamless micropayments that integrates directly with HTTP.
The x402 protocol uses HTTP status code 402 ("Payment Required"), a code that has been reserved since the creation of HTTP but never widely implemented. When an API endpoint requires payment, the server returns HTTP 402 with payment details in the response header: recipient address, amount in USDC (atomic units, 6 decimal places), target network (as a CAIP-2 identifier), and a validity window.
The wallet's HTTP interceptor catches the 402 response and handles the payment flow:
- Parse the payment requirements from the Base64-encoded header
- Check auto-pay rules: is the amount below the user's threshold? Is the daily spending limit intact? Is the network whitelisted?
- If auto-pay conditions are met, sign automatically. Otherwise, show a confirmation modal
- Sign an EIP-3009 TransferWithAuthorization, a gasless USDC transfer that doesn't require ETH
- Retry the original request with the payment signature in the header
- Receive the content as a normal HTTP 200 response
All amounts use BigInt arithmetic, with no floating-point anywhere in the payment pipeline. Single transactions are capped at 100 USDC as a safety measure. The private key used for signing is nullified from memory immediately after the signature is generated.
From the user's perspective, nothing happened except content appeared. No wallet pop-ups, no chain switching, no gas fees. Payments become invisible.
Local versus delegated proof generation
ZK-SNARK proof generation is computationally expensive. On a modern smartphone, generating a shield proof takes 30-60 seconds. On older devices, it might not complete at all.
We handle this with a two-path architecture:
Local proof generation runs on the device's native thread — not the UI thread, which would freeze the app. The circuit's SRS file is loaded from local storage, and the proof is computed entirely on-device. This is the preferred path for privacy: the proof inputs (which contain sensitive information) never leave the user's phone.
Delegated proof generation is the fallback. If the device doesn't have sufficient computational power, the proof inputs are sent to our server, which generates the proof and returns it. The client polls for completion or receives status updates via WebSocket. This is faster but requires trusting the server with proof inputs during computation.
The device capability check happens automatically. Users don't choose. The wallet routes to the appropriate path based on hardware profiling. A progress indicator shows proof generation status regardless of which path is used.
Regulatory reality and what we learned
MiCA (the EU's Markets in Crypto-Assets regulation) left privacy protocols in a grey area. It addresses stablecoins and exchanges but says nothing about privacy pools or ZK proofs. We took that as a signal to build compliance in from the start rather than bolt it on later. Proof of Innocence exists because we think regulators will eventually demand some form of sanctions screening for private transactions. Better to have the answer ready.
Thirty seconds of proof generation is an eternity in mobile UX. Users expect instant payments. We mitigate with progress indicators and the delegated proof option, but there's no getting around the computation cost. GPU-based proof generation will eventually help here. Not available on mobile yet, though.
Circuit file management turned out to be harder than the cryptography. The SRS files total roughly 200MB across four circuits. Downloading, caching, versioning, and integrity-checking them on mobile devices (across iOS and Android, with varying storage constraints) became its own engineering project. We solved it with a CDN-backed manifest system and SHA-256 verification, but honestly, it was more work than implementing the proofs themselves.
The deeper question is where to sit on the privacy-compliance spectrum. Full privacy and full compliance are both technically achievable, but every user and jurisdiction needs a different balance. Proof of Innocence is our current answer. We designed the architecture so the compliance layer and the privacy layer are separable. When the regulatory landscape shifts (and it will), we can adjust one without rewriting the other.
The wallet that runs all of this also uses Shamir's Secret Sharing for key management, which splits private keys across three locations so no single point of failure can compromise the user's funds. The privacy layer and the key management layer are independent but complementary: one protects transaction history, the other protects the keys themselves.
If you're building privacy-preserving financial infrastructure — or trying to understand how ZK proofs work in production rather than in research papers — we'd like to hear from you. You can also explore how we approach regulatory compliance in investment platforms, where the constraints are different but the design philosophy is the same: build compliance into the architecture, not around it.
Working on something similar?
We bring the same engineering approach to client projects. Tell us about yours.