
Stealth Pay
Stealth-address payment system using Bitgo wallet
Demo
About
The problem it solves # Bitcoin's Privacy Gap Bitcoin is often described as anonymous — it isn't. Every transaction is permanently recorded on a public ledger. If someone knows your wallet address, they can see your entire payment history: every incoming transaction, every balance, every counterparty. Share your address once to receive a payment and you've permanently exposed your financial activity to anyone who looks. ## Real-World Implications This matters in real situations: - A freelancer sharing their Bitcoin address for payment reveals their total earnings and every other client who ever paid them. - A business accepting Bitcoin donations publicly exposes its revenue stream. - Anyone paying a known address can be linked to the recipient by a blockchain analytics firm. ## What This Project Does Stealth-Pay gives every recipient a stealth meta-address — a pair of public keys they can share openly. Senders use those keys to compute a unique, one-time Bitcoin address per payment using ECDH cryptography. Each payment lands at a different address that is: - **Unlinkable** — no two payments to the same recipient share an address. - **Unscannable** — nobody can find your payments without your private view key, not even by scanning the blockchain exhaustively. - **Trustless** — the spend key that authorizes withdrawals never leaves your browser, encrypted with a passphrase only you know. ## Who Can Use It | Use case | How it helps | | --- | --- | | Freelancers & contractors | Publish one stealth meta-address; each client pays a different on-chain address | | Crypto donations | Accept donations without exposing total funds raised or donor list | | Business payroll | Pay employees in Bitcoin without revealing who gets paid or how much | | Personal transfers | Receive funds from friends or exchanges without linking addresses to your identity | | Privacy-sensitive payments | Any scenario where the receiver's address should not be publicly correlatable | ## Why It's Safer Than Alternatives Most "privacy" approaches on Bitcoin today are either mixing (which relies on trusted coordinators or is trivially deanonymized) or address reuse avoidance (which still doesn't help if the recipient shares the same address twice). Stealth addresses solve this at the protocol level — the sender derives a fresh address mathematically, no coordinator required, no cooperation needed beyond sharing the meta-address once. The custodial risk is also eliminated by design: the platform cannot move your funds. The spend private key is encrypted in your browser and the server stores null — following the same model as MetaMask's local keystore.
How it's made
Challenges we ran into # 1. Building a Non-Custodial System Without Compromising UX The biggest design challenge was ensuring the spend private key — the key that authorises withdrawals from stealth addresses — never touches our server, while still keeping the product usable. We took direct inspiration from MetaMask's model: the spend key is returned to the client exactly once on generation, encrypted in the browser using WebCrypto AES-GCM + PBKDF2 (200,000 iterations), and stored in localStorage. The server stores null for the spend key field — by design. The view key (used only for scanning, not spending) lives server-side so scanning can run without user interaction. This split meant we had to build two separate signing paths: BitGo's MPC TSS for outbound sends, and a fully client-side PSBT pipeline for sweeps. # 2. Client-Side Bitcoin Signing in the Browser Signing a Bitcoin PSBT (`bitcoinjs-lib` v7) in the browser using `@noble/secp256k1` v2 surfaced several non-obvious incompatibilities: - **initEccLib() requirement** — `bitcoinjs-lib` v7 ships with no ECC backend. Calling `signAllInputs` without first registering an ECC library throws silently per input, surfacing only as the opaque "No inputs were signed" error. Fixed by writing a custom `TinySecp256k1Interface` adapter backed by `@noble/secp256k1`. - **hmacSha256Sync not wired** — `@noble/secp256k1` v2 ships with `hmacSha256Sync = undefined`. Its synchronous sign() path (required by the PSBT signer interface) calls HMAC-DRBG internally, and throws if this is not set. This error was also swallowed silently by `signAllInputs`, giving the same misleading message. Fixed by wiring `@noble/hashes` HMAC into `secp.etc.hmacSha256Sync` at module load. - **Buffer polyfill** — `bitcoinjs-lib` uses Node.js Buffer throughout. In the browser bundle it wasn't resolving. Required adding the buffer npm package and configuring a webpack fallback in `next.config.mjs`. # 3. Implementing the Stealth Address Protocol Correctly There is no ready-made Bitcoin stealth address library. We implemented the full dual-key scheme from scratch in a dedicated `@stealth/crypto` monorepo package: - Sender side: generate ephemeral `(r, R)`, derive `P = H(r·A)·G + B`, convert to P2WPKH - Receiver side: derive `S = H(a·R)`, reconstruct `P = S·G + B`, compare against stored one_time_address - Spending: derive private key `x = S + b mod n`, build and sign PSBT client-side Getting the scalar arithmetic (`addScalars`, `scalarMult`, `pointAdd`) correct on the secp256k1 curve — and ensuring the derived address on the receiver side matched what the sender computed — required careful implementation and testing at each step. # 4. Blockstream Testnet Reliability Bitcoin testnet infrastructure is notoriously unreliable. During development, Blockstream's testnet API repeatedly returned 504 Gateway Time-out for both UTXO fetches and transaction broadcasts, stalling the entire sweep flow. We resolved this by adding mempool.space/testnet as a sequential fallback with explicit AbortSignal timeouts (15s for UTXO fetches, 20s for broadcasts) across all three places that contact the Bitcoin network.