@shakesco/silent
v1.1.4
Published
Bitcoin Silent Payments
Readme
@shakesco/silent
JavaScript SDK for receiving Bitcoin privately with silent payments (BIP-352).
Special thanks to Ruben Somsen and Josie Bake for their groundbreaking work on BIP-352.
What It Does
The @shakesco/silent SDK lets you implement Bitcoin silent payments, allowing users to:
- ✅ Share a single address for all payments
- ✅ Receive Bitcoin privately
- ✅ Maintain transaction unlinkability
- ✅ Avoid notification transaction fees
Learn more:
Installation
npm i @shakesco/silentQuick Start
const shakesco = require("@shakesco/silent");
const {
KeyGeneration,
SilentPaymentDestination,
SilentPaymentBuilder,
ECPrivateInfo,
Network,
BitcoinScriptOutput,
bip32,
bip39,
} = shakesco;Integration Workflow
- Generate silent payment address
- Create destination address for each payment
- Scan for incoming funds
- Spend received funds
1. Generate Silent Payment Address
From Private Keys (Recommended for Apps)
Best for: Non-wallet applications where users control their keys
const 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
rasb_scan - Use
sasb_spend(or vice versa)
This ensures cryptographically secure randomness without storing additional keys.
From Mnemonic (For Wallets)
Best for: Wallet providers managing user funds
const mnemonic = ""; // 12, 15, or 24 word phrase
const keys = KeyGeneration.fromMnemonic(mnemonic);
const silentPaymentAddress = keys.toAddress();
console.log(silentPaymentAddress);Alternative using HD key:
const seed = bip39.mnemonicToSeedSync(mnemonic);
const node = bip32.fromSeed(seed);
const keys = KeyGeneration.fromHd(node);
const silentPaymentAddress = keys.toAddress();Security Note: If not using the signature-derived method, ensure you're using a cryptographically secure random number generator.
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());Why this matters: If you send 10 silent payments to friends, then send change to your public address, 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
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)
3. Scan for Incoming Funds
Trade-off: This is the main drawback of silent payments - you must scan the blockchain to detect incoming transactions.
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
txidandoutput_index - Public key from the output
- Script and amount from the taproot address
Learn more: BIP-352 Scanning
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);Tip: 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 receive Bitcoin privately without address reuse.
Documentation
For complete integration guides and examples, visit: docs.shakesco.com/silent-payments
Resources
- BIP-352 Specification - Complete technical specification
- bitcoinjs-lib - Build Bitcoin transactions in JavaScript
- Silent Payments Explained - Protocol deep dive
- ECDSA Signatures - Learn about signature-based key derivation
