Skip to main content

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);

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

SetupSender VisibleRecipient VisibleAmount
Standard L2✓ Yes✓ YesHidden
Stealth L2✓ Yes✗ NoHidden
Stealth → L3✗ No✗ NoHidden

Best Practices

  1. One address per payment — Never reuse stealth addresses
  2. Pre-generate a batch — Create 50-100 ahead of time
  3. Use labels — Track what each address is for locally
  4. Backup your mnemonic — All stealths can be regenerated
  5. Consider L3 for high-value — Maximum unlinkability
  6. 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.