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:
| Prefix | Network | Script Type | Derivation |
|---|
| xpub | Mainnet | P2PKH/P2TR | BIP32 |
| ypub | Mainnet | P2SH-P2WPKH | BIP49 |
| zpub | Mainnet | P2WPKH | BIP84 |
| tpub | Testnet | Any | BIP32 |
| upub | Testnet | P2SH-P2WPKH | BIP49 |
| vpub | Testnet | P2WPKH | BIP84 |
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
}
From Hardware Wallet
Most hardware wallets can export xpubs:
Ledger:
- Open Bitcoin app
- Navigate to “Settings” > “Public Key”
- Select account and export
- Copy extended public key
Trezor:
- Connect device
- Open Trezor Suite
- Navigate to account details
- Click “Show public key”
- Copy xpub/zpub
Coldcard:
- Advanced > MicroSD Card > Export Wallet
- Select “Generic JSON” or “Descriptor”
- Save to SD card
- 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:
- File > Export Wallet
- Select “Output Descriptors”
- Copy descriptor text
- Import to SatSigner
BlueWallet:
- Wallet > Settings
- “Export/Backup”
- “View in wallet export format”
- 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:
- Increase gap limit
- Force resync
- Verify correct xpub/descriptor
- Check network selection
- 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
- Use descriptors when possible - Most comprehensive
- Verify first address - Ensure correct derivation
- Label everything - Maintain organization
- Regular syncs - Keep data current
- Secure xpubs - Treat as semi-sensitive
- Test first - Verify on testnet
- Document setup - Note derivation paths
- 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.