| name | wallet-brc100 |
| description | This skill should be used when the user asks to "implement BRC-100 wallet", "use wallet-toolbox", "TypeScript BSV wallet", "BRC-100 implementation", "desktop wallet", "Electron wallet", "browser wallet", "IndexedDB wallet storage", "wallet actions", "wallet baskets", "UTXO management", "createAction", "listOutputs", "wallet certificates", "WalletClient", "noSend", "BEEF payment", "pay-beef", "send BEEF", "Transaction.fromBEEF", "toHexBEEF", or needs guidance on building conforming wallets using @bsv/wallet-toolbox or connecting to a user's BRC-100 wallet via WalletClient. |
BRC-100 Wallet Implementation Guide
This skill provides comprehensive guidance for implementing BRC-100 conforming wallets using the @bsv/wallet-toolbox package (v1.7.18+).
Getting Started: Before implementing, review the Strategic Questionnaire to determine the right architecture for your wallet type.
Quick Reference
Core Dependencies
{
"@bsv/wallet-toolbox": "^1.7.18",
"@bsv/sdk": "^1.9.29"
}
WalletClient vs Wallet — Choose the Right Class
This is the most important distinction in the BSV wallet stack:
| Class | Package | Use When |
|---|
WalletClient | @bsv/sdk | Your app connects to a user's existing wallet (browser extension, MetaNet Client, etc.) — you don't control the keys |
Wallet | @bsv/wallet-toolbox | You are building the wallet — you own the keys, storage, and services |
import { WalletClient } from '@bsv/sdk'
const wallet = new WalletClient()
import { Wallet } from '@bsv/wallet-toolbox'
const wallet = new Wallet({ chain, keyDeriver, storage, services })
Most apps (dApps, payment integrations) should use WalletClient. Only wallet builders need Wallet from @bsv/wallet-toolbox.
Main Classes
| Class | Purpose | Use When |
|---|
| WalletClient | Thin BRC-100 client | Apps connecting to user's external wallet |
| Wallet | Full BRC-100 wallet | Building production wallet apps |
| SimpleWalletManager | Lightweight wrapper | Simple key-based authentication |
| CWIStyleWalletManager | Multi-profile wallet | Advanced UMP token flows |
| WalletSigner | Transaction signing | Custom signing logic |
Table of Contents
- Installation & Setup
- Wallet Initialization
- Transaction Operations
- Key Management
- Storage Configuration
- Certificate Operations
- Error Handling
- Production Patterns
1. Installation & Setup
Install Dependencies
bun add @bsv/wallet-toolbox @bsv/sdk
bun add knex sqlite3
bun add knex mysql2
bun add idb
Basic Imports
import {
Wallet,
WalletStorageManager,
StorageKnex,
StorageIdb,
Services,
WalletServices,
PrivilegedKeyManager
} from '@bsv/wallet-toolbox'
import {
PrivateKey,
KeyDeriver,
Random,
Utils
} from '@bsv/sdk'
2. Wallet Initialization
Pattern A: Simple Wallet (Node.js with SQLite)
import { Wallet, StorageKnex, Services } from '@bsv/wallet-toolbox'
import { PrivateKey, Random } from '@bsv/sdk'
import Knex from 'knex'
async function createSimpleWallet() {
const rootKey = new PrivateKey(Random(32))
const keyDeriver = new KeyDeriver(rootKey)
const knex = Knex({
client: 'sqlite3',
connection: { filename: './wallet.db' },
useNullAsDefault: true
})
const storage = new StorageKnex({
knex,
storageIdentityKey: rootKey.toPublicKey().toString(),
storageName: 'my-wallet-storage'
})
await storage.makeAvailable()
const services = new Services({
chain: 'main',
bsvExchangeRate: { timestamp: new Date(), base: 'USD', rate: 50 },
bsvUpdateMsecs: 15 * 60 * 1000,
fiatExchangeRates: {
timestamp: new Date(),
base: 'USD',
rates: { EUR: 0.85, GBP: 0.73 }
},
fiatUpdateMsecs: 24 * 60 * 60 * 1000,
arcUrl: 'https://arc.taal.com',
arcConfig: {}
})
const wallet = new Wallet({
chain: 'main',
keyDeriver,
storage,
services
})
return wallet
}
Pattern B: Browser Wallet (IndexedDB)
import { Wallet, StorageIdb, Services } from '@bsv/wallet-toolbox'
import { PrivateKey, Random } from '@bsv/sdk'
async function createBrowserWallet() {
const rootKey = new PrivateKey(Random(32))
const storage = new StorageIdb({
idb: await openDB('my-wallet-db', 1),
storageIdentityKey: rootKey.toPublicKey().toString(),
storageName: 'browser-wallet'
})
await storage.makeAvailable()
const services = new Services({
chain: 'main',
})
const wallet = new Wallet({
chain: 'main',
keyDeriver: createKeyDeriver(rootKey),
storage,
services
})
return wallet
}
Pattern C: Multi-Profile Wallet
import { CWIStyleWalletManager, OverlayUMPTokenInteractor } from '@bsv/wallet-toolbox'
async function createMultiProfileWallet() {
const manager = new CWIStyleWalletManager(
'example.com',
async (profilePrimaryKey, profilePrivilegedKeyManager, profileId) => {
const keyDeriver = createKeyDeriver(new PrivateKey(profilePrimaryKey))
const storage = await createStorage(profileId)
const services = new Services({ chain: 'main', })
return new Wallet({
chain: 'main',
keyDeriver,
storage,
services,
privilegedKeyManager: profilePrivilegedKeyManager
})
},
new OverlayUMPTokenInteractor(),
async (recoveryKey) => {
console.log('SAVE THIS RECOVERY KEY:', Utils.toBase64(recoveryKey))
return true
},
async (reason, test) => {
const password = prompt(`Enter password for: ${reason}`)
if (!password) throw new Error('Password required')
if (!test(password)) throw new Error('Invalid password')
return password
}
)
const presentationKey = Random(32)
await manager.providePresentationKey(presentationKey)
await manager.providePassword('user-password')
return manager
}
3. Transaction Operations
Create a Transaction
import { CreateActionArgs, CreateActionResult } from '@bsv/sdk'
async function sendBSV(
wallet: Wallet,
recipientAddress: string,
satoshis: number
) {
const args: CreateActionArgs = {
description: 'Send BSV payment',
outputs: [{
lockingScript: Script.fromAddress(recipientAddress).toHex(),
satoshis,
outputDescription: `Payment to ${recipientAddress}`,
basket: 'default',
tags: ['payment']
}],
options: {
acceptDelayedBroadcast: false,
randomizeOutputs: true
}
}
const result: CreateActionResult = await wallet.createAction(args)
if (result.txid) {
console.log('Transaction created:', result.txid)
return result.txid
} else {
console.log('Transaction pending signature')
return result.signableTransaction
}
}
Spending Existing Outputs (inputBEEF Required)
CRITICAL: When spending known wallet outputs via createAction, you MUST provide inputBEEF. The wallet needs the full BEEF proof chain (merkle proofs back to confirmed ancestors) for every input being spent. Without it, createAction will fail with "missing full proof in the inputBEEF".
Two ways to obtain BEEF for inputs:
1. From listOutputs with include: 'entire transactions' — for wallet-owned outputs:
const result = await wallet.listOutputs({
basket: 'my-basket',
include: 'entire transactions',
includeTags: true,
})
const createResult = await wallet.createAction({
description: 'Spend basket outputs',
inputBEEF: result.BEEF,
inputs: result.outputs.map(o => ({
outpoint: o.outpoint,
inputDescription: 'Basket output',
unlockingScriptLength: 180,
sequenceNumber: 0xffffffff,
})),
outputs: [],
options: { signAndProcess: false },
})
2. From a service BEEF lookup — for external inputs (sweep, purchase):
const beef = await services.getBeefForTxid(txid)
for (const additionalTxid of otherTxids) {
beef.mergeBeef(await services.getBeefForTxid(additionalTxid))
}
const createResult = await wallet.createAction({
description: 'Spend external inputs',
inputBEEF: beef.toBinary(),
inputs: [...],
outputs: [...],
})
Never call createAction with inputs but without inputBEEF — even if the wallet "owns" the outputs.
noSend + BEEF Relay Pattern
Use noSend: true when you want to create and sign a transaction but let a backend service validate and/or broadcast it. The result is returned as BEEF (Background Evaluation Extended Format) — a bundle of the transaction plus merkle proofs of its inputs, enabling SPV verification without a full node.
import { WalletClient, P2PKH, Transaction } from '@bsv/sdk'
const wallet = new WalletClient()
const { tx } = await wallet.createAction({
description: 'Payment to service',
outputs: [{
lockingScript: new P2PKH().lock(recipientAddress).toHex(),
satoshis: 1000,
outputDescription: 'Service payment',
}],
options: { noSend: true },
})
const beefHex = Transaction.fromBEEF(tx).toHexBEEF()
await fetch(`https://your-service.example.com/pay?session=${sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beefHex }),
})
Why this pattern?
- The receiving service can verify the payment is valid via SPV before granting access or broadcasting
- Useful for payment-gated APIs, content unlocking, and atomic service flows
- BEEF includes merkle proofs so the server doesn't need a full node
Common mistake: forgetting the $ in template literals — {session} is a literal string, ${session} interpolates the variable.
Sign a Transaction (Two-Phase with completeSignedAction)
Note: completeSignedAction requires @1sat/actions and @bsv/sdk v2+. If using @bsv/wallet-toolbox v1.x without @1sat/actions, use wallet.signAction() directly instead.
For two-phase actions (signAndProcess: false), use the completeSignedAction helper from @1sat/actions instead of calling signAction directly:
import { completeSignedAction } from '@1sat/actions'
const result = await completeSignedAction(
wallet,
createResult,
inputBEEF as number[],
async (tx) => {
return { 0: { unlockingScript: myScript.toHex() } }
},
)
The helper handles BEEF merge, script verification, signAction, and abortAction on failure.
signableTransaction BEEF Stripping
makeSignableTransactionBeef in wallet-toolbox intentionally strips merkle proofs (uses mergeRawTx). The completeSignedAction helper fixes this by merging the unsigned tx into the original inputBEEF:
import { Beef } from '@bsv/sdk'
const beef = Beef.fromBinary(inputBEEF)
beef.mergeRawTx(unsignedTx.toBinary())
const atomicTx = beef.findAtomicTransaction(txid)
This reconstructs the full BEEF with merkle proofs intact.
abortAction Scope
abortAction({ reference }) works on these statuses only:
nosend, unsigned, unprocessed
Does NOT work on: completed, failed, sending, unproven.
Server-side processAction verifies unlocking scripts independently after signAction. On verification failure, the server sets status to failed and releases inputs automatically.
Sign a Transaction (Direct signAction)
For simple cases where you need raw signAction:
async function signTransaction(
wallet: Wallet,
reference: string,
unlockingScripts: Record<number, { unlockingScript: string }>
) {
const result = await wallet.signAction({
reference,
spends: unlockingScripts
})
console.log('Transaction signed:', result.txid)
return result
}
Check Wallet Balance
async function getWalletBalance(wallet: Wallet) {
const balance = await wallet.balance()
console.log(`Balance: ${balance} satoshis`)
const detailed = await wallet.balanceAndUtxos('default')
console.log(`Total: ${detailed.total} satoshis`)
console.log(`UTXOs: ${detailed.utxos.length}`)
detailed.utxos.forEach(utxo => {
console.log(` ${utxo.outpoint}: ${utxo.satoshis} sats`)
})
return balance
}
List Outputs
import { ListOutputsArgs, ListOutputsResult } from '@bsv/sdk'
async function listSpendableOutputs(wallet: Wallet) {
const args: ListOutputsArgs = {
basket: 'default',
spendable: true,
limit: 100,
offset: 0,
tags: ['payment']
}
const result: ListOutputsResult = await wallet.listOutputs(args)
console.log(`Found ${result.totalOutputs} outputs`)
result.outputs.forEach(output => {
console.log(` ${output.outpoint}: ${output.satoshis} sats`)
})
return result
}
List Actions (Transactions)
async function listTransactionHistory(wallet: Wallet) {
const result = await wallet.listActions({
labels: [],
labelQueryMode: 'any',
limit: 50,
offset: 0
})
console.log(`Found ${result.totalActions} actions`)
result.actions.forEach(action => {
console.log(` ${action.txid}: ${action.status} - ${action.description}`)
})
return result
}
4. Key Management
Get Public Key
async function getIdentityKey(wallet: Wallet) {
const result = await wallet.getPublicKey({ identityKey: true })
console.log('Identity Key:', result.publicKey)
return result.publicKey
}
async function getDerivedKey(wallet: Wallet) {
const result = await wallet.getPublicKey({
protocolID: [2, 'my-app'],
keyID: 'encryption-key-1',
counterparty: 'recipient-identity-key'
})
return result.publicKey
}
Encrypt/Decrypt Data
async function encryptMessage(
wallet: Wallet,
plaintext: string,
recipientPubKey: string
) {
const result = await wallet.encrypt({
plaintext: Utils.toArray(plaintext, 'utf8'),
protocolID: [2, 'secure-messaging'],
keyID: 'msg-key',
counterparty: recipientPubKey
})
return Utils.toBase64(result.ciphertext)
}
async function decryptMessage(
wallet: Wallet,
ciphertext: string,
senderPubKey: string
) {
const result = await wallet.decrypt({
ciphertext: Utils.toArray(ciphertext, 'base64'),
protocolID: [2, 'secure-messaging'],
keyID: 'msg-key',
counterparty: senderPubKey
})
return Utils.toUTF8(result.plaintext)
}
Create Signature
async function signData(wallet: Wallet, data: string) {
const result = await wallet.createSignature({
data: Utils.toArray(data, 'utf8'),
protocolID: [2, 'document-signing'],
keyID: 'sig-key',
counterparty: 'self'
})
return Utils.toBase64(result.signature)
}
5. Storage Configuration
See references/storage-config.md for storage configuration details (SQLite, MySQL, IndexedDB, multi-storage manager).
6. Certificate Operations
See references/certificates.md for certificate operations (acquire, list, prove).
7. Error Handling
See references/error-handling.md for error handling patterns (WalletError types, WERR_REVIEW_ACTIONS, double-spend detection).
8. Production Patterns
See references/production-patterns.md for production patterns (wallet state management, transaction retry logic, background monitoring).
Related Skills
For comprehensive wallet development, also reference these skills:
| Skill | Relationship |
|---|
encrypt-decrypt-backup | Standard backup formats (.bep files, AES-256-GCM) |
junglebus | Populate UTXO set from blockchain, real-time streaming |
key-derivation | Type42/BRC-42 and BIP32 key derivation details |
wallet-encrypt-decrypt | ECDH message encryption patterns |
wallet-send-bsv | Basic transaction creation (simpler than BRC-100) |
For 1Sat Ordinals / Token Support:
If your BRC-100 wallet needs to handle 1Sat Ordinals, BSV-20/BSV-21 tokens, or inscriptions, use @1sat/wallet-toolbox which wraps the core wallet-toolbox with ordinals capabilities.
See 1sat for:
wallet-create-ordinals - Mint ordinals/NFTs
extract-blockchain-media - Extract inscribed media from transactions
ordinals-marketplace - List/buy/cancel ordinals (OrdLock)
token-operations - BSV21 token send/receive/deploy
wallet-setup - BRC-100 wallet creation and sync
transaction-building - Action-based tx building (sendBsv, signBsm)
sweep-import - Import from external wallets via WIF
opns-names - OpNS name registration
dapp-connect - dApp wallet connection (@1sat/connect, @1sat/react)
timelock - CLTV time-locked BSV
Additional Resources
Research: For deep dives into BRC specifications or implementation patterns, use the browser-agent to fetch current documentation from bsv.brc.dev.
Platform Guides
Platform-specific implementation guides:
See references/key-concepts.md for BRC-100 unique concepts:
- Actions vs Transactions
- Baskets and Tags
- Certificate system (BRC-52/53/64/65)
- Background Monitoring
Common Patterns Summary
| Task | Method | Key Args |
|---|
| Send BSV | createAction() | outputs, options |
| Check balance | balance() | None |
| List UTXOs | listOutputs() | basket, spendable |
| Get history | listActions() | labels, limit |
| Get pubkey | getPublicKey() | protocolID, keyID |
| Encrypt data | encrypt() | plaintext, counterparty |
| Get certificate | acquireCertificate() | type, certifier |
Remember: Always handle errors properly, use privileged keys securely, and follow BRC-100 security levels for sensitive operations!