Skip to main content
SatSigner implements BIP329, the standard for exporting and importing wallet labels, enabling comprehensive organization of your Bitcoin transactions, addresses, and UTXOs.

BIP329 Overview

What is BIP329?

Bitcoin Improvement Proposal 329 defines a standardized format for wallet labels, enabling:
  • Portability - Move labels between wallets
  • Backup - Export labels separately from wallet
  • Privacy - Keep labels separate from blockchain
  • Interoperability - Share labels across wallet software
  • Organization - Maintain transaction context

Label Types

SatSigner supports all BIP329 label types:
type LabelType = 
  | 'tx'       // Transaction labels
  | 'addr'     // Address labels
  | 'pubkey'   // Public key labels
  | 'input'    // Input labels
  | 'output'   // Output labels (UTXOs)
  | 'xpub'     // Extended public key labels

type Label = {
  type: LabelType
  ref: string           // Reference (txid, address, etc.)
  label: string         // User-defined label text
  
  // Optional metadata
  spendable?: boolean   // Mark as spendable/frozen
  fee?: number         // Transaction fee
  fmv?: Prices         // Fair market value
  height?: number      // Block height
  keypath?: string     // Derivation path
  origin?: string      // Source description
  time?: Date          // Timestamp
  value?: number       // Amount in sats
}

Label Management

Creating Labels

Address Labels

// Label an address
setAddrLabel(
  accountId,
  "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
  "Customer Payment - Invoice #1234"
)

// Label is stored
const label = {
  type: 'addr',
  ref: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
  label: 'Customer Payment - Invoice #1234',
  spendable: true
}
Address Label Inheritance: When you label an address, the label automatically cascades:
// Label address
setAddrLabel(accountId, address, "Donation Address")

// Automatically inherits to:
// 1. All transactions to this address
// 2. All UTXOs at this address

// Unless they already have their own labels

Transaction Labels

// Label a transaction
setTxLabel(
  accountId,
  "1a2b3c4d5e6f7g8h9i0j...",
  "Equipment Purchase - Office Supplies"
)

// Transaction label inheritance
const label = {
  type: 'tx',
  ref: '1a2b3c4d5e6f7g8h9i0j...',
  label: 'Equipment Purchase - Office Supplies'
}

// Inherits to:
// - All outputs (if unlabeled)
// - All input previous outputs (if unlabeled)
// - Recipient addresses (if unlabeled)
Transaction Label Cascading:
// When you label a transaction:
setTxLabel(accountId, txid, "Payroll - March 2024")

// Unlabeled outputs inherit the label
for (const output of tx.vout) {
  if (!hasLabel(output.address)) {
    setAddrLabel(accountId, output.address, "Payroll - March 2024")
  }
  
  const utxoRef = `${txid}:${output.index}`
  if (!hasLabel(utxoRef)) {
    setUtxoLabel(accountId, txid, output.index, "Payroll - March 2024")
  }
}

// Unlabeled inputs inherit the label
for (const input of tx.vin) {
  const inputRef = `${input.previousOutput.txid}:${input.previousOutput.vout}`
  if (!hasLabel(inputRef)) {
    setUtxoLabel(
      accountId,
      input.previousOutput.txid,
      input.previousOutput.vout,
      "Payroll - March 2024"
    )
  }
}

UTXO Labels

// Label a specific UTXO (output)
setUtxoLabel(
  accountId,
  "1a2b3c4d5e6f7g8h9i0j...",  // txid
  0,                            // vout
  "Large Deposit - Client ABC"
)

// UTXO label format
const label = {
  type: 'output',
  ref: '1a2b3c4d5e6f7g8h9i0j...:0',  // txid:vout
  label: 'Large Deposit - Client ABC',
  spendable: true
}

// UTXO label inheritance
// Inherits to:
// - Parent transaction (if unlabeled)
// - Output address (if unlabeled)

Label Hierarchy

Label inheritance follows specificity:
Most Specific (highest priority)

UTXO Label (output:txid:vout)

Address Label (addr:address)

Transaction Label (tx:txid)

No Label
Example:
// Address labeled "Savings"
setAddrLabel(accountId, address, "Savings")

// Transaction to that address labeled "Bonus"
setTxLabel(accountId, txid, "Bonus")

// Specific UTXO labeled "Q1 2024 Bonus"
setUtxoLabel(accountId, txid, vout, "Q1 2024 Bonus")

// Display priority:
// UTXO shows: "Q1 2024 Bonus" (most specific)
// Transaction shows: "Bonus"
// Address shows: "Savings"

Label Export/Import

Export Formats

