Every payment on Stellar is a public record: who paid you, who you paid, and the account that ties it all together. Connect that one account to a few dApps and your balances, history, and habits travel with you to every site you touch.
Kivuli is a browser extension wallet that breaks that linkage twice over:
- A wallet per site. Connecting to a dApp derives a fresh, unlinkable Stellar account for that origin alone. Sites never see your other money or each other.
- Private receiving. You hand out ordinary
G...addresses that any wallet or exchange can pay, yet each one is a fresh stealth account only you can connect back to yourself.
It is built on the Opaque protocol's stealth address rails for Stellar, runs against the deployed Soroban contracts on Stellar testnet, and speaks a Freighter-compatible API so existing Stellar dApps connect without a code change.
Testnet. Kivuli for Stellar runs on Stellar testnet, where the Opaque protocol contracts are deployed. Distributed as a GitHub release zip, loaded unpacked. Unaudited: read the security model below.
- A wallet per site. Each dApp origin gets its own ed25519 account, derived deterministically from your seed. The address a site sees exists for that site alone, and because each wallet is funded from its own stealth note, no shared funding address links them on-chain either. That second half is what makes the unlinkability real rather than cosmetic.
- Private receiving. The Receive screen hands out plain
G...addresses from a seed-derived ladder, so any ordinary wallet, exchange, or friend can pay you with a normal transfer. Each payment lands at a fresh one-time account that only you can find and spend, and sweeps wherever you want it. Stealth-aware senders can pay your meta-address instead and the announcement scanner finds it. - Honest approvals. Transaction XDR is decoded into what actually changes for you: every operation in plain words, fees in XLM, memos shown verbatim. Account merges, signer changes, and threshold edits get a hard warning, not a neutral row. An XDR that cannot be decoded says so instead of pretending to be harmless.
- A full surface, not just a popup. The same wallet runs in the popup, Chrome's side panel, and an expanded tab. Issued assets on your accounts are detected from Horizon and shown alongside XLM.
Stellar settles payments for people: remittances, payroll, savings, merchant flows. Those are exactly the payments whose history should not be public reading material. Kivuli gives that privacy a consumer-grade home:
- No new trust. Stealth accounts are ordinary Stellar accounts. The math that hides them runs in your extension; the money moves through standard
create_accountandpaymentoperations. There is no custodian, no mixer pool, and no operator who can lose or freeze your funds. - Payable by the whole ecosystem. Most privacy schemes only work when the sender cooperates. Kivuli's receive ladder inverts that: because the wallet chooses the ephemeral secret itself, the addresses it hands out are plain accounts anyone can pay, and the wallet still holds the key. Freighter users, exchanges, and anchors can pay you privately without knowing Kivuli exists.
- A second wallet standard done right. The per-origin model means dApp connections stop being a tracking vector. Stellar Wallets Kit integrations and Freighter-API dApps connect to Kivuli the way they already know how, and each one meets a different you.
Kivuli implements DKSAP (dual-key stealth address protocol) on Stellar via the Opaque protocol.
One secret. A 12-word recovery phrase derives everything: your viewing and spending keys, your meta-address, every receive address, and every per-site wallet. Restore the phrase and the wallet rebuilds itself, including walking the receive ladder to re-adopt every address you ever handed out.
Receiving from anyone. For each receive address, the wallet derives an ephemeral secret from its seed, runs the same computation a stealth sender would run against its own meta-address, and hands out the resulting one-time Stellar account. Any wallet can pay it. The extension scans its ladder through Horizon, so payments are found by balance, with no cooperation from the sender.
Receiving from stealth senders. Protocol-aware senders pay your meta-address: they derive a one-time account you control, fund it, and publish an announcement (an ephemeral public key and a one-byte view tag) through the stealth announcer contract on Soroban. Your wallet scans announcements, filters by view tag, reconstructs the one-time key for matches, and the money appears in your inbox. Scanning is pure TypeScript from the @opaquecash/stellar SDK.
Sending privately. A stealth send is two transactions: an ordinary transfer funding the recipient's one-time account, then a Soroban invocation publishing the announcement. Kivuli persists the announcement plan before any value moves and retries it on every scan tick, so a crash between the legs never strands money the recipient cannot find.
Sweeping. A discovered note is an account you fully control. Sweeping signs with the reconstructed one-time key and pays your chosen destination directly through Horizon. No relayer is needed, and the connected wallet never appears in the path.
Kivuli's stealth rail touches two Opaque protocol contracts, both live on Stellar testnet and inspectable on stellar.expert:
| Contract | Purpose | Address |
|---|---|---|
| Stealth registry | Publishes meta-addresses so senders can resolve a G... account to stealth keys (ERC-6538 analogue) |
CAIXWMGYZR3YAQ3CPCXOU42WG62E3ARUSG4GDHHDMNRXUD44YSGE5VXW |
| Stealth announcer | Emits the announcement events that let recipients discover stealth payments (ERC-5564 analogue) | CB2Y3GJMPY5BUSZLXG3DSIMERCNCTUM63IIEQ2GUNYEJ3DBKPFIZQGCS |
Neither contract custodies funds: money moves entirely through classic Stellar operations. Contract addresses resolve from the pinned @opaquecash/stellar SDK; the protocol's full address book (privacy pool, verifiers, relayer registry) lives in the Opaque deployment manifest.
- One secret. Everything derives from a 12-word phrase. The backup ceremony verifies the words before the wallet activates, because on this distribution channel the phrase is the recovery path.
- Keys stay in the service worker. The master secret exists only in service-worker memory while unlocked. The vault is encrypted under your passcode (PBKDF2, 600k iterations, AES-256-GCM); the scan cache and the origin-to-address map are encrypted at rest under keys derived from the unlocked wallet, so a locked wallet serves nothing.
- Isolation per origin. Each dApp origin gets its own account, and signing for it requires that origin's connection. A site asking for a signature from any other account is refused.
- A small, unprobeable surface. Install-time host permissions cover only the built-in Horizon and Soroban RPC endpoints (custom ones are granted individually at runtime), and the build strips
web_accessible_resources, so pages cannot probe for Kivuli's presence. - Not audited yet. There has been no external audit, and the wallet runs on testnet. Vulnerability reports go through the private path in SECURITY.md.
- Download
kivuli-extension-stellar-v<version>.zipfrom the latest GitHub release. - Verify it if you like: releases ship
SHA256SUMS.txtand a GitHub build-provenance attestation (gh attestation verify <zip> --repo kivuliapp/extension-stellar). - Unzip into a folder you will keep, then
chrome://extensions, Developer mode, Load unpacked.
Unpacked installs never auto-update, and removing the extension deletes the wallet vault. Before updating anything, read docs/release.md: the update procedure in it is what keeps your vault alive.
Node 20 (.nvmrc).
npm ci
npm run build # typecheck + production build + hardening -> dist/
npm test # full vitest suiteLoad dist/ as an unpacked extension via chrome://extensions (Developer mode). Other useful scripts:
npm run dev # popup UI on mock data (no extension APIs needed)
npm run storybook # component workbench
npm run demo:dev # the test bench (see below) at :5173
npm run demo:build # bench -> demo-dist/, deployable as a static site
npm run release # dist/ packaged as the release zip, with verificationStealth receiving needs a counterparty, so a fresh install has nothing to show until somebody pays you. demo/ is that somebody: a standalone bench page with its own throwaway wallet, funded free by friendbot, that pays plain addresses, pays meta-addresses over the full stealth rail, and doubles as the dApp for provider testing.
The loop: load dist/ unpacked, copy an address from Receive, open the bench, fund its wallet with friendbot, pay yourself, watch the note land in Kivuli, sweep it. Then connect the bench to Kivuli and run the provider smoke tests: the address it reports should be unique to that origin.
Everything runs on Stellar testnet, so the whole loop is free. docs/runtime-qa.md is the full checklist.
Kivuli injects a Freighter-compatible API, so if your dApp already supports Freighter or the Stellar Wallets Kit, it can talk to Kivuli today:
const kivuli = window.kivuli; // also window.freighterApi when no other wallet claimed it
const { address } = await kivuli.requestAccess(); // this origin's private account
const { network, networkPassphrase } = await kivuli.getNetwork();
const { signedTxXdr } = await kivuli.signTransaction(xdr, { networkPassphrase });requestAccess, getAddress, getNetwork, getNetworkDetails, isAllowed, setAllowed, isConnected, signTransaction, signMessage, and signAuthEntry are supported. Errors come back as values in the Freighter shape ({ error: { code, message } }). The one deliberate difference: the address you get is yours alone. Every origin receives a different account, so do not expect your users' balances from other apps to be visible, that is the point.
This build is deliberately narrow. Scope is decided in exactly two places: ENABLED_CHAINS in src/lib/config.ts and capabilities() in src/ui/services/bridge.ts. The UI reads both rather than guessing:
- Stellar testnet only. The Opaque contracts Kivuli builds on hold mainnet until their security register is signed off, and this wallet follows them. The network table has one place for the mainnet row to land.
- Local vault, no accounts server. Onboarding is a passcode plus a 12-word recovery phrase. The Kivuli login model (@handles, pay links, nothing to write down) lands with the server, behind the
kivuliLogincapability. - XLM moves, assets show. Receive, sweep, and send work for native XLM. Issued assets are detected and shown, and can be sent from funded accounts to addresses with a trustline; the stealth rail itself is XLM in this build.
src/background/: the MV3 service worker. Approvals queue, XDR decoding, origin tracking, Horizon access, announcement scanning, the receive ladder, stealth send rails, keyring.src/content/+src/inpage/: the Freighter-compatible provider bridge, re-bundled as self-contained IIFEs byscripts/finalize-dist.mjsso no web-accessible resources ship.src/ui/: popup, side panel, and approval UI over aWalletServiceseam (services/bridge.tsis the real one;services/mock.tspowers dev and Storybook).demo/: the test bench, its own vite root.
The docs go deeper: docs/release.md (release process), docs/runtime-qa.md (runbook), docs/service-delta.md (the service seam's design record), and docs/ux-research.md (design brief).
Kivuli for Stellar is the Stellar sibling of the Kivuli extension (Base/Ethereum) and is built on the Opaque protocol for Stellar, which supplies the DKSAP cryptography, the Soroban contracts, and the @opaquecash/stellar SDK this wallet consumes. Protocol-level issues (contracts, SDK, circuits) belong upstream; wallet-level issues belong here.
Contributions are welcome. See CONTRIBUTING.md for setup, test expectations, commit style, and the recommended branch protection. Bugs and feature requests go through the issue forms; vulnerabilities go through SECURITY.md, never a public issue.
GPL-3.0-or-later.
Every payment deserves the right to be private.