Built on the shoulders of giants - Special thanks to Umbra
Cash for pioneering stealth payment infrastructure.
What You’ll Build
The @shakesco/private SDK lets you implement truly private crypto transactions. No one except the sender and receiver can link the payment to the recipient’s known address.
Installation
Import the SDK components:
const shakesco = require ( "@shakesco/private" );
const { KeyPair , RandomNumber , StealthKeyRegistry , utils } = shakesco ;
const { IsUsersFunds , generateKeyPair , prepareSend } = shakesco ;
Security Note: This implementation assumes a single private key secures
your wallet and that you’re signing the same message hash. Multi-sig or
threshold signatures require a different approach.
Complete Integration Workflow
Step 1: Check for Existing Stealth Keys
Before sending private transactions, verify if the recipient has registered stealth keys in the Umbra registry :
const provider = new ethers . JsonRpcProvider ( process . env . RPC_URL );
const registry = new StealthKeyRegistry ( provider );
const { spendingPublicKey , viewingPublicKey } =
await registry . getStealthKeys ( recipientId );
if ( ! spendingPublicKey ) {
console . log ( "User needs to register stealth keys first" );
}
What are spending and viewing keys?
Spending Keys (spendingPublicKey)
Used to generate stealth addresses where funds are sent
Only the recipient can derive the private key to spend from these addresses
Viewing Keys (viewingPublicKey)
Allow scanning for incoming private transactions
Can detect payments without exposing spending ability
Safe to use for monitoring wallets
This separation means you can check for payments without risking your funds.
Step 2: Register Stealth Keys
If the user hasn’t registered, you’ll need to generate and register their key pairs.
Smart Wallets (ERC-4337)
EOAs (Regular Wallets)
For account abstraction wallets, register via a contract call: const provider = new ethers . JsonRpcProvider ( process . env . RPC_URL );
const signer = new ethers . Wallet ( process . env . PRIV_KEY , provider );
const signature = await signer . signMessage ( messageHash );
// Generate deterministic key pairs from signature
const { spendingKeyPair , viewingKeyPair } = await generateKeyPair ( signature );
const registry = new StealthKeyRegistry ( provider );
const { spendingPrefix , spendingPubKeyX , viewingPrefix , viewingPubKeyX } =
await registry . setSmartStealthKeys (
spendingKeyPair . publicKeyHex ,
viewingKeyPair . publicKeyHex
);
Then execute the registration via your smart wallet: const calldata = accountABI . encodeFunctionData ( "execute" , [
CONTRACTS [ chainID ][ "StealthRegistry" ],
0 ,
stealthABI . encodeFunctionData ( "setStealthKeys" , [
spendingPrefix ,
spendingPubKeyX ,
viewingPrefix ,
viewingPubKeyX ,
]),
]);
Storing the viewingKeyPair.privateKeyHex for users is acceptable - it only enables transaction scanning, not spending. This lets you build features like automatic payment detection.
For standard Ethereum wallets, register directly: const provider = new ethers . JsonRpcProvider ( process . env . RPC_URL );
const { spendingKeyPair , viewingKeyPair } = await generateKeyPair ( setupSig );
const registry = new StealthKeyRegistry ( provider );
const { spendingPrefix , spendingPubKeyX , viewingPrefix , viewingPubKeyX } =
await registry . SetEOAStealthKeys (
spendingKeyPair . publicKeyHex ,
viewingKeyPair . publicKeyHex
);
Step 3: Generate Stealth Address for Payment
Ready to send a private transaction? Generate a one-time stealth address:
const payee = "0x..." ; // Recipient's address
const provider = new ethers . JsonRpcProvider ( process . env . RPC_URL );
const { stealthKeyPair , pubKeyXCoordinate , encrypted } = await prepareSend (
payee ,
provider
);
console . log ( stealthKeyPair . address ); // ← Send funds HERE
console . log ( pubKeyXCoordinate ); // ← Share with recipient
console . log ( encrypted . ciphertext ); // ← Share with recipient
Send funds to the stealth address
Transfer crypto to stealthKeyPair.address - this is a brand new address
only the recipient can control
Publish announcement data
The recipient needs pubKeyXCoordinate and encrypted.ciphertext to prove
ownership and spend the funds
Step 4: Announce the Payment
Critical: Without the announcement data, the recipient cannot access their
funds!
Emit this event from your private transaction contract:
event Announcement (
address indexed receiver , // Stealth address
uint256 amount ,
address indexed tokenAddress ,
bytes32 pkx , // pubKeyXCoordinate
bytes32 ciphertext // encrypted . ciphertext
);
How recipients discover payments
Recipients scan the blockchain for Announcement events. Use indexing services for efficient scanning:
The Graph - Decentralized indexing protocol
Moralis - Web3 data APIs
Custom indexer - Query RPC nodes directly (slower)
These services let recipients quickly find all announcements directed to their registered keys.
Step 5: Scan for Incoming Funds
Recipients check if an announcement belongs to them:
IsUsersFunds (
object . announcements [ i ],
provider ,
secret , // Viewing private key
sender
). then (( data ) => {
if ( data . isForUser ) {
// 🎉 This payment is for you!
console . log ( "Amount:" , data . amount );
console . log ( "Token:" , data . tokenAddress );
console . log ( "Stealth address:" , data . stealthAddress );
}
});
Step 6: Spend the Private Funds
Once you’ve confirmed funds belong to you, derive the private key to spend them:
const provider = new ethers . JsonRpcProvider ( process . env . RPC_URL );
const signer = new ethers . Wallet ( process . env . PRIV_KEY , provider );
const signature = await signer . signMessage ( messageHash );
// Regenerate your key pairs (deterministic from signature)
const { spendingKeyPair , viewingKeyPair } = await generateKeyPair ( signature );
// Decrypt the random number used to generate the stealth address
const payload = {
ephemeralPublicKey: uncompressedPubKey ,
ciphertext: ciphertext ,
};
const random = await viewingKeyPair . decrypt ( payload );
// Compute the stealth address private key
const stealthPrivateKey = KeyPair . computeStealthPrivateKey (
spendingKeyPair . privateKeyHex ,
random
);
// Now spend the funds!
const wallet = new ethers . Wallet ( stealthPrivateKey , provider );
const txResponse = await wallet . sendTransaction ({
value: ethers . parseEther ( value ),
to: destinationAddress ,
});
await txResponse . wait ();
console . log ( "✅ Private funds successfully transferred!" );
What’s Next?
While stealth addresses provide strong privacy today, zero-knowledge
proofs will eventually offer even better solutions. Until then, stealth
payments are the best way to bring privacy to Ethereum transactions.
Additional Resources