BSV SDK General Guide for Application Developers | Traceport

Building Applications with the BSV SDK: A Practical TypeScript Guide

A comprehensive guide for building any Bitcoin SV application using the TypeScript SDK

Article Updated: March 2026 This guide has been updated to reflect changes in BSV SDK v2.0.x, including a critical security update and API changes. Update notes appear throughout the article.
Security Notice (March 2026) CVE-2025-69287 (CVSS 5.4): A cryptographic vulnerability in the SDK's BRC-104 authentication implementation affects all versions prior to 2.0.0. The issue involved incorrect signature data preparation, resulting in potential authentication bypass scenarios.

Action Required: Upgrade to version 2.0.0 or later.
npm update @bsv/sdk
Current version as of March 2026: v2.0.13
Reference: GitLab Advisory Database

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 signing
  • PublicKey - Public keys for verification and addresses
  • BigNumber - Large numbers for crypto math
  • Signature - Digital signatures

Key Functions:

  • Hash.sha256(), Hash.ripemd160() - Hashing
  • ECDSA.sign(), ECDSA.verify() - Signing/verification

Module 2: Script (@bsv/sdk/script)

Bitcoin's programming language for transactions:

Key Classes:

  • Script - Base script operations
  • P2PKH - Standard "pay to address" scripts
  • PushDrop - Token/attestation scripts

Module 3: Transaction (@bsv/sdk/transaction)

Building and managing complete transactions:

Key Classes:

  • Transaction - Complete transactions with inputs/outputs
  • SatoshisPerKilobyte - Fee calculation

Module 4: Utilities

Helper functions for encoding, broadcasting, etc.

New Sub-Paths in v2.0 (March 2026) The SDK now includes additional module sub-paths:
  • @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()
Breaking Change: broadcast() Method (March 2026) The broadcast() method now requires an ARC broadcaster instance:
const txid = await tx.broadcast()

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.

New: Transaction Broadcasting with ARC (March 2026) ARC (Authorized Relay Client) is required for submitting transactions to the BSV network. The SDK supports multiple ARC providers and custom HTTP clients:
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.
Alternative Pattern: Transaction Constructor (v2.0+) Transactions can also be constructed with inputs and outputs passed directly to the constructor:
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
)
PushDrop Updates (v2.0+) The PushDrop class has new parameters:
  • Constructor: Now accepts optional originator parameter: new PushDrop(wallet, originator)
  • lock() method: New optional lockPosition parameter ('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()
  }
}
Update Required The 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')
  }
}
Update Required The 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')
  }
}
Update Required The 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'
Updated Import (v2.0+) Add 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
)
Reminder All 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')
}
Update Required The safeBroadcast and broadcastWithRetry functions need to accept and use an ARC instance parameter.

Performance Optimization Tips

  1. Reuse SDK objects:
    const p2pkh = new P2PKH()
  2. Batch operations:
    inputs.forEach(input => tx.addInput(input))
    outputs.forEach(output => tx.addOutput(output))
    await tx.sign()
  3. 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)!
    }
  4. Use appropriate fee models:
    await tx.fee(new SatoshisPerKilobyte(1))  // Low priority
    await tx.fee(new SatoshisPerKilobyte(10)) // Normal priority
Additional Performance Tips (v2.0+)
  • 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

  1. Define Your Use Case: Payment app? Data storage? Token system? Custom application?
  2. Start with Basic Operations:
    • Generate keys and addresses
    • Create simple transactions
    • Practice with testnet
  3. Build Core Classes:
    • Wallet class for key management
    • Transaction builder for specific needs
    • Data models for application
  4. Add Application Logic:
    • Business rules and validation
    • User interface integration
    • Error handling and recovery
  5. Test Thoroughly:
    • Unit tests for core functions
    • Integration tests with real transactions
    • Edge case handling
New: WalletClient (BRC-100 Compliance) The SDK now includes 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.