SatSigner supports three BIP329 export formats: JSON Lines format - one JSON object per line:
{"type":"tx","ref":"1a2b3c4d5e6f...","label":"Payment to Vendor A"}
{"type":"addr","ref":"bc1qar0srrr7xfkvy...","label":"Cold Storage"}
{"type":"output","ref":"1a2b3c4d:0","label":"Large Deposit","spendable":true}
Benefits:
  • Streamable
  • Easy to process line-by-line
  • Human readable
  • Append-friendly

JSON

Standard JSON array:
[
  {
    "type": "tx",
    "ref": "1a2b3c4d5e6f7g8h9i0j...",
    "label": "Payment to Vendor A"
  },
  {
    "type": "addr",
    "ref": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
    "label": "Cold Storage"
  },
  {
    "type": "output",
    "ref": "1a2b3c4d5e6f7g8h9i0j...:0",
    "label": "Large Deposit",
    "spendable": true
  }
]
Benefits:
  • Standard format
  • Easy to parse
  • Tool-friendly

CSV

Comma-separated values:
type,ref,spendable,label
tx,1a2b3c4d5e6f7g8h9i0j...,true,"Payment to Vendor A"
addr,bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq,true,"Cold Storage"
output,1a2b3c4d5e6f7g8h9i0j...:0,true,"Large Deposit"
Benefits:
  • Spreadsheet compatible
  • Excel/Sheets friendly
  • Simple format

Exporting Labels

// Export all account labels
function exportAccountLabels(
  account: Account,
  format: 'JSONL' | 'JSON' | 'CSV'
): string {
  // Collect all labels
  const labels = formatAccountLabels(account)
  
  // Export in selected format
  switch (format) {
    case 'JSONL':
      return labelsToJSONL(labels)
    case 'JSON':
      return labelsToJSON(labels)
    case 'CSV':
      return labelsToCSV(labels)
  }
}

// Usage
const jsonlExport = exportAccountLabels(account, 'JSONL')
saveToFile('labels.jsonl', jsonlExport)

const csvExport = exportAccountLabels(account, 'CSV')
saveToFile('labels.csv', csvExport)

Importing Labels

// Import labels from file
function importLabelsFromFile(
  accountId: string,
  fileContent: string,
  format: 'JSONL' | 'JSON' | 'CSV'
): number {
  // Parse based on format
  let labels: Label[]
  
  switch (format) {
    case 'JSONL':
      labels = JSONLtoLabels(fileContent)
      break
    case 'JSON':
      labels = JSONtoLabels(fileContent)
      break
    case 'CSV':
      labels = CSVtoLabels(fileContent)
      break
  }
  
  // Import to account
  const importedCount = importLabels(accountId, labels)
  
  return importedCount
}

// Usage
const fileContent = readFile('labels.jsonl')
const count = importLabelsFromFile(accountId, fileContent, 'JSONL')

console.log(`Imported ${count} labels`)

Cross-Wallet Compatibility

SatSigner’s BIP329 implementation is compatible with: Supported Wallets:
  • Sparrow Wallet
  • Bitcoin Core (with patch)
  • BlueWallet
  • Electrum (custom format)
  • Specter Desktop
  • Nunchuk
Import Notes:
// Handle wallet-specific formats
const bip329Aliases = {
  ref: ['ref', 'txid', 'address', 'Payment Address'],
  label: ['label', 'memo'],
  fee: ['fee', 'Fee sat/vbyte'],
  height: ['height', 'Block height', 'Blockheight'],
  time: ['date', 'Date (UTC)', 'time', 'timestamp'],
  value: ['value', 'sats', 'satoshis', 'amount']
}

// Auto-detect and map fields
function normalizeLabel(rawLabel: any): Label {
  const normalized = {}
  
  for (const [key, value] of Object.entries(rawLabel)) {
    const standardKey = bip329Alias[key.toLowerCase()]
    if (standardKey) {
      normalized[standardKey] = value
    }
  }
  
  return normalized as Label
}

Label Organization

Using Tags

Organize labels with tags:
// Tag system
const tags = [
  'income',
  'expense',
  'savings',
  'investment',
  'donation',
  'tax-deductible',
  'business',
  'personal'
]

// Tag in label text
setTxLabel(
  accountId,
  txid,
  "#income #business Client Payment - Invoice #1234"
)

// Extract tags
function extractTags(label: string): string[] {
  const tagRegex = /#(\w+)/g
  const tags = []
  let match
  
  while ((match = tagRegex.exec(label)) !== null) {
    tags.push(match[1])
  }
  
  return tags
}

// Filter by tag
function getTransactionsByTag(account: Account, tag: string) {
  return account.transactions.filter(tx => {
    const tags = extractTags(tx.label || '')
    return tags.includes(tag)
  })
}

Label Templates

