Overview
This guide shows you how to sign Bitcoin transactions using Dynamic’s Node SDK. Bitcoin uses PSBTs (Partially Signed Bitcoin Transactions) for signing. You create a PSBT, pass it to signTransaction() as a base64 string, and receive the signed PSBT back.
Prerequisites
Step 1: Install Dependencies
Step 2: Create and Sign a PSBT
The approach for signing transactions depends on how you created your wallet:
With Automatic Backup (Recommended)
If you created your wallet with backUpToDynamic: true, you can sign directly without retrieving key shares:
import { DynamicBtcWalletClient } from '@dynamic-labs-wallet/node-btc';
import { BitcoinNetwork } from '@dynamic-labs-wallet/core';
import * as bitcoin from 'bitcoinjs-lib';
export const authenticatedBtcClient = async () => {
const client = new DynamicBtcWalletClient({
environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);
return client;
};
const btcClient = await authenticatedBtcClient();
// Create a PSBT (example: spending from a Native SegWit address)
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
// Add input (UTXO to spend)
psbt.addInput({
hash: 'previous_txid_here',
index: 0,
witnessUtxo: {
script: Buffer.from('0014...', 'hex'), // Your address script
value: 50000, // Amount in satoshis
},
});
// Add output (recipient)
psbt.addOutput({
address: 'bc1q...recipient-address',
value: 40000, // Amount in satoshis (minus fee)
});
// Convert PSBT to base64 for signing
const psbtBase64 = psbt.toBase64();
// Sign the PSBT
const signedPsbtBase64 = await btcClient.signTransaction({
transaction: psbtBase64,
walletMetadata,
network: BitcoinNetwork.MAINNET,
});
console.log('Signed PSBT (base64):', signedPsbtBase64);
With Manual Backup
If you created your wallet with backUpToDynamic: false, you must retrieve and provide external key shares:
// First, get external server key shares (required for manual backup)
const keyShares = await btcClient.getExternalServerKeyShares({
walletMetadata,
});
const signedPsbtBase64 = await btcClient.signTransaction({
transaction: psbtBase64,
walletMetadata,
network: BitcoinNetwork.MAINNET,
externalServerKeyShares: keyShares, // Required for manual backup!
password: 'your-password', // Only if wallet was created with password
});
Password Handling Notes:
- If your wallet was created without a password, omit the
password parameter
- If your wallet was created with a password, you must provide it for all operations
- The password parameter is always optional in the API, but required if the wallet is password-protected
Step 3: Finalize and Broadcast
After signing, finalize the PSBT and extract the raw transaction for broadcasting:
// Parse the signed PSBT
const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64);
// Finalize all inputs
signedPsbt.finalizeAllInputs();
// Extract the raw transaction hex
const rawTx = signedPsbt.extractTransaction().toHex();
console.log('Raw transaction hex:', rawTx);
// Broadcast using your preferred method (e.g., blockstream.info API)
const response = await fetch('https://blockstream.info/api/tx', {
method: 'POST',
body: rawTx,
});
const txid = await response.text();
console.log('Transaction broadcasted:', txid);
Complete Example: Send Bitcoin
Here’s a complete example of creating, signing, and broadcasting a Bitcoin transaction:
import { DynamicBtcWalletClient } from '@dynamic-labs-wallet/node-btc';
import { BitcoinNetwork } from '@dynamic-labs-wallet/core';
import * as bitcoin from 'bitcoinjs-lib';
async function sendBitcoin({
walletMetadata,
recipientAddress,
amountSats,
feeSats,
utxo,
}: {
walletMetadata: WalletMetadata;
recipientAddress: string;
amountSats: number;
feeSats: number;
utxo: {
txid: string;
vout: number;
value: number;
script: Buffer;
};
}) {
const btcClient = await authenticatedBtcClient();
const senderAddress = walletMetadata.accountAddress;
const externalServerKeyShares = await vault.read(`wallet:${senderAddress}/shares`);
// Create PSBT
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
// Add the UTXO input
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: utxo.script,
value: utxo.value,
},
});
// Add recipient output
psbt.addOutput({
address: recipientAddress,
value: amountSats,
});
// Add change output (if there's change)
const change = utxo.value - amountSats - feeSats;
if (change > 546) { // Dust threshold
psbt.addOutput({
address: senderAddress,
value: change,
});
}
// Sign the PSBT
const signedPsbtBase64 = await btcClient.signTransaction({
transaction: psbt.toBase64(),
walletMetadata,
externalServerKeyShares,
network: BitcoinNetwork.MAINNET,
});
// Finalize and extract
const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64);
signedPsbt.finalizeAllInputs();
const rawTx = signedPsbt.extractTransaction().toHex();
// Broadcast
const response = await fetch('https://blockstream.info/api/tx', {
method: 'POST',
body: rawTx,
});
return await response.text(); // Returns txid
}
Taproot Transaction Signing
For Taproot addresses, the signing process is the same, but uses Schnorr signatures internally:
// Taproot PSBT signing
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
psbt.addInput({
hash: 'previous_txid_here',
index: 0,
witnessUtxo: {
script: Buffer.from('5120...', 'hex'), // Taproot output script (OP_1 <pubkey>)
value: 50000,
},
tapInternalKey: Buffer.from('...', 'hex'), // 32-byte x-only public key
});
psbt.addOutput({
address: 'bc1p...recipient-taproot-address',
value: 40000,
});
// Sign - the SDK handles Schnorr signatures automatically
const signedPsbtBase64 = await btcClient.signTransaction({
transaction: psbt.toBase64(),
walletMetadata,
network: BitcoinNetwork.MAINNET,
});
The SDK only signs inputs that belong to the sender address. Other inputs remain unsigned:
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
// Input from your wallet - will be signed
psbt.addInput({
hash: 'txid1',
index: 0,
witnessUtxo: { script: yourScript, value: 50000 },
});
// Input from another party - will NOT be signed
psbt.addInput({
hash: 'txid2',
index: 0,
witnessUtxo: { script: otherScript, value: 30000 },
});
// Sign - only your input gets signed
const signedPsbtBase64 = await btcClient.signTransaction({
transaction: psbt.toBase64(),
walletMetadata,
network: BitcoinNetwork.MAINNET,
});
Error Handling
try {
const signedPsbt = await btcClient.signTransaction({
transaction: psbt.toBase64(),
walletMetadata,
network: BitcoinNetwork.MAINNET,
});
console.log('Transaction signed successfully');
} catch (error) {
if (error.message.includes('No inputs found')) {
console.error('None of the inputs belong to the sender address');
} else {
console.error('Transaction signing failed:', error.message);
}
}
Next Steps
Last modified on May 21, 2026