Bitcoin Wallet Usage Guide
UTXOS Bitcoin Wallet provides address management, UTXO handling, transfers, PSBT signing, and message signing for Bitcoin.
The Bitcoin wallet API follows the
Xverse Wallet API specification
for compatibility with existing Bitcoin tooling.
Integration Options
Minimal setup (no extra API keys needed for wallet actions):
# .env - Uses your existing UTXOS credentials
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
NEXT_PUBLIC_NETWORK_ID=0 # 0 = testnet/regtest, 1 = mainnetUTXOS does not expose a node API. For on-chain queries, use a provider like Maestro.
Hello World
💡
Minimal code to enable Bitcoin wallet functionality.
import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
const options: EnableWeb3WalletOptions = {
networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: testnet/regtest, 1: mainnet
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
};
const wallet = await Web3Wallet.enable(options);
const bitcoinWallet = wallet.bitcoin;1. Install Dependencies
npm install @meshsdk/web3-sdk @meshsdk/bitcoin2. Initialize the Wallet
import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
import { MaestroProvider } from "@meshsdk/bitcoin";
async function initializeWallet() {
try {
// Configure Bitcoin provider via a secure proxy (keep API keys server-side)
const bitcoinProvider = new MaestroProvider({
network: "testnet", // or "mainnet"
baseUrl: "/api/maestro", // proxy endpoint
});
const options: EnableWeb3WalletOptions = {
networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0,
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
bitcoinProvider, // optional provider for enhanced functionality
};
const wallet = await Web3Wallet.enable(options);
return wallet;
} catch (error) {
console.error("Failed to initialize wallet:", error);
throw error;
}
}3. Verify Connection
const wallet = await initializeWallet();
const bitcoinWallet = wallet.bitcoin;
const addresses = await bitcoinWallet.getAddresses();
console.log("Bitcoin Address:", addresses[0].address);
const balance = await bitcoinWallet.getBalance();
console.log("Bitcoin Balance:", balance.total, "sats");Core Wallet Operations
Address Management
// Request addresses (Xverse format)
const addresses = await bitcoinWallet.getAddresses(
["payment", "ordinals"], // optional purposes
"Please provide payment and ordinals addresses", // optional message
);
addresses.forEach((addr) => {
console.log("Address:", addr.address);
console.log("Public Key:", addr.publicKey);
console.log("Purpose:", addr.purpose); // "payment" or "ordinals"
console.log("Type:", addr.addressType); // "p2wpkh", "p2tr", etc.
console.log("Network:", addr.network); // "mainnet", "testnet", "regtest"
});
const publicKey = bitcoinWallet.getPublicKey();
console.log("Wallet Public Key:", publicKey);
const networkId = bitcoinWallet.getNetworkId();
console.log("Network ID:", networkId); // 0: testnet, 1: mainnet, 2: regtestBalance
const balance = await bitcoinWallet.getBalance();
console.log("Confirmed:", balance.confirmed, "sats");
console.log("Unconfirmed:", balance.unconfirmed, "sats");
console.log("Total:", balance.total, "sats");
const totalBTC = parseInt(balance.total) / 100_000_000;
console.log("Total:", totalBTC, "BTC");UTXOs
const utxos = await bitcoinWallet.getUTxOs();
utxos.forEach((u) => {
console.log("UTXO:", u.txid, ":", u.vout);
console.log("Value:", u.value, "sats");
console.log("Script:", u.script);
});
console.log("Count:", utxos.length);
console.log(
"Total Value:",
utxos.reduce((sum, u) => sum + u.value, 0),
"sats",
);Transfers
try {
const transferResult = await bitcoinWallet.sendTransfer({
recipients: [
{ address: "tb1q...", amount: 100000 }, // sats
{ address: "bc1q...", amount: 50000 },
],
});
console.log("Transfer txid:", transferResult.txid);
} catch (error) {
console.error("Transfer failed:", error);
}PSBT Operations
Sign a PSBT
try {
const psbtBase64 = "cHNidP8BAF4CAAAAAe...";
const signResult = await bitcoinWallet.signPsbt({
psbt: psbtBase64,
signInputs: {
"tb1q...": [0, 1], // address -> input indices
},
broadcast: false, // set true to broadcast immediately
});
console.log("Signed PSBT:", signResult.psbt);
if (signResult.txid) {
console.log("Broadcasted txid:", signResult.txid);
}
} catch (error) {
console.error("PSBT signing failed:", error);
}Message Signing
const addresses = await bitcoinWallet.getAddresses();
const address = addresses[0].address;
try {
const signResult = await bitcoinWallet.signMessage({
address,
message: "Authentication message for my dApp",
protocol: "ECDSA", // currently supported
});
console.log("Signature:", signResult.signature);
console.log("Message Hash:", signResult.messageHash);
console.log("Address:", signResult.address);
} catch (error) {
console.error("Message signing failed:", error);
}Complete Examples
Example 1: Transfer with Balance Check
import { Web3Wallet } from "@meshsdk/web3-sdk";
async function sendBitcoin() {
try {
const wallet = await Web3Wallet.enable({
networkId: 0, // testnet
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
const bitcoinWallet = wallet.bitcoin;
const balance = await bitcoinWallet.getBalance();
const totalSats = parseInt(balance.total);
console.log(`Balance: ${totalSats} sats`);
if (totalSats < 150000) {
throw new Error("Insufficient balance");
}
const result = await bitcoinWallet.sendTransfer({
recipients: [
{
address: "tb1q9pvjqz5u5sdgpatg3wn0ce4v3seqfrqhp4qd2e", // replace
amount: 100000,
},
],
});
console.log("✅ txid:", result.txid);
const newBalance = await bitcoinWallet.getBalance();
console.log(`New balance: ${newBalance.total} sats`);
return result.txid;
} catch (error) {
console.error("❌ Transfer failed:", error);
throw error;
}
}Example 2: PSBT Workflow
async function psbtWorkflow() {
try {
const wallet = await Web3Wallet.enable({
networkId: 0,
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
const bitcoinWallet = wallet.bitcoin;
const addresses = await bitcoinWallet.getAddresses();
const utxos = await bitcoinWallet.getUTxOs();
console.log("Addresses:", addresses.length);
console.log("UTXOs:", utxos.length);
const psbtBase64 = "cHNidP8BAF4CAAAAAe...";
const signResult = await bitcoinWallet.signPsbt({
psbt: psbtBase64,
signInputs: {
[addresses[0].address]: [0],
},
broadcast: true,
});
console.log("✅ Broadcasted txid:", signResult.txid);
return signResult.txid;
} catch (error) {
console.error("❌ PSBT workflow failed:", error);
throw error;
}
}Example 3: Multi-Chain Integration
async function multiChainBitcoinExample() {
try {
const wallet = await Web3Wallet.enable({
networkId: 0,
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
const addresses = {
cardano: await wallet.cardano.getChangeAddress(),
bitcoin: (await wallet.bitcoin.getAddresses())[0]?.address,
spark: await wallet.spark.getSparkAddress(),
};
console.log("🌐 Addresses:", addresses);
const balances = {
cardano: await wallet.cardano.getBalance(),
bitcoin: await wallet.bitcoin.getBalance(),
spark: await wallet.spark.getBalance(),
};
console.log("💰 BTC:", balances.bitcoin.total, "sats");
console.log("💰 Spark:", balances.spark.balance.toString(), "sats");
const message = "Multi-chain authentication";
const bitcoinSignature = await wallet.bitcoin.signMessage({
address: addresses.bitcoin!,
message,
protocol: "ECDSA",
});
console.log("🔐 BTC signature:", bitcoinSignature.signature);
return addresses;
} catch (error) {
console.error("❌ Multi-chain operation failed:", error);
throw error;
}
}Error Handling
import { ApiError } from "@meshsdk/web3-sdk";
async function handleBitcoinOperation() {
try {
const result = await bitcoinWallet.sendTransfer({
recipients: [{ address: "tb1q...", amount: 50000 }],
});
return result;
} catch (error) {
if (error.message.includes("Insufficient funds")) {
throw new Error("Not enough Bitcoin to complete the transaction");
}
if (error.message.includes("Invalid address")) {
throw new Error("Invalid Bitcoin address format");
}
if (error.message.includes("read-only wallet")) {
throw new Error("Cannot sign with an address-only wallet");
}
throw error;
}
}Network Configuration
Development (Testnet)
const wallet = await Web3Wallet.enable({
networkId: 0, // testnet
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
const bitcoinWallet = wallet.bitcoin;Production (Mainnet)
const wallet = await Web3Wallet.enable({
networkId: 1, // mainnet
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
const bitcoinWallet = wallet.bitcoin;Address Types
const addresses = await bitcoinWallet.getAddresses(["payment", "ordinals"]);
addresses.forEach((addr) => {
console.log("Type:", addr.addressType);
// Supported:
// - "p2wpkh" (Native SegWit) -> bc1q / tb1q
// - "p2tr" (Taproot) -> bc1p / tb1p
// - "p2sh" (Wrapped SegWit) -> 3 / 2
});Security
⚠️
- Validate addresses before sending
- Use testnet during development and test thoroughly before mainnet
- Implement robust error handling
- Check balances and UTXOs before sending
- Never use test mnemonics in production
- Verify PSBT details before signing
- Monitor fee rates to avoid overpaying
Set Up a Bitcoin Data Provider
Supported providers: MaestroProvider and Blockstream (more coming soon).
Keep provider API keys server-side via a proxy:
# .env
MAESTRO_BITCOIN_API_KEY_TESTNET=your_testnet_key
MAESTRO_BITCOIN_API_KEY_MAINNET=your_mainnet_keyMinimal Next.js proxy route:
// app/api/maestro/[...slug]/route.ts
export async function GET(request, { params }) {
return handleMaestroRequest(request, params, "GET");
}
export async function POST(request, { params }) {
return handleMaestroRequest(request, params, "POST");
}
async function handleMaestroRequest(request, { params }, method) {
try {
const { slug } = params;
const network = slug[0]; // "mainnet" | "testnet"
const apiKey =
network === "mainnet"
? process.env.MAESTRO_BITCOIN_API_KEY_MAINNET
: process.env.MAESTRO_BITCOIN_API_KEY_TESTNET;
if (!apiKey) {
return Response.json(
{
error: `Missing Maestro Bitcoin API key for ${network}`,
message: `Add MAESTRO_BITCOIN_API_KEY_${network.toUpperCase()} to .env`,
},
{ status: 500 },
);
}
const baseURL =
network === "mainnet"
? "https://xbt-mainnet.gomaestro-api.org/v0"
: "https://xbt-testnet.gomaestro-api.org/v0";
const endpoint = slug.slice(1).join("/");
const url = `${baseURL}/${endpoint}`;
const requestBody = method !== "GET" ? await request.json() : undefined;
const response = await fetch(url, {
method,
headers: {
"api-key": apiKey,
Accept: "application/json",
...(method !== "GET" && { "Content-Type": "application/json" }),
},
body: requestBody ? JSON.stringify(requestBody) : undefined,
});
const data = await response.json();
return Response.json(data, { status: response.status });
} catch (error) {
console.error("Maestro Bitcoin API route error:", error);
return Response.json({ error: error.message }, { status: 500 });
}
}With the proxy in place, you can safely use enhanced Bitcoin features that require provider access.
Test Your Integration
import { Web3Wallet } from "@meshsdk/web3-sdk";
async function testBitcoinIntegration() {
try {
const wallet = await Web3Wallet.enable({
networkId: 0,
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
});
console.log("✅ Wallet initialized");
const addresses = await wallet.bitcoin.getAddresses();
console.assert(addresses.length > 0, "No addresses returned");
console.assert(
addresses[0].address.startsWith("tb1"),
"Invalid testnet address",
);
console.log("✅ Addresses OK");
const balance = await wallet.bitcoin.getBalance();
console.assert(typeof balance.total === "string", "Invalid balance format");
console.log("✅ Balance OK");
const utxos = await wallet.bitcoin.getUTxOs();
console.assert(Array.isArray(utxos), "Invalid UTXO format");
console.log("✅ UTXOs OK");
const signResult = await wallet.bitcoin.signMessage({
address: addresses[0].address,
message: "test message",
protocol: "ECDSA",
});
console.assert(
typeof signResult.signature === "string",
"Invalid signature",
);
console.log("✅ Message signing OK");
console.log("🎉 All tests passed");
} catch (error) {
console.error("❌ Integration test failed:", error);
}
}