Stealth Addresses
Stealth addresses let you receive payments without linking them to your main identity. Each stealth address has its own keypair—EVM address for registration and EB keys for encryption.
The Problem
With standard L2 transfers, your address is visible:
Bob sends to 0xAlice...
On-chain: Transfer to 0xAlice
Observer: "0xAlice received something"
If 0xAlice is linked to Alice's identity (via ENS, exchange, etc.), her incoming payments are traceable.
The Solution
With stealth addresses:
Alice generates fresh address: 0xStealth_42
Alice shares 0xStealth_42 with Bob
Bob sends to 0xStealth_42
On-chain: Transfer to 0xStealth_42
Observer: "Some address received something"
Alice: Controls 0xStealth_42, can spend
Observer cannot link 0xStealth_42 to Alice.
How It Works
Each stealth address is derived from your master spending key:
const stealth = await wallet.stealth.create({ label: 'Payment from Bob' });
// stealth contains:
// - evmAddress: 0x... (fresh EVM address)
// - bpk: { x, y } (fresh EB public key)
// - index: 42 (derivation index)
Key insight: You generate the stealth addresses yourself. You already know all of them—no scanning needed.
Derivation
Master Spending Key (spending_key)
│
└─── derive(spending_key, "stealth", index)
│
├── Stealth EVM Key (secp256k1)
│ └── evmAddress = 0x...
│
└── Stealth EB Key (Grumpkin)
└── bpk = { x, y }
Both keys are derived deterministically from your master key + index. You can regenerate them anytime.
Using Stealth Addresses
Generate and Share
// Create stealth address
const stealth = await wallet.stealth.create({
label: 'Invoice #1234', // Optional local label
});
console.log('EVM Address:', stealth.evmAddress);
console.log('EB Address:', bpkToAddress(stealth.bpk));
// Share with sender (via invoice, QR code, etc.)
const paymentInfo = {
address: stealth.evmAddress,
bpk: stealth.bpk,
};
Sender Transfers
The sender just needs your stealth address:
await bob.USD.transfer({
to: paymentInfo.bpk,
amount: 100_000000n,
});
Bob doesn't know it's a stealth address—looks like any other transfer.
Check Balances
Since you generated the addresses, you know them all:
// List all stealth addresses
const stealths = await wallet.stealth.list();
// Get total balance across all stealths
const total = await wallet.stealth.getTotalBalance();
// Get individual balances
const balances = await wallet.stealth.getBalances();
for (const { index, evmAddress, balance } of balances) {
console.log(`Stealth ${index}: ${balance}`);
}
Spend from Stealth
You control the stealth keys, so you can transfer funds back from a stealth address:
await wallet.USD.transferFromStealth(stealth.index);
await wallet.USD.transferFromStealth(stealth.index, 50_000000n);
Pre-Generation (Recommended)
Generate stealth addresses ahead of time:
// Generate batch of addresses
for (let i = 0; i < 100; i++) {
await wallet.stealth.create();
}
// List all for use in invoices
const addresses = await wallet.stealth.list();
// Use them one-per-payment
const payment1 = addresses[0]; // For customer A
const payment2 = addresses[1]; // For customer B
Benefits:
- No scanning ever—you know all addresses
- One-time setup, reuse addresses as needed
- Each payment is unlinkable
Registration for Autoshield
If you want autoshield on a stealth address:
// Create and register stealth address
const stealth = await wallet.stealth.create({ register: true });
// Now incoming ERC-20 transfers auto-convert to encrypted
Trade-off: Registration links the stealth EVM address to its BPK on-chain.
Combining with L3
For maximum privacy, combine stealth addresses with L3 staking:
// 1. Receive at stealth address (L2)
// Bob → 0xStealth_42: Enc(100)
// 2. Auto-stake watches incoming funds and stakes them to L3
await wallet.USD.stealth.startAutoStakeWatcher(10_000);
// 3. Later, unstake to any BPK (breaks the link)
const stakes = await wallet.USD.getStakes({ status: 'active' });
await wallet.USD.unstake([stakes[0].id], freshBPK);
Result: Zero link between Bob and your final address.
Privacy Levels
| Setup | Sender Visible | Recipient Visible | Amount |
|---|---|---|---|
| Standard L2 | ✓ Yes | ✓ Yes | Hidden |
| Stealth L2 | ✓ Yes | ✗ No | Hidden |
| Stealth → L3 | ✗ No | ✗ No | Hidden |
Best Practices
- One address per payment — Never reuse stealth addresses
- Pre-generate a batch — Create 50-100 ahead of time
- Use labels — Track what each address is for locally
- Backup your mnemonic — All stealths can be regenerated
- Consider L3 for high-value — Maximum unlinkability
- Use relayer for L3 ops — Don't reveal your stealth when staking
Storage
Stealth metadata is stored locally in IndexedDB:
// Data stored per stealth
interface StealthAddress {
index: number;
evmAddress: string;
bpk: Point;
label?: string;
createdAt: Date;
}
The keys themselves are not stored—they're derived on-demand from your spending key.