Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/satsigner/satsigner/llms.txt

Use this file to discover all available pages before exploring further.

SatSigner’s watch-only mode enables monitoring of Bitcoin addresses and transaction history without exposing private keys, perfect for cold storage monitoring, portfolio tracking, and treasury oversight.

What is Watch-Only?

Watch-only accounts can: View balance - See total funds ✓ Monitor transactions - Track incoming/outgoing ✓ Generate addresses - Create new receive addresses ✓ View transaction history - See all past activity ✓ Label transactions - Organize and categorize ✓ Export reports - Generate statements Cannot sign - No spending ability ✗ Cannot access keys - No private key access

Import Methods

SatSigner supports three watch-only import methods:

1. Output Descriptor

Most comprehensive and recommended method:
type DescriptorImport = {
  creationType: 'importDescriptor'
  externalDescriptor: string  // Receive addresses (0/*)
  internalDescriptor?: string // Change addresses (1/*)
}

// Example P2WPKH descriptor
const descriptor = {
  externalDescriptor: "wpkh([a1b2c3d4/84h/0h/0h]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz/0/*)#checksum",
  internalDescriptor: "wpkh([a1b2c3d4/84h/0h/0h]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz/1/*)#checksum"
}
Descriptor Components:
  • wpkh() - Script type (wpkh, sh, wsh, tr)
  • [fingerprint/path] - Key derivation info
  • xpub... - Extended public key
  • /0/* or /1/* - Address index wildcard
  • #checksum - Optional descriptor checksum
Supported Script Types:
// Legacy
"pkh([fp/44h/0h/0h]xpub.../0/*)"  // P2PKH

// Nested SegWit
"sh(wpkh([fp/49h/0h/0h]ypub.../0/*))"  // P2SH-P2WPKH

// Native SegWit
"wpkh([fp/84h/0h/0h]zpub.../0/*)"  // P2WPKH

// Taproot
"tr([fp/86h/0h/0h]xpub.../0/*)"  // P2TR

// Multisig
"wsh(sortedmulti(2,[fp1/48h/0h/0h/2h]xpub.../0/*,[fp2/48h/0h/0h/2h]xpub.../0/*))"  // 2-of-2 P2WSH

2. Extended Public Key (xpub)

Simplified method for standard wallets:
type XpubImport = {
  creationType: 'importExtendedPub'
  extendedPublicKey: string
  fingerprint: string
  scriptVersion: ScriptVersionType
}

// Example imports
const imports = {
  // Legacy xpub (P2PKH)
  legacy: {
    extendedPublicKey: "xpub6CUGRUonZSQ4TWtTMmzXdrXDty...",
    fingerprint: "a1b2c3d4",
    scriptVersion: "P2PKH"
  },
  
  // ypub for nested SegWit (P2SH-P2WPKH)
  nestedSegwit: {
    extendedPublicKey: "ypub6XFn7hfb6MQX8HhFHpY...",
    fingerprint: "a1b2c3d4",
    scriptVersion: "P2SH-P2WPKH"
  },
  
  // zpub for native SegWit (P2WPKH)
  nativeSegwit: {
    extendedPublicKey: "zpub6rFR7y4Q2AijBEqTUquhVz298h...",
    fingerprint: "a1b2c3d4",
    scriptVersion: "P2WPKH"
  },
  
  // Standard xpub for Taproot (P2TR)
  taproot: {
    extendedPublicKey: "xpub6BgBgsespWvERZLy3...",
    fingerprint: "a1b2c3d4",
    scriptVersion: "P2TR"
  }
}
Extended Key Prefixes:
PrefixNetworkScript TypeDerivation
xpubMainnetP2PKH/P2TRBIP32
ypubMainnetP2SH-P2WPKHBIP49
zpubMainnetP2WPKHBIP84
tpubTestnetAnyBIP32
upubTestnetP2SH-P2WPKHBIP49
vpubTestnetP2WPKHBIP84

3. Single Address

Simplest method for monitoring specific addresses:
type AddressImport = {
  creationType: 'importAddress'
  address: string
}

// Example address imports
const addresses = [
  // Legacy
  "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
  
  // Nested SegWit
  "3J98t1WpEZ73CNmYviecrnyiWrnqRhWNLy",
  
  // Native SegWit
  "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
  
  // Taproot
  "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"
]
Limitations:
  • No address derivation (only single address)
  • Cannot generate new receive addresses
  • No change address support
  • Manual tracking required

Setting Up Watch-Only Accounts

From Descriptor

// Create watch-only account from output descriptor
const watchOnlyAccount = {
  name: "Cold Storage Monitor",
  network: "bitcoin",
  policyType: "watchonly",
  keys: [{
    index: 0,
    creationType: "importDescriptor",
    secret: {
      externalDescriptor: "wpkh([a1b2c3d4/84h/0h/0h]xpub.../0/*)",
      internalDescriptor: "wpkh([a1b2c3d4/84h/0h/0h]xpub.../1/*)"
    }
  }],
  keyCount: 1,
  keysRequired: 1
}

From Extended Public Key

// Create watch-only account from xpub
const xpubAccount = {
  name: "Hardware Wallet Monitor",
  network: "bitcoin",
  policyType: "watchonly",
  keys: [{
    index: 0,
    creationType: "importExtendedPub",
    secret: {
      extendedPublicKey: "zpub6rFR7y4Q2AijBEqTUquhVz...",
      fingerprint: "a1b2c3d4"
    },
    scriptVersion: "P2WPKH"
  }],
  keyCount: 1,
  keysRequired: 1
}

From Single Address

// Create watch-only account for single address
const addressAccount = {
  name: "Donation Address Monitor",
  network: "bitcoin",
  policyType: "watchonly",
  keys: [{
    index: 0,
    creationType: "importAddress",
    secret: {
      externalDescriptor: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
    }
  }],
  keyCount: 1,
  keysRequired: 1
}

Extracting Watch-Only Information

From Hardware Wallet

Most hardware wallets can export xpubs: Ledger:
  1. Open Bitcoin app
  2. Navigate to “Settings” > “Public Key”
  3. Select account and export
  4. Copy extended public key
Trezor:
  1. Connect device
  2. Open Trezor Suite
  3. Navigate to account details
  4. Click “Show public key”
  5. Copy xpub/zpub
Coldcard:
  1. Advanced > MicroSD Card > Export Wallet
  2. Select “Generic JSON” or “Descriptor”
  3. Save to SD card
  4. Import file to SatSigner

From Software Wallet

Exporting descriptors from compatible wallets: Bitcoin Core:
# Get wallet descriptor
bitcoin-cli getdescriptorinfo "wpkh([fingerprint/84h/0h/0h]xpub.../0/*)"

# List descriptors
bitcoin-cli listdescriptors
Sparrow Wallet:
  1. File > Export Wallet
  2. Select “Output Descriptors”
  3. Copy descriptor text
  4. Import to SatSigner
BlueWallet:
  1. Wallet > Settings
  2. “Export/Backup”
  3. “View in wallet export format”
  4. Copy descriptor

Creating Descriptor from Xpub

Manually construct descriptor if needed:
function createDescriptor(
  xpub: string,
  fingerprint: string,
  scriptType: string,
  account: number = 0
): string {
  const paths = {
    'P2PKH': `44h/0h/${account}h`,
    'P2SH-P2WPKH': `49h/0h/${account}h`,
    'P2WPKH': `84h/0h/${account}h`,
    'P2TR': `86h/0h/${account}h`
  }
  
  const scripts = {
    'P2PKH': 'pkh',
    'P2SH-P2WPKH': 'sh(wpkh',
    'P2WPKH': 'wpkh',
    'P2TR': 'tr'
  }
  
  const path = paths[scriptType]
  const script = scripts[scriptType]
  
  const descriptor = `${script}([${fingerprint}/${path}]${xpub}/0/*)`
  
  return descriptor + (scriptType === 'P2SH-P2WPKH' ? ')' : '')
}

// Usage
const descriptor = createDescriptor(
  "zpub6rFR7y4Q2AijBEqTUquhVz...",
  "a1b2c3d4",
  "P2WPKH",
  0
)

Watch-Only Features

Address Generation

// Generate new receive address
const newAddress = await wallet.getAddress(AddressIndex.New)
const addressString = await newAddress.address.asString()

console.log('New receive address:', addressString)

// Generate specific address index
const addressInfo = await wallet.getAddress(5)
const fifthAddress = await addressInfo.address.asString()
Note: Address generation only available for descriptor/xpub imports, not single addresses.

Balance Monitoring

// Get current balance
const balance = await wallet.getBalance()

const balanceInfo = {
  confirmed: balance.confirmed,        // Confirmed sats
  unconfirmed: balance.trustedPending, // Unconfirmed trusted
  untrusted: balance.untrustedPending, // Unconfirmed untrusted
  total: balance.confirmed + balance.trustedPending
}

console.log('Total balance:', balanceInfo.total, 'sats')

Transaction History

// List all transactions
const transactions = await wallet.listTransactions(true)

// Process transaction history
for (const tx of transactions) {
  const txDetails = {
    txid: tx.txid,
    timestamp: tx.confirmationTime?.timestamp,
    blockHeight: tx.confirmationTime?.height,
    amount: tx.received - tx.sent,
    fee: tx.fee,
    type: tx.sent > 0 ? 'send' : 'receive'
  }
  
  console.log('Transaction:', txDetails)
}

UTXO Tracking

// List unspent outputs
const utxos = await wallet.listUnspent()

// Analyze UTXO set
const utxoAnalysis = {
  count: utxos.length,
  totalValue: utxos.reduce((sum, u) => sum + u.txout.value, 0),
  averageValue: totalValue / count,
  largestUtxo: Math.max(...utxos.map(u => u.txout.value)),
  smallestUtxo: Math.min(...utxos.map(u => u.txout.value))
}

Labeling and Organization

Watch-only accounts support full labeling:
// Label addresses
setAddrLabel(accountId, address, "Cold Storage")

// Label transactions
setTxLabel(accountId, txid, "Payment from Client A")

// Label UTXOs
setUtxoLabel(accountId, txid, vout, "Large deposit")

// Import BIP329 labels
importLabels(accountId, labels)

// Export labels
const labels = formatAccountLabels(account)
exportLabels(labels, 'JSONL')
See Labeling for complete documentation.

Use Cases

Cold Storage Monitoring

Monitor offline storage without risk:
// Setup
1. Generate seed on air-gapped device
2. Export xpub/descriptor
3. Import to SatSigner as watch-only
4. Monitor balance and transactions
5. Generate receive addresses as needed
6. Sign transactions on air-gapped device when spending
Benefits:
  • Check balance anytime
  • Generate addresses without cold device
  • Monitor incoming transactions
  • Zero exposure of private keys

Portfolio Tracking

Track multiple wallets in one place:
const portfolio = [
  {
    name: "Personal Savings",
    xpub: "zpub6rFR7y4Q2...",
    type: "P2WPKH"
  },
  {
    name: "Hardware Wallet",
    xpub: "zpub6s5wLY...",
    type: "P2WPKH"
  },
  {
    name: "Company Treasury",
    descriptor: "wsh(sortedmulti(2,...)))",
    type: "Multisig"
  }
]

// Import all as watch-only
for (const wallet of portfolio) {
  await createWatchOnlyAccount(wallet)
}

// View consolidated balance
const totalBalance = accounts
  .reduce((sum, acc) => sum + acc.summary.balance, 0)

Treasury Oversight

Accountants and auditors can monitor without spending:
// Corporate setup
const treasuryWatch = {
  name: "Company Treasury (Accountant View)",
  descriptor: multisigDescriptor,
  policyType: "watchonly",
  access: "read-only"
}

// Generate reports
const report = {
  period: "Q1 2024",
  startBalance: getBalanceAt(startDate),
  endBalance: getBalanceAt(endDate),
  transactions: getTransactionsBetween(startDate, endDate),
  income: sumIncoming(transactions),
  expenses: sumOutgoing(transactions)
}

Exchange Deposit Monitoring

Track exchange deposits:
// Monitor exchange deposit address
const exchangeMonitor = {
  name: "Exchange Deposit Address",
  address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  policyType: "watchonly"
}

// Alert on deposits
function monitorDeposits() {
  const previousBalance = loadPreviousBalance()
  const currentBalance = account.summary.balance
  
  if (currentBalance > previousBalance) {
    const depositAmount = currentBalance - previousBalance
    alert(`New deposit: ${depositAmount} sats`)
  }
}

Converting to/from Watch-Only

Drop Seed (Signing → Watch-Only)

Convert signing wallet to watch-only:
// Remove mnemonic while keeping xpub
await dropSeedFromKey(accountId, keyIndex)

// Key transformation:
// Before:
{
  secret: {
    mnemonic: "word1 word2 word3...",
    extendedPublicKey: "xpub...",
    fingerprint: "a1b2c3d4"
  }
}

// After:
{
  secret: {
    extendedPublicKey: "xpub...",
    fingerprint: "a1b2c3d4"
  }
}

// Result: Watch-only with full history retained
Use Cases:
  • Enhanced security after setup
  • Temporary signing device
  • Paranoid security model
  • Regulatory compliance

Restore Seed (Watch-Only → Signing)

Re-import seed to enable signing:
// Import mnemonic to existing watch-only account
const key = account.keys[0]

// Verify fingerprint match
const seedFingerprint = getFingerprintFromMnemonic(mnemonic)
if (seedFingerprint !== key.fingerprint) {
  throw new Error('Seed does not match account')
}

// Restore signing capability
key.secret = {
  ...key.secret,
  mnemonic: mnemonic,
  passphrase: passphrase // if used
}

// Account now has signing capability

Security Considerations

What Watch-Only Reveals

Public Information:
  • All addresses in wallet
  • Complete transaction history
  • Current balance
  • UTXO set
Privacy Implications:
  • Anyone with xpub can see all transactions
  • Can link all addresses to same wallet
  • May reveal spending patterns
  • Shows wallet balance
Mitigation:
  • Treat xpub as sensitive
  • Use separate wallets for different purposes
  • Consider privacy-enhancing techniques (CoinJoin)
  • Don’t share xpub publicly

Safe Sharing Practices

Do:
  • Share via encrypted channels
  • Limit sharing to trusted parties
  • Use watch-only for public tracking
  • Rotate wallets periodically
Don’t:
  • Post xpub on social media
  • Share on insecure channels
  • Use same xpub for multiple purposes
  • Reuse xpub after compromise

Gap Limit Considerations

Watch-only accounts scan with gap limit:
// Gap limit (default 20)
const GAP_LIMIT = 20

// Address scanning
let gap = 0
let index = 0

while (gap < GAP_LIMIT) {
  const address = await wallet.getAddress(index)
  const hasTransactions = await checkAddressUsage(address)
  
  if (hasTransactions) {
    gap = 0  // Reset gap
  } else {
    gap++
  }
  
  index++
}
Issue: If you have addresses beyond gap limit with funds, they won’t be detected. Solution: Increase gap limit in settings or manually check specific addresses.

Advanced Features

Multi-Currency Support

Watch-only supports all networks:
const networks = ['bitcoin', 'testnet', 'signet', 'regtest']

// Same xpub, different networks
for (const network of networks) {
  await createWatchOnlyAccount({
    name: `Watch-Only ${network}`,
    network: network,
    xpub: xpub
  })
}

Custom Derivation Paths

Advanced users can specify custom paths:
// Custom derivation
const customDescriptor = `wpkh([a1b2c3d4/84h/0h/5h]xpub.../0/*)` // Account 5

// Non-standard path
const nonStandardDescriptor = `wpkh([a1b2c3d4/44h/0h/0h/2h]xpub.../0/*)` // Custom

Batch Import

Import multiple watch-only accounts:
const wallets = [
  { name: "Wallet 1", xpub: "zpub...1", type: "P2WPKH" },
  { name: "Wallet 2", xpub: "zpub...2", type: "P2WPKH" },
  { name: "Wallet 3", descriptor: "wsh(...)" }
]

// Batch import
for (const wallet of wallets) {
  await createWatchOnlyAccount(wallet)
}

// Sync all
await Promise.all(
  accounts.map(acc => syncWallet(acc.wallet, backend))
)

Troubleshooting

Missing Transactions

Problem: Transactions not showing in watch-only account Solutions:
  1. Increase gap limit
  2. Force resync
  3. Verify correct xpub/descriptor
  4. Check network selection
  5. Verify derivation path

Wrong Balance

Problem: Balance doesn’t match actual funds Possible Causes:
  • Using wrong derivation path
  • Incorrect script type
  • Passphrase not accounted for
  • Custom derivation not specified
Verification:
// Check first address
const firstAddress = await wallet.getAddress(0)
console.log('First address:', await firstAddress.address.asString())

// Compare with expected
if (firstAddress !== expectedFirstAddress) {
  console.error('Descriptor mismatch')
}

Cannot Generate Addresses

Problem: “Cannot generate addresses” error Cause: Using single address import method Solution: Re-import using descriptor or xpub method

Best Practices

  1. Use descriptors when possible - Most comprehensive
  2. Verify first address - Ensure correct derivation
  3. Label everything - Maintain organization
  4. Regular syncs - Keep data current
  5. Secure xpubs - Treat as semi-sensitive
  6. Test first - Verify on testnet
  7. Document setup - Note derivation paths
  8. Backup descriptors - Store safely

Integration Examples

Automated Monitoring

// Monitor watch-only account for changes
setInterval(async () => {
  const previousBalance = loadBalance(accountId)
  await syncWallet(wallet, backend)
  const currentBalance = await wallet.getBalance()
  
  if (currentBalance.confirmed !== previousBalance) {
    const change = currentBalance.confirmed - previousBalance
    notifyBalanceChange(accountId, change)
    saveBalance(accountId, currentBalance.confirmed)
  }
}, 60000) // Check every minute

Report Generation

// Generate transaction report
function generateReport(account: Account, period: DateRange) {
  const transactions = account.transactions.filter(
    tx => tx.timestamp >= period.start && tx.timestamp <= period.end
  )
  
  return {
    period: period,
    transactionCount: transactions.length,
    incoming: transactions.filter(tx => tx.type === 'receive'),
    outgoing: transactions.filter(tx => tx.type === 'send'),
    totalReceived: sumReceived(transactions),
    totalSent: sumSent(transactions),
    netChange: sumReceived(transactions) - sumSent(transactions)
  }
}
Watch-only accounts in SatSigner provide powerful monitoring capabilities while maintaining maximum security through complete separation from private keys.