Fluent transaction builder and wallet-compatible script templates for BSV — construct multi-output transactions (P2PKH, ordinals, custom) with method chaining, BRC-29 key derivation, and no private key exposure.
@bsv/wallet-helper is a good starting point for developers coming from other blockchain ecosystems who expect to build transactions explicitly. It gives you a transaction-builder shape for outputs, scripts, ordinals, metadata, inputs, and explicit change destinations, while still delegating keys and signing to a BRC-100 wallet.
Use it when @bsv/simple feels too task-oriented, but raw @bsv/sdk WalletClient.createAction / signAction calls are more protocol surface than you want to handle directly.
npm install @bsv/wallet-helperimport { TransactionBuilder } from '@bsv/wallet-helper'
const recipientAddress = '1EvmsbpAY7nESLkN4ajLTMbvsaQ1HpJPGX'
const result = await new TransactionBuilder(wallet, 'Payment with metadata')
.addP2PKHOutput({
address: recipientAddress,
satoshis: 5000,
description: 'Payment to Bob'
})
// Appends data to this output's locking script; it is not a separate output.
.addOpReturn(['APP_ID', JSON.stringify({ memo: 'Thanks!' })])
.build()
console.log(`Sent: ${result.txid}`)const aliceAddress = '1EvmsbpAY7nESLkN4ajLTMbvsaQ1HpJPGX'
const bobAddress = '1BoatSLRHtKNngkdXEeobR76b53LETtpyT'
await new TransactionBuilder(wallet, 'Multi-output payment')
.addP2PKHOutput({ address: aliceAddress, satoshis: 1000 })
.addP2PKHOutput({ address: bobAddress, satoshis: 2000 })
.build()The wallet still calculates funding and normal change through createAction / signAction. Use addChangeOutput only when you need to specify the change locking script yourself.
Omitting address, publicKey, and walletParams derives with counterparty self. Use this for outputs the same wallet should unlock later, not for sending to another user.
await new TransactionBuilder(wallet, 'Self-controlled output')
.addP2PKHOutput({ satoshis: 1000, description: 'Output for this wallet' })
.basket('my-basket')
.customInstructions('app-data')
.build()const recipientAddress = '1EvmsbpAY7nESLkN4ajLTMbvsaQ1HpJPGX'
await new TransactionBuilder(wallet, 'Spend UTXO')
.addP2PKHInput({ sourceTransaction, sourceOutputIndex: 0, description: 'UTXO' })
.addP2PKHOutput({ address: recipientAddress, satoshis: 500 })
.build()const ordResult = await new TransactionBuilder(wallet, 'Mint ordinal')
.addOrdinalP2PKHOutput({
walletParams: { protocolID: [2, 'p2pkh'], keyID: '0', counterparty: 'self' },
satoshis: 1,
inscription: {
dataB64: Buffer.from('Hello ordinals').toString('base64'),
contentType: 'text/plain'
},
metadata: { app: 'gallery', type: 'greeting', author: 'Alice' }
})
.build()address, publicKey, and walletParams only for outputs this wallet should controlcreateAction / signActionoptions({ randomizeOutputs: false })walletParams but try to unlock with publicKey, it failsselfaddOpReturn appends OP_RETURN data to the output returned by the previous add...Output calladdChangeOutput is for controlling where change goes; normal wallet-managed change is handled by createAction / signActionWalletClient