Create consistent labels:
// Label templates
const templates = {
  invoice: (invoiceNumber: string, client: string) =>
    `Invoice #${invoiceNumber} - ${client}`,
  
  payroll: (month: string, year: number, recipient: string) =>
    `Payroll - ${month} ${year} - ${recipient}`,
  
  purchase: (category: string, vendor: string) =>
    `Purchase - ${category} - ${vendor}`,
  
  donation: (organization: string, taxDeductible: boolean) =>
    `Donation - ${organization}${taxDeductible ? ' (Tax Deductible)' : ''}`,
  
  savings: (goal: string, date: Date) =>
    `Savings - ${goal} - ${date.toISOString().split('T')[0]}`
}

// Usage
setTxLabel(
  accountId,
  txid,
  templates.invoice('1234', 'Acme Corp')
)
// Result: "Invoice #1234 - Acme Corp"

setAddrLabel(
  accountId,
  address,
  templates.donation('Red Cross', true)
)
// Result: "Donation - Red Cross (Tax Deductible)"
// Search labels
function searchLabels(
  account: Account,
  query: string
): {
  transactions: Transaction[]
  addresses: Address[]
  utxos: Utxo[]
} {
  const lowerQuery = query.toLowerCase()
  
  return {
    transactions: account.transactions.filter(tx =>
      tx.label?.toLowerCase().includes(lowerQuery)
    ),
    addresses: account.addresses.filter(addr =>
      addr.label?.toLowerCase().includes(lowerQuery)
    ),
    utxos: account.utxos.filter(utxo =>
      utxo.label?.toLowerCase().includes(lowerQuery)
    )
  }
}

// Usage
const results = searchLabels(account, 'invoice')
console.log(`Found ${results.transactions.length} transactions`)

Advanced Features

Spendable Flag

Mark UTXOs as frozen (unspendable):
// Freeze UTXO
const frozenLabel: Label = {
  type: 'output',
  ref: `${txid}:${vout}`,
  label: 'Emergency Fund - Do Not Spend',
  spendable: false  // Marks as frozen
}

// Filter spendable UTXOs
function getSpendableUtxos(account: Account): Utxo[] {
  return account.utxos.filter(utxo => {
    const outpoint = getUtxoOutpoint(utxo)
    const label = account.labels[outpoint]
    return label?.spendable !== false
  })
}

// Use in transaction building
const spendableUtxos = getSpendableUtxos(account)
const { inputs } = selectEfficientUtxos(
  spendableUtxos,
  targetAmount,
  feeRate
)

Metadata Fields

// Rich label with metadata
const detailedLabel: Label = {
  type: 'tx',
  ref: txid,
  label: 'Equipment Purchase',
  
  // Financial metadata
  value: 500000,              // 0.005 BTC
  fee: 2000,                  // 2000 sats
  fmv: { USD: 250.00 },      // Fair market value
  
  // Blockchain metadata
  height: 750000,             // Block height
  time: new Date('2024-01-15'),
  
  // Key metadata
  keypath: "m/84'/0'/0'/0/5",
  origin: 'Hardware Wallet',
  
  // Status
  spendable: true
}

Label Analytics

// Analyze labels
function analyzeLabelUsage(account: Account) {
  const stats = {
    totalLabels: 0,
    byType: {
      tx: 0,
      addr: 0,
      output: 0
    },
    avgLabelLength: 0,
    mostCommonTags: [],
    unlabeled: {
      transactions: 0,
      addresses: 0,
      utxos: 0
    }
  }
  
  // Count by type
  for (const label of Object.values(account.labels)) {
    stats.totalLabels++
    stats.byType[label.type]++
  }
  
  // Count unlabeled
  stats.unlabeled.transactions = account.transactions
    .filter(tx => !tx.label).length
  stats.unlabeled.addresses = account.addresses
    .filter(addr => !addr.label).length
  stats.unlabeled.utxos = account.utxos
    .filter(utxo => !utxo.label).length
  
  // Average label length
  const labelTexts = Object.values(account.labels)
    .map(l => l.label)
  stats.avgLabelLength = labelTexts
    .reduce((sum, text) => sum + text.length, 0) / labelTexts.length
  
  return stats
}

Best Practices

Labeling Strategy

  1. Label Immediately - Label transactions as they occur
  2. Be Consistent - Use consistent naming conventions
  3. Be Descriptive - Include context and purpose
  4. Use Tags - Organize with hashtags
  5. Review Regularly - Update labels as needed
  6. Backup Labels - Export regularly
  7. Document System - Maintain labeling guidelines

Label Format Guidelines

Good Labels:
"Invoice #1234 - Acme Corp - Web Design"
"Payroll - March 2024 - John Doe"
"Equipment Purchase - Laptop - $2500"
"#income #business Client Payment"
"Cold Storage - Long Term Hold"
Poor Labels:
"tx"                  // Too vague
"payment"             // No context
"stuff"              // Meaningless
"asdf"               // Random characters
"123"                // Just numbers

