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 = mainnet

UTXOS 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/bitcoin

2. 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: regtest

Balance

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_key

Minimal 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);
  }
}