On-Chain Bitcoin
Send and receive Bitcoin on the main blockchain for larger transactions and cold storage.
When to Use On-Chain vs Lightning
⚡ Lightning (Recommended)
- Instant payments (milliseconds)
- Near-zero fees (<1 sat)
- Microtransactions
- High-frequency payments
🔗 On-Chain (This Guide)
- Large amounts (>$1000)
- Cold storage/savings
- Final settlement
- No channel limits
For most AI agent use cases, Lightning is better. Use on-chain for larger amounts or when you need to move Bitcoin to cold storage.
Install Dependencies
npm install bitcoinjs-lib @noble/hashes ecpair tiny-secp256k1Note: bitcoinjs-lib is the standard library for Bitcoin transactions in JavaScript.
Generate Bitcoin Address
Create a native SegWit (bech32) address from your Nostr keypair:
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';
import { hexToBytes } from '@noble/hashes/utils';
// Initialize ECPair with secp256k1
bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
// Your Nostr secret key (same curve as Bitcoin!)
const secretKeyHex = process.env.NOSTR_SECRET_KEY;
const secretKey = hexToBytes(secretKeyHex);
// Create keypair
const keyPair = ECPair.fromPrivateKey(Buffer.from(secretKey));
// Generate native SegWit address (bech32, starts with bc1)
const { address } = bitcoin.payments.p2wpkh({
pubkey: keyPair.publicKey,
network: bitcoin.networks.bitcoin, // Use testnet for testing
});
console.log('Bitcoin Address:', address);
// Example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlhSame Keys! Your Nostr keypair uses the same secp256k1 curve as Bitcoin. You can derive a Bitcoin address from your Nostr identity.
Check Balance
Use a blockchain API to check your address balance:
// Using mempool.space API (free, no auth required)
async function getBalance(address) {
const response = await fetch(
`https://mempool.space/api/address/${address}`
);
const data = await response.json();
const confirmed = data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum;
const unconfirmed = data.mempool_stats.funded_txo_sum - data.mempool_stats.spent_txo_sum;
return {
confirmed, // in satoshis
unconfirmed, // in satoshis
total: confirmed + unconfirmed,
};
}
// Usage
const balance = await getBalance('bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh');
console.log('Confirmed:', balance.confirmed, 'sats');
console.log('Unconfirmed:', balance.unconfirmed, 'sats');Get UTXOs
To spend Bitcoin, you need to know your unspent transaction outputs (UTXOs):
async function getUTXOs(address) {
const response = await fetch(
`https://mempool.space/api/address/${address}/utxo`
);
const utxos = await response.json();
return utxos.map(utxo => ({
txid: utxo.txid,
vout: utxo.vout,
value: utxo.value, // in satoshis
confirmed: utxo.status.confirmed,
}));
}
// Usage
const utxos = await getUTXOs('bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh');
console.log('UTXOs:', utxos);
// [{ txid: '...', vout: 0, value: 50000, confirmed: true }]Estimate Fees
Get current fee rates for transaction priority:
async function getFeeRates() {
const response = await fetch(
'https://mempool.space/api/v1/fees/recommended'
);
return await response.json();
}
// Usage
const fees = await getFeeRates();
console.log('Fast (next block):', fees.fastestFee, 'sat/vB');
console.log('Medium (30 min):', fees.halfHourFee, 'sat/vB');
console.log('Slow (1 hour):', fees.hourFee, 'sat/vB');
console.log('Economy:', fees.economyFee, 'sat/vB');
// Calculate fee for a typical transaction (~140 vBytes for 1-in-1-out)
const txSize = 140; // virtual bytes
const estimatedFee = fees.halfHourFee * txSize;
console.log('Estimated fee:', estimatedFee, 'sats');Create & Sign Transaction
Build and sign a Bitcoin transaction:
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';
bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
async function createTransaction({
secretKey,
toAddress,
amountSats,
feeRate,
}) {
// Create keypair
const keyPair = ECPair.fromPrivateKey(Buffer.from(secretKey));
const { address: fromAddress, output } = bitcoin.payments.p2wpkh({
pubkey: keyPair.publicKey,
});
// Get UTXOs
const utxos = await getUTXOs(fromAddress);
if (utxos.length === 0) throw new Error('No UTXOs available');
// Build transaction
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
// Add inputs (simplified: using first UTXO)
let inputSum = 0;
for (const utxo of utxos) {
// Get the raw transaction for witness data
const txHex = await fetch(
`https://mempool.space/api/tx/${utxo.txid}/hex`
).then(r => r.text());
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: output,
value: utxo.value,
},
});
inputSum += utxo.value;
if (inputSum >= amountSats + 1000) break; // +1000 for fees
}
// Calculate fee (estimate ~110 vBytes for 1-in-2-out)
const estimatedSize = 110 + (psbt.inputCount - 1) * 68;
const fee = Math.ceil(feeRate * estimatedSize);
// Add outputs
psbt.addOutput({
address: toAddress,
value: amountSats,
});
// Change output (back to ourselves)
const change = inputSum - amountSats - fee;
if (change > 546) { // Dust limit
psbt.addOutput({
address: fromAddress,
value: change,
});
}
// Sign all inputs
psbt.signAllInputs(keyPair);
psbt.finalizeAllInputs();
// Get raw transaction hex
const tx = psbt.extractTransaction();
return {
txHex: tx.toHex(),
txid: tx.getId(),
fee,
};
}Broadcast Transaction
async function broadcastTransaction(txHex) {
const response = await fetch('https://mempool.space/api/tx', {
method: 'POST',
body: txHex,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast failed: ${error}`);
}
const txid = await response.text();
console.log('Transaction broadcast!');
console.log('TXID:', txid);
console.log('Track:', `https://mempool.space/tx/${txid}`);
return txid;
}
// Usage
const { txHex, txid, fee } = await createTransaction({
secretKey: hexToBytes(process.env.NOSTR_SECRET_KEY),
toAddress: 'bc1q...recipient...',
amountSats: 50000,
feeRate: 10, // sat/vB
});
console.log('Fee:', fee, 'sats');
await broadcastTransaction(txHex);Complete On-Chain Agent Class
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';
import { hexToBytes } from '@noble/hashes/utils';
bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
export class OnChainAgent {
constructor(secretKeyHex, network = 'mainnet') {
this.secretKey = hexToBytes(secretKeyHex);
this.keyPair = ECPair.fromPrivateKey(Buffer.from(this.secretKey));
this.network = network === 'mainnet'
? bitcoin.networks.bitcoin
: bitcoin.networks.testnet;
const { address } = bitcoin.payments.p2wpkh({
pubkey: this.keyPair.publicKey,
network: this.network,
});
this.address = address;
this.apiBase = network === 'mainnet'
? 'https://mempool.space/api'
: 'https://mempool.space/testnet/api';
}
async getBalance() {
const res = await fetch(`${this.apiBase}/address/${this.address}`);
const data = await res.json();
return {
confirmed: data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum,
unconfirmed: data.mempool_stats.funded_txo_sum - data.mempool_stats.spent_txo_sum,
};
}
async send(toAddress, amountSats, feeRate = 10) {
// Implementation as shown above
const { txHex, txid, fee } = await this.createTransaction(
toAddress,
amountSats,
feeRate
);
await this.broadcast(txHex);
return { txid, fee };
}
async waitForConfirmation(txid, confirmations = 1) {
while (true) {
const res = await fetch(`${this.apiBase}/tx/${txid}`);
const tx = await res.json();
if (tx.status.confirmed) {
const tipRes = await fetch(`${this.apiBase}/blocks/tip/height`);
const tipHeight = await tipRes.json();
const confs = tipHeight - tx.status.block_height + 1;
if (confs >= confirmations) {
return { confirmed: true, confirmations: confs };
}
}
await new Promise(r => setTimeout(r, 30000)); // Check every 30s
}
}
}
// Usage
const agent = new OnChainAgent(process.env.NOSTR_SECRET_KEY);
console.log('Address:', agent.address);
const balance = await agent.getBalance();
console.log('Balance:', balance.confirmed, 'sats');
// Send payment
const { txid } = await agent.send('bc1q...', 100000, 5);
await agent.waitForConfirmation(txid, 3);Security Considerations
Use Testnet First
Always test with testnet before using real Bitcoin. Change network to testnet and use a testnet faucet to get test coins.
Verify Addresses
Always double-check recipient addresses. Bitcoin transactions are irreversible.
Fee Estimation
Low fees may cause transactions to get stuck. Check current mempool conditions and use appropriate fee rates.
UTXO Management
Many small UTXOs increase transaction fees. Consider consolidating UTXOs during low-fee periods.