Building Applications with the BSV SDK: A Practical TypeScript Guide
A comprehensive guide for building any Bitcoin SV application using the TypeScript SDK
Published by: Traceport
Created: October 2025
Method: Direct examination of BSV SDK TypeScript declaration files using filesystem tools
Action Required: Upgrade to version 2.0.0 or later.
npm update @bsv/sdkCurrent version as of March 2026: v2.0.13
Reference: GitLab Advisory Database
Table of Contents
Understanding the Basics
What's the difference between Classes, Functions, and Methods?
Functions operate as standalone operations accepting input and returning output:
const hash = Hash.sha256(data)
const signature = ECDSA.sign(message, privateKey)
Classes function as blueprints for objects containing data and behavior:
const tx = new Transaction()
tx.addInput(...)
tx.addOutput(...)
Methods are functions belonging to classes. Static methods are called on the class itself, while instance methods are called on created objects:
const key = PrivateKey.fromRandom()
const publicKey = key.toPublicKey()
const signature = key.sign(message)
Key Concepts for BSV Development
Stateful versus Stateless operations differ fundamentally:
- Stateful (Classes): Objects remembering information between operations
- Stateless (Functions): Operations without memory
Example distinction:
Hash.sha256("hello") // Always returns same hash
const tx = new Transaction()
tx.addInput(input1) // tx now has 1 input
tx.addInput(input2) // tx now has 2 inputs
SDK Structure
The BSV SDK organizes into functional modules:
Module 1: Primitives (@bsv/sdk/primitives)
Core cryptographic building blocks including:
Key Classes:
PrivateKey- Secret keys for signingPublicKey- Public keys for verification and addressesBigNumber- Large numbers for crypto mathSignature- Digital signatures
Key Functions:
Hash.sha256(),Hash.ripemd160()- HashingECDSA.sign(),ECDSA.verify()- Signing/verification
Module 2: Script (@bsv/sdk/script)
Bitcoin's programming language for transactions:
Key Classes:
Script- Base script operationsP2PKH- Standard "pay to address" scriptsPushDrop- Token/attestation scripts
Module 3: Transaction (@bsv/sdk/transaction)
Building and managing complete transactions:
Key Classes:
Transaction- Complete transactions with inputs/outputsSatoshisPerKilobyte- Fee calculation
Module 4: Utilities
Helper functions for encoding, broadcasting, etc.
@bsv/sdk/wallet– BRC-100 compliant wallet interface@bsv/sdk/auth– P2P authentication mechanisms@bsv/sdk/messages– Signed message handling@bsv/sdk/overlay-tools– Overlay network utilities@bsv/sdk/storage– Distributed storage@bsv/sdk/kvstore– Key-value store
Essential Classes
1. PrivateKey - Your Secret Keys
import { PrivateKey } from '@bsv/sdk'
const randomKey = PrivateKey.fromRandom()
const keyFromHex = PrivateKey.fromHex('abc123...')
const keyFromWIF = PrivateKey.fromWif('L1234...')
const publicKey = randomKey.toPublicKey()
const address = randomKey.toAddress()
const signature = randomKey.sign('my message')
const childKey = randomKey.deriveChild(publicKey, 'unique-path')
Use when signing anything or generating addresses.
2. Transaction - Building Bitcoin Transactions
import { Transaction, P2PKH } from '@bsv/sdk'
const tx = new Transaction()
tx.addInput({
sourceTransaction: previousTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: new P2PKH().unlock(myPrivateKey)
})
tx.addOutput({
satoshis: 1000,
lockingScript: new P2PKH().lock(recipientAddress)
})
await tx.fee()
await tx.sign()
const txid = await tx.broadcast()
broadcast() method now requires an ARC broadcaster instance:
import { ARC } from '@bsv/sdk' const apiKey = 'your_arc_api_key' // from https://console.taal.com const txid = await tx.broadcast(new ARC('https://api.taal.com/arc', apiKey))
Use when sending Bitcoin or creating transactions.
import { ARC } from '@bsv/sdk'
// Default usage with API key
const arc = new ARC('https://api.taal.com/arc', apiKey)
// Alternative ARC providers
const gorillaPool = new ARC('https://arc.gorillapool.io', apiKey)
// Custom HTTP client (browser)
const arc = new ARC('https://api.taal.com/arc', apiKey, { fetch })
// Broadcast
const txid = await tx.broadcast(arc)
Get API keys from TAAL Console.
const version = 1
const input = {
sourceTransaction: Transaction.fromHex('...'),
sourceOutputIndex: 0,
unlockingScriptTemplate: new P2PKH().unlock(privKey),
}
const output = {
lockingScript: new P2PKH().lock(privKey.toAddress()),
change: true
}
const tx = new Transaction(version, [input], [output])
await tx.fee()
await tx.sign()
await tx.broadcast(new ARC('https://api.taal.com/arc', apiKey))
3. P2PKH - Standard Payment Scripts
import { P2PKH } from '@bsv/sdk'
const p2pkh = new P2PKH()
const lockingScript = p2pkh.lock(recipientAddress)
const unlockingTemplate = p2pkh.unlock(myPrivateKey)
Use for standard Bitcoin payments (most common).
4. PushDrop - Tokens and Data Storage
import { PushDrop } from '@bsv/sdk'
const pushDrop = new PushDrop(wallet)
const tokenScript = await pushDrop.lock(
[[1, 2, 3], [4, 5, 6]],
[0, 'myProtocol'],
'myKeyID',
'self',
true,
true
)
- Constructor: Now accepts optional
originatorparameter:new PushDrop(wallet, originator) - lock() method: New optional
lockPositionparameter ('before'|'after') controls where the lock script appears relative to data - New static method:
PushDrop.decode(script, lockPosition)reconstructs token fields and locking public key from a PushDrop script
const { lockingPublicKey, fields } = PushDrop.decode(script, 'before')
Use for tokens, data storage on-chain, or custom outputs.
Common Utility Functions
Hash Functions - Convert Data to Fixed-Size Fingerprints
import { Hash } from '@bsv/sdk'
const sha256Hash = Hash.sha256('my data')
const hash160 = Hash.hash160('my data')
const doubleHash = Hash.hash256('my data')
const hashFromHex = Hash.sha256('deadbeef', 'hex')
const hashFromText = Hash.sha256('hello world', 'utf8')
ECDSA Functions - Digital Signatures
import { ECDSA, Hash, BigNumber } from '@bsv/sdk'
const message = 'my message'
const hash = Hash.sha256(message)
const hashBN = new BigNumber(hash)
const signature = ECDSA.sign(hashBN, privateKey)
const isValid = ECDSA.verify(hashBN, signature, publicKey)
Encoding Functions - Convert Between Formats
import { Utils } from '@bsv/sdk'
const hexString = Utils.toHex([1, 2, 3, 4])
const byteArray = Utils.toArray('deadbeef', 'hex')
const base58 = Utils.toBase58([1, 2, 3])
const decoded = Utils.fromBase58('1A1zP1eP')
Common Use Cases
Use Case 1: Simple Payment Application
import { PrivateKey, Transaction, P2PKH, SatoshisPerKilobyte } from '@bsv/sdk'
class SimpleWallet {
private privateKey: PrivateKey
constructor(privateKeyWIF?: string) {
this.privateKey = privateKeyWIF
? PrivateKey.fromWif(privateKeyWIF)
: PrivateKey.fromRandom()
}
getAddress(): string {
return this.privateKey.toAddress()
}
async sendPayment(
recipientAddress: string,
amountSats: number,
utxos: Array<{tx: Transaction, outputIndex: number, satoshis: number}>
): Promise<string> {
const tx = new Transaction()
let totalInput = 0
for (const utxo of utxos) {
tx.addInput({
sourceTransaction: utxo.tx,
sourceOutputIndex: utxo.outputIndex,
unlockingScriptTemplate: new P2PKH().unlock(this.privateKey)
})
totalInput += utxo.satoshis
}
tx.addOutput({
satoshis: amountSats,
lockingScript: new P2PKH().lock(recipientAddress)
})
tx.addOutput({
satoshis: 0,
lockingScript: new P2PKH().lock(this.getAddress()),
change: true
})
await tx.fee(new SatoshisPerKilobyte(10))
await tx.sign()
return await tx.broadcast()
}
}
sendPayment method's broadcast() call needs an ARC instance:
import { ARC } from '@bsv/sdk'
// ... in sendPayment method:
return await tx.broadcast(new ARC('https://api.taal.com/arc', apiKey))
Use Case 2: Data Storage Application
import { PushDrop, PrivateKey, Transaction, Hash, ECDSA, BigNumber } from '@bsv/sdk'
class DataStorage {
private privateKey: PrivateKey
constructor(privateKey: PrivateKey) {
this.privateKey = privateKey
}
private createWalletInterface() {
return {
async getPublicKey() {
return {
publicKey: this.privateKey.toPublicKey().toString()
}
},
async createSignature(params: any) {
const dataArray = Array.isArray(params.data)
? params.data
: Array.from(params.data)
const hash = Hash.sha256(dataArray)
const hashBN = new BigNumber(hash)
const signature = ECDSA.sign(hashBN, this.privateKey)
return {
signature: signature.toDER()
}
}
}
}
async storeData(
data: string,
protocolName: string,
fundingTx: Transaction,
fundingOutputIndex: number
): Promise<string> {
const wallet = this.createWalletInterface()
const pushDrop = new PushDrop(wallet)
const tx = new Transaction()
tx.addInput({
sourceTransaction: fundingTx,
sourceOutputIndex: fundingOutputIndex,
unlockingScriptTemplate: new P2PKH().unlock(this.privateKey)
})
const dataBytes = Array.from(Buffer.from(data, 'utf8'))
const dataScript = await pushDrop.lock(
[dataBytes],
[0, protocolName],
Date.now().toString(),
'self',
true,
true
)
tx.addOutput({
satoshis: 1,
lockingScript: dataScript
})
await tx.fee()
await tx.sign()
return await tx.broadcast()
}
async retrieveData(txid: string, outputIndex: number): Promise<string> {
throw new Error('Implementation depends on your data source')
}
}
storeData method's broadcast() call needs an ARC instance.
Use Case 3: Token System
import { PushDrop, PrivateKey, Transaction } from '@bsv/sdk'
interface TokenMetadata {
name: string
symbol: string
totalSupply: number
decimals: number
}
class SimpleTokenSystem {
private issuerKey: PrivateKey
private protocolId: string
constructor(issuerKey: PrivateKey, protocolId: string) {
this.issuerKey = issuerKey
this.protocolId = protocolId
}
private createWalletInterface(privateKey: PrivateKey) {
return {
async getPublicKey() {
return { publicKey: privateKey.toPublicKey().toString() }
},
async createSignature(params: any) {
const hash = Hash.sha256(params.data)
const signature = ECDSA.sign(new BigNumber(hash), privateKey)
return { signature: signature.toDER() }
}
}
}
async issueTokens(
recipient: PrivateKey,
amount: number,
metadata: TokenMetadata,
fundingTx: Transaction,
fundingOutputIndex: number
): Promise<string> {
const wallet = this.createWalletInterface(this.issuerKey)
const pushDrop = new PushDrop(wallet)
const tx = new Transaction()
tx.addInput({
sourceTransaction: fundingTx,
sourceOutputIndex: fundingOutputIndex,
unlockingScriptTemplate: new P2PKH().unlock(this.issuerKey)
})
const tokenData = [
Array.from(Buffer.from(JSON.stringify(metadata))),
Array.from(Buffer.from(amount.toString())),
Array.from(Buffer.from(recipient.toAddress()))
]
const tokenScript = await pushDrop.lock(
tokenData,
[0, this.protocolId],
`token-${Date.now()}`,
'self',
true,
true
)
tx.addOutput({
satoshis: 1,
lockingScript: tokenScript
})
await tx.fee()
await tx.sign()
return await tx.broadcast()
}
async transferTokens(
from: PrivateKey,
to: PrivateKey,
amount: number,
tokenTx: Transaction,
tokenOutputIndex: number,
fundingTx: Transaction,
fundingOutputIndex: number
): Promise<string> {
throw new Error('Token transfer implementation required')
}
}
issueTokens method's broadcast() call needs an ARC instance.
Use Case 4: Digital Signature Verification Service
import { PublicKey, ECDSA, Hash, BigNumber, Signature } from '@bsv/sdk'
class SignatureVerificationService {
verifyMessage(
message: string,
signatureHex: string,
publicKeyHex: string
): { valid: boolean; error?: string } {
try {
const publicKey = PublicKey.fromString(publicKeyHex)
const signature = Signature.fromDER(signatureHex, 'hex')
const hash = Hash.sha256(message, 'utf8')
const hashBN = new BigNumber(hash)
const isValid = ECDSA.verify(hashBN, signature, publicKey)
return { valid: isValid }
} catch (error) {
return {
valid: false,
error: `Verification failed: ${error.message}`
}
}
}
verifyDocument(
documentBuffer: Buffer,
signatureHex: string,
publicKeyHex: string
): { valid: boolean; documentHash?: string; error?: string } {
try {
const documentHash = Hash.sha256(Array.from(documentBuffer))
const documentHashHex = Buffer.from(documentHash).toString('hex')
const result = this.verifyMessage(
documentHashHex,
signatureHex,
publicKeyHex
)
return {
...result,
documentHash: documentHashHex
}
} catch (error) {
return {
valid: false,
error: `Document verification failed: ${error.message}`
}
}
}
createDocumentSignature(
documentBuffer: Buffer,
privateKey: PrivateKey
): { signature: string; documentHash: string; publicKey: string } {
const documentHash = Hash.sha256(Array.from(documentBuffer))
const documentHashHex = Buffer.from(documentHash).toString('hex')
const hashBN = new BigNumber(documentHash)
const signature = ECDSA.sign(hashBN, privateKey)
return {
signature: signature.toDER('hex'),
documentHash: documentHashHex,
publicKey: privateKey.toPublicKey().toString()
}
}
}
Application Patterns
Pattern 1: Hierarchical Key Management
class HierarchicalWallet {
private masterKey: PrivateKey
private masterPub: PublicKey
private derivedKeys: Map<string, PrivateKey> = new Map()
constructor(masterSeed?: string) {
this.masterKey = masterSeed
? PrivateKey.fromHex(masterSeed)
: PrivateKey.fromRandom()
this.masterPub = this.masterKey.toPublicKey()
}
getDerivedKey(path: string): PrivateKey {
if (!this.derivedKeys.has(path)) {
const derivedKey = this.masterKey.deriveChild(this.masterPub, path)
this.derivedKeys.set(path, derivedKey)
}
return this.derivedKeys.get(path)!
}
getAddressForUser(userId: string): string {
const userKey = this.getDerivedKey(`user-${userId}`)
return userKey.toAddress()
}
getSigningKeyForPurpose(userId: string, purpose: string): PrivateKey {
return this.getDerivedKey(`user-${userId}-${purpose}`)
}
}
Pattern 2: Transaction Builder with Multiple Outputs
interface Output {
address: string
satoshis: number
data?: Buffer
}
class TransactionBuilder {
private transaction: Transaction
private totalInputSats: number = 0
private totalOutputSats: number = 0
constructor() {
this.transaction = new Transaction()
}
addInput(
previousTx: Transaction,
outputIndex: number,
satoshis: number,
privateKey: PrivateKey
): TransactionBuilder {
this.transaction.addInput({
sourceTransaction: previousTx,
sourceOutputIndex: outputIndex,
unlockingScriptTemplate: new P2PKH().unlock(privateKey)
})
this.totalInputSats += satoshis
return this
}
addPaymentOutput(address: string, satoshis: number): TransactionBuilder {
this.transaction.addOutput({
satoshis,
lockingScript: new P2PKH().lock(address)
})
this.totalOutputSats += satoshis
return this
}
addDataOutput(data: Buffer, protocolId: string, privateKey: PrivateKey): TransactionBuilder {
this.transaction.addOutput({
satoshis: 1,
lockingScript: new P2PKH().lock(privateKey.toAddress())
})
this.totalOutputSats += 1
return this
}
addChangeOutput(changeAddress: string): TransactionBuilder {
this.transaction.addOutput({
satoshis: 0,
lockingScript: new P2PKH().lock(changeAddress),
change: true
})
return this
}
async build(): Promise<Transaction> {
await this.transaction.fee()
await this.transaction.sign()
return this.transaction
}
getEstimatedFee(): number {
const inputs = this.transaction.inputs?.length || 0
const outputs = this.transaction.outputs?.length || 0
return (inputs * 200) + (outputs * 50)
}
}
Pattern 3: Event-Driven Transaction Processing
interface TransactionEvent {
type: 'sent' | 'received' | 'confirmed'
txid: string
amount?: number
address?: string
blockHeight?: number
}
class TransactionMonitor {
private eventHandlers: Map<string, Function[]> = new Map()
private watchedAddresses: Set<string> = new Set()
watchAddress(address: string): void {
this.watchedAddresses.add(address)
}
on(event: string, handler: Function): void {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, [])
}
this.eventHandlers.get(event)!.push(handler)
}
private emit(event: string, data: any): void {
const handlers = this.eventHandlers.get(event) || []
handlers.forEach(handler => handler(data))
}
async processTransaction(tx: Transaction): Promise<void> {
const txid = tx.id('hex')
tx.outputs?.forEach((output, index) => {
const address = this.extractAddressFromScript(output.lockingScript)
if (address && this.watchedAddresses.has(address)) {
this.emit('received', {
type: 'received',
txid,
amount: output.satoshis,
address
} as TransactionEvent)
}
})
tx.inputs?.forEach((input, index) => {
// Implementation would require looking up previous transaction
})
}
private extractAddressFromScript(lockingScript: any): string | null {
return null
}
}
Quick Reference
Essential Imports
import {
PrivateKey,
PublicKey,
BigNumber,
Hash,
ECDSA
} from '@bsv/sdk'
import {
Transaction,
P2PKH,
PushDrop,
SatoshisPerKilobyte
} from '@bsv/sdk'
import { Utils } from '@bsv/sdk'
ARC to your imports — it's required for transaction broadcasting:
import {
PrivateKey,
PublicKey,
BigNumber,
Hash,
ECDSA,
Transaction,
P2PKH,
PushDrop,
SatoshisPerKilobyte,
ARC // Required for broadcasting
} from '@bsv/sdk'
Common Operations Cheat Sheet
// Key Management
const key = PrivateKey.fromRandom()
const pubKey = key.toPublicKey()
const address = key.toAddress()
const childKey = key.deriveChild(pubKey, 'derivation-path')
// Hashing and Signing
const hash = Hash.sha256(data)
const hashBN = new BigNumber(hash)
const signature = ECDSA.sign(hashBN, privateKey)
const isValid = ECDSA.verify(hashBN, signature, publicKey)
// Basic Transaction
const tx = new Transaction()
tx.addInput({
sourceTransaction: prevTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: new P2PKH().unlock(privateKey)
})
tx.addOutput({
satoshis: 1000,
lockingScript: new P2PKH().lock(recipientAddress)
})
await tx.fee()
await tx.sign()
const txid = await tx.broadcast()
// PushDrop (Data/Tokens)
const wallet = createWalletInterface(privateKey)
const pushDrop = new PushDrop(wallet)
const dataScript = await pushDrop.lock(
[dataArray],
[0, 'protocol'],
'keyId',
'self',
true,
true
)
broadcast() calls in the cheat sheet above need an ARC instance:
const txid = await tx.broadcast(new ARC('https://api.taal.com/arc', apiKey))
Error Handling Best Practices
function safeKeyFromHex(hex: string): PrivateKey | null {
try {
return PrivateKey.fromHex(hex)
} catch (error) {
console.error('Invalid private key hex:', error)
return null
}
}
async function safeBroadcast(tx: Transaction): Promise<string> {
try {
const isValid = await tx.verify()
if (!isValid) {
throw new Error('Transaction failed validation')
}
return await tx.broadcast()
} catch (error) {
console.error('Broadcast failed:', error)
throw error
}
}
async function broadcastWithRetry(
tx: Transaction,
maxRetries: number = 3
): Promise<string> {
for (let i = 0; i < maxRetries; i++) {
try {
return await tx.broadcast()
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
throw new Error('Max retries exceeded')
}
safeBroadcast and broadcastWithRetry functions need to accept and use an ARC instance parameter.
Performance Optimization Tips
- Reuse SDK objects:
const p2pkh = new P2PKH() - Batch operations:
inputs.forEach(input => tx.addInput(input)) outputs.forEach(output => tx.addOutput(output)) await tx.sign() - Cache derived keys:
const keyCache = new Map<string, PrivateKey>() function getCachedKey(path: string): PrivateKey { if (!keyCache.has(path)) { keyCache.set(path, masterKey.deriveChild(masterPub, path)) } return keyCache.get(path)! } - Use appropriate fee models:
await tx.fee(new SatoshisPerKilobyte(1)) // Low priority await tx.fee(new SatoshisPerKilobyte(10)) // Normal priority
- Use appropriate HTTP clients: Configure ARC with the optimal HTTP client for your environment (native fetch for browsers, Node.js https module for servers)
- Consider wallet-toolbox: For production applications, use @bsv/wallet-toolbox for persistent storage and protocol compliance
Next Steps for Your Project
- Define Your Use Case: Payment app? Data storage? Token system? Custom application?
- Start with Basic Operations:
- Generate keys and addresses
- Create simple transactions
- Practice with testnet
- Build Core Classes:
- Wallet class for key management
- Transaction builder for specific needs
- Data models for application
- Add Application Logic:
- Business rules and validation
- User interface integration
- Error handling and recovery
- Test Thoroughly:
- Unit tests for core functions
- Integration tests with real transactions
- Edge case handling
WalletClient for BRC-100 compliant wallet operations:
import { WalletClient } from '@bsv/sdk/wallet'
// WalletClient provides methods for:
// - Key management
// - Transaction signing
// - Identity verification
// - P2P authentication
For building full wallet applications, see the BSV Wallet Toolbox.Interactive quickstart: fast.brc.dev
A guide: Start simple and build complexity gradually. The BSV SDK handles cryptographic complexity — focus on the unique value proposition of your application.