Privacy Considerations

Remember:
  • Labels are stored locally only
  • Never included on blockchain
  • Can contain sensitive information
  • Export files should be encrypted
  • Backup securely
Sensitive Label Handling:
// Sanitize labels for export
function sanitizeLabelForExport(label: string): string {
  // Remove personal identifiers
  return label
    .replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, '[NAME]')  // Names
    .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]')        // SSN
    .replace(/\b[\w.-]+@[\w.-]+\.\w+\b/g, '[EMAIL]')   // Email
    .replace(/\b\d{4}-\d{4}-\d{4}-\d{4}\b/g, '[CARD]') // Card
}

// Export with sanitization
function exportSanitizedLabels(account: Account) {
  const labels = Object.values(account.labels).map(label => ({
    ...label,
    label: sanitizeLabelForExport(label.label)
  }))
  
  return labelsToJSONL(labels)
}

Tax and Accounting

Tax Lot Tracking

// Label with tax information
const taxLabel: Label = {
  type: 'output',
  ref: `${txid}:${vout}`,
  label: 'Purchase - 2024-01-15 - $45,000',
  value: 100000000,  // 1 BTC
  fmv: { USD: 45000 },
  time: new Date('2024-01-15'),
  origin: 'Exchange Purchase'
}

// Calculate capital gains
function calculateGains(
  acquisition: Label,
  disposal: Transaction
): {
  costBasis: number
  proceeds: number
  gain: number
  holdingPeriod: number
} {
  const costBasis = acquisition.fmv?.USD || 0
  const proceeds = disposal.fmv?.USD || 0
  const gain = proceeds - costBasis
  
  const holdingPeriod = disposal.timestamp.getTime() -
    acquisition.time.getTime()
  
  return {
    costBasis,
    proceeds,
    gain,
    holdingPeriod
  }
}

Report Generation

// Generate tax report
function generateTaxReport(
  account: Account,
  year: number
): {
  shortTerm: Transaction[]
  longTerm: Transaction[]
  income: Transaction[]
  deductions: Transaction[]
} {
  const yearStart = new Date(year, 0, 1)
  const yearEnd = new Date(year, 11, 31)
  
  const yearTxs = account.transactions.filter(
    tx => tx.timestamp >= yearStart && tx.timestamp <= yearEnd
  )
  
  return {
    shortTerm: yearTxs.filter(tx => 
      tx.label?.includes('#short-term')
    ),
    longTerm: yearTxs.filter(tx => 
      tx.label?.includes('#long-term')
    ),
    income: yearTxs.filter(tx => 
      tx.label?.includes('#income')
    ),
    deductions: yearTxs.filter(tx => 
      tx.label?.includes('#tax-deductible')
    )
  }
}

Troubleshooting

Labels Not Importing

Problem: Imported labels not applying Solutions:
  1. Verify format matches (JSONL vs JSON vs CSV)
  2. Check for syntax errors in file
  3. Ensure references match account data
  4. Try alternative format
  5. Check for encoding issues (UTF-8)

Labels Not Cascading

Problem: Child items not inheriting labels Cause: Child item already has label Remember: Label inheritance only applies to unlabeled items

Missing Labels After Sync

Problem: Labels disappeared after sync Cause: Labels stored separately from blockchain data Solution:
  • Labels persist through syncs
  • Check if using correct account
  • Restore from backup if needed

Integration with Other Wallets

Export to Sparrow

// Export for Sparrow Wallet
const sparrowLabels = exportAccountLabels(account, 'JSONL')
saveToFile('sparrow-labels.jsonl', sparrowLabels)

// Import in Sparrow:
// File > Import > Labels (BIP329)

Export to Bitcoin Core

// Bitcoin Core uses 'setlabel' RPC
function exportForBitcoinCore(account: Account): string[] {
  const commands = []
  
  for (const label of Object.values(account.labels)) {
    if (label.type === 'addr') {
      commands.push(
        `bitcoin-cli setlabel "${label.ref}" "${label.label}"`
      )
    }
  }
  
  return commands
}

Import from Electrum

// Electrum uses custom format
function importFromElectrum(electrumLabels: any): Label[] {
  const labels: Label[] = []
  
  for (const [ref, label] of Object.entries(electrumLabels)) {
    // Detect type from reference format
    const type = ref.includes(':') ? 'output' :
                 ref.length === 64 ? 'tx' : 'addr'
    
    labels.push({
      type,
      ref,
      label: label as string
    })
  }
  
  return labels
}
SatSigner’s BIP329 implementation provides professional-grade label management with full portability across compatible Bitcoin wallets, enabling organized and documented transaction history.

Build docs developers (and LLMs) love