When you share a public blockchain address, every payment you receive becomes visible to anyone who looks — your total balance, every counterparty, every amount. Stealth addresses eliminate that exposure by giving each payment its own unique, unlinkable destination address that only you can detect and spend.
The problem with public addresses
On most blockchains, addresses are permanent and public. Share your address once and anyone can:
- See every payment you have ever received
- Track your total balance at any time
- Link all financial activity to your identity
Normal: Stealth:
Alice -> 0xBob Alice -> 0xRandom1 (owned by Bob)
Carol -> 0xBob Carol -> 0xRandom2 (owned by Bob)
Dave -> 0xBob Dave -> 0xRandom3 (owned by Bob)
Observers: "Bob received Observers: "Three unrelated
3 payments totaling 5 ETH" addresses received funds"
How stealth addresses solve this
Instead of receiving payments at a single reusable address, each payment goes to a fresh one-time address that only you can detect and spend from. The sender generates a unique address per payment using your public stealth meta-address. You scan announcements to find payments that belong to you.
A stealth meta-address is what you publish or share. It contains two public keys:
st:eth:0x{spendingPubKey}{viewingPubKey}
- Spending public key — used to derive stealth addresses. The matching private key lets you spend.
- Viewing public key — used to detect incoming payments. The matching private key lets you scan.
This two-key design means you can give a third party your viewing key to detect payments on your behalf without giving them the ability to spend.
How a payment works, step by step
Setup (one time)
+----------------------------------------------+
| Wallet signs message |
| | |
| v |
| Derive spending key + viewing key |
| | |
| v |
| Encode stealth meta-address |
| "st:eth:0x{spend}{view}" |
| | |
| v |
| Register as alice.wraith on-chain |
+----------------------------------------------+
Send (per payment)
+----------------------------------------------+
| Sender resolves alice.wraith |
| | |
| v |
| Generate random ephemeral key |
| | |
| v |
| ECDH shared secret with viewing key |
| | |
| v |
| Derive one-time stealth address |
| | |
| v |
| Send ETH + publish announcement |
+----------------------------------------------+
Step 1: Generate a stealth address
import { generateStealthAddress } from "@wraith-protocol/sdk/chains/evm";
const { stealthAddress, ephemeralPubKey, viewTag } = generateStealthAddress(
recipientSpendingPubKey,
recipientViewingPubKey
);
// stealthAddress: fresh one-time address
// ephemeralPubKey: publish on-chain
// viewTag: publish on-chain (1 byte for fast filtering)
The sender:
- Generates a random ephemeral key pair
(r, R)
- Computes a shared secret
S = r * viewingPubKey (ECDH)
- Hashes the secret:
h = hash(S)
- Computes stealth address =
spendingPubKey + h * G
- Sends funds to the stealth address
- Publishes an announcement:
(R, viewTag)
Step 2: Announcement
The sender publishes an on-chain announcement containing the ephemeral public key R and a view tag. This is public, but reveals nothing about who the recipient is.
Receive (scanning)
+----------------------------------------------+
| Fetch announcements from chain |
| | |
| v |
| For each: check view tag (fast reject) |
| | |
| v |
| If tag matches: compute full address |
| | |
| v |
| If address matches: derive private key |
| | |
| v |
| Spend from stealth address |
+----------------------------------------------+
Step 3: Scan for payments
import { scanAnnouncements } from "@wraith-protocol/sdk/chains/evm";
const matched = scanAnnouncements(
announcements,
keys.viewingKey,
keys.spendingPubKey,
keys.spendingKey
);
// matched: announcements that belong to you, with private keys
For each announcement, the recipient:
- Computes shared secret
S = viewingKey * R (same ECDH, other side)
- Checks the view tag — if it doesn’t match, skips (rejects ~255/256 non-matches)
- Computes expected address =
spendingPubKey + hash(S) * G
- If the expected address matches the announced address, it’s yours
Step 4: Derive the private key to spend
import { deriveStealthPrivateKey } from "@wraith-protocol/sdk/chains/evm";
const privateKey = deriveStealthPrivateKey(
keys.spendingKey,
ephemeralPubKey,
keys.viewingKey
);
// privateKey controls the stealth address
The stealth private key: stealthPrivateKey = spendingKey + hash(sharedSecret) mod n
Without view tags, scanning requires a full ECDH computation and point addition for every announcement on the chain. View tags add a 1-byte shortcut that eliminates ~255/256 non-matching announcements with a single byte comparison:
With 1000 announcements:
Without view tags: 1000 full checks
With view tags: 1000 byte comparisons + ~4 full checks
EVM vs. Stellar
The same concept works on both chains, adapted to their cryptographic primitives:
| Step | EVM (secp256k1) | Stellar (ed25519) |
|---|
| Key derivation | keccak256(r), keccak256(s) of wallet sig | SHA-256("wraith:spending:" || sig) |
| ECDH | secp256k1.getSharedSecret | X25519 (Montgomery form) |
| Hash to scalar | keccak256(S) mod n | SHA-256("wraith:scalar:" || S) mod L |
| View tag | keccak256(S)[0] | SHA-256("wraith:tag:" || S)[0] |
| Address format | 0x... (20 bytes) | G... (56 chars) |
| Signing | secp256k1 ECDSA | ed25519 with raw scalar |
Standards
EVM stealth addresses are based on:
- ERC-5564 — Stealth Address Messenger (announcement format)
- ERC-6538 — Stealth Meta-Address Registry (meta-address storage)
Wraith extends these with:
- WraithNames — human-readable
.wraith name to meta-address mapping
- WraithSender — atomic send + announce in one transaction
- WraithWithdrawer — gas-sponsored withdrawals via EIP-7702
Using the managed agent
With the Wraith agent, all of this is handled automatically. You never call the low-level crypto functions directly unless you are building a custom integration:
import { Wraith, Chain } from "@wraith-protocol/sdk";
const wraith = new Wraith({ apiKey: "wraith_..." });
const agent = await wraith.createAgent({
name: "alice",
chain: Chain.Horizen,
wallet: "0x...",
signature: "0x...",
});
// Sending handles stealth address generation + announcement
await agent.chat("send 0.1 ETH to bob.wraith");
// Scanning handles announcement detection + key derivation
await agent.chat("scan for payments");
// Withdrawing handles private key derivation + transaction signing
await agent.chat("withdraw all to 0xMyWallet");
Use the @wraith-protocol/sdk/chains/evm and @wraith-protocol/sdk/chains/stellar entry points only when you need to build a custom stealth address integration outside the managed agent platform.