Built by the community - Special thanks to Ruben
Somsen and Josie Bake
for their groundbreaking work on BIP-352.
Installation
Import the SDK:
const shakesco = require ( "@shakesco/silent" );
const {
KeyGeneration ,
SilentPaymentDestination ,
SilentPaymentBuilder ,
ECPrivateInfo ,
Network ,
BitcoinScriptOutput ,
bip32 ,
bip39 ,
} = shakesco ;
Integration Workflow
Generate silent payment address
Create a reusable address for receiving private payments
Create destination address
Generate a one-time taproot address for each payment
Scan for incoming funds
Detect payments without exposing spending keys
Spend received funds
Derive private keys and move Bitcoin
Step 1: Generate Silent Payment Address
Choose your key generation method based on your use case:
Best for: Non-wallet applications where users control their keysconst b_scan = "" ; // Scan private key
const b_spend = "" ; // Spend private key
const keys = KeyGeneration . fromPrivateKeys ({
b_scan: b_scan ,
b_spend: b_spend ,
network: "testnet" ,
});
const silentPaymentAddress = keys . toAddress ();
console . log ( silentPaymentAddress );
Pro tip: Make users sign a message, then derive b_scan and b_spend from the ECDSA signature :
Use r as b_scan
Use s as b_spend (or vice versa)
This ensures cryptographically secure randomness without storing additional keys. Best for: Wallet providers managing user fundsconst mnemonic = "" ; // 12, 15, or 24 word phrase
const keys = KeyGeneration . fromMnemonic ( mnemonic );
const silentPaymentAddress = keys . toAddress ();
console . log ( silentPaymentAddress );
Alternative: From HD key directly
const seed = bip39 . mnemonicToSeedSync ( mnemonic );
const node = bip32 . fromSeed ( seed );
const keys = KeyGeneration . fromHd ( node );
const silentPaymentAddress = keys . toAddress ();
If not using the signature-derived method, ensure you’re using a
cryptographically secure random number generator for b_scan and
b_spend.
Create a Change Address
Critical for privacy: Never send change to a public address after making silent payments.
const keys = KeyGeneration . fromPrivateKeys ({
b_scan: b_scan ,
b_spend: b_spend ,
network: "testnet" ,
});
// Always use label 0 for change (per BIP-352 spec)
const changeSilentPaymentAddress = keys . toLabeledSilentPaymentAddress ( 0 );
console . log ( changeSilentPaymentAddress . toAddress ());
Scenario: You send 10 silent payments to friends, then send change to your public address.Result: You’ve exposed:
❌ Your own private transaction history
❌ Your friends’ payment patterns
❌ Links between all 10 transactions
Solution: Always use a labeled silent payment address for change.Reference: BIP-352 Labels for Change
Step 2: Create Destination Address
Generate a unique taproot address for the payment:
// Parse recipient's silent payment address
const addressPubKeys = KeyGeneration . fromAddress ( silentPaymentAddress );
// Your UTXO details
const vinOutpoints = [
{
txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc" ,
index: 1 ,
},
];
const pubkeys = [
"025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0" ,
];
const UTXOPrivatekey = "" ; // Your UTXO private key
// Build the destination
const builder = new SilentPaymentBuilder ({
vinOutpoints: vinOutpoints ,
pubkeys: pubkeys ,
}). createOutputs (
[
new ECPrivateInfo (
UTXOPrivatekey ,
false // Set true if output is from taproot
),
],
[
new SilentPaymentDestination ({
amount: 1000 , // Satoshis (1 BTC = 100,000,000 sats)
network: Network . Testnet ,
version: 0 ,
scanPubkey: addressPubKeys . B_scan ,
spendPubkey: addressPubKeys . B_spend ,
}),
]
);
// Get the destination taproot address
const destinationAddress = builder [ silentPaymentAddress ][ 0 ];
console . log ( "Send 1000 sats to:" , destinationAddress );
What you need: - UTXO transaction ID and output index - UTXO private key -
Amount in satoshis - Recipient’s scan and spend public keys (B_scan,
B_spend)
Step 3: Scan for Incoming Funds
Scanning trade-off: This is the main drawback of silent payments - you
must scan the blockchain to detect incoming transactions.
Check if a transaction belongs to you:
const vinOutpoints = [
{
txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc" ,
index: 1 ,
},
];
const pubkeys = [
"025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0" ,
];
const search = new SilentPaymentBuilder ({
vinOutpoints: vinOutpoints ,
pubkeys: pubkeys ,
network: Network . Testnet ,
}). scanOutputs (
keys . b_scan , // Your scan private key
keys . B_spend , // Your spend public key
[
new BitcoinScriptOutput (
"5120fdcb28bcea339a5d36d0c00a3e110b837bf1151be9e7ac9a8544e18b2f63307d" ,
BigInt ( 1000 )
),
]
);
const foundOutput =
search [ builder [ keys . toAddress ()][ 0 ]. address . pubkey . toString ( "hex" )]. output ;
console . log ( foundOutput );
If the output matches the taproot address → it’s yours! 🎉
What you need for scanning
Transaction input’s txid and output_index
Public key from the output
Script and amount from the taproot address
Learn more: BIP-352 Scanning
Step 4: Spend the Funds
Once you’ve confirmed funds belong to you, derive the private key:
const vinOutpoints = [
{
txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc" ,
index: 1 ,
},
];
const pubkeys = [
"025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0" ,
];
const private_key = new SilentPaymentBuilder ({
vinOutpoints: vinOutpoints ,
pubkeys: pubkeys ,
}). spendOutputs ( keys . b_scan , keys . b_spend );
console . log ( "Private key:" , private_key );
Use this private key with
bitcoinjs-lib to build and sign
your taproot transaction.
That’s It
You’ve successfully implemented Bitcoin silent payments. Your users can now:
✅ Share a single address for all payments
✅ Receive Bitcoin privately
✅ Maintain transaction unlinkability
✅ Avoid notification transaction fees
Additional Resources