Skip to main content

How to Monitor Bitcoin Address Balance - A Complete Guide

ยท 7 min read
Yaroslav Pavliuk
Dreamer at EasyLayer.io

Monitoring Bitcoin address balances in real-time is a common requirement for exchanges, payment processors, and portfolio tracking applications. In this comprehensive guide, we'll build a complete Bitcoin address balance monitor using EasyLayer's Bitcoin Crawler that tracks transactions and maintains accurate balance state.

What We're Buildingโ€‹

By the end of this tutorial, you'll have a working Bitcoin address monitor that:

  • Tracks all incoming and outgoing transactions for a specific address
  • Maintains accurate balance calculations in real-time
  • Handles blockchain reorganizations automatically
  • Provides easy access to current balance via API queries

Prerequisitesโ€‹

Before we start, make sure you have:

  • Node.js version 17 or higher
  • Access to a Bitcoin node (your own or QuickNode)
  • Basic understanding of Bitcoin transactions and UTXOs

Setting Up the Projectโ€‹

First, install the Bitcoin Crawler package:

npm install @easylayer/bitcoin-crawler @easylayer/transport-sdk
npm install uuid @types/uuid # For generating request IDs

Step 1: Bootstrap the Applicationโ€‹

Let's start by creating the main application file that initializes our Bitcoin Crawler:

// main.ts
import { bootstrap } from '@easylayer/bitcoin-crawler';
import BalanceModel from './models';

bootstrap({
Models: [BalanceModel],
rpc: true,
}).catch((error: Error) => console.error(error));

This simple bootstrap call starts the Bitcoin Crawler with our custom balance model and enables RPC transport for querying data.

Step 2: Define Custom Eventsโ€‹

Next, we need to define the events that our model will emit when processing blocks. These events capture the essential transaction data:

// events.ts
import { BasicEvent, EventBasePayload } from '@easylayer/bitcoin-crawler';

type TxId = string;
type N = string;
type OutputKey = `${TxId}_${N}`;
type Value = string;

export type Outputs = Map<OutputKey, Value>;

export type Input = {
txid: string; // Current transaction ID
outputTxid: string; // Referenced output transaction ID
outputN: number; // Referenced output index
}

interface BlockAddedEventPayload extends EventBasePayload {
inputs: Input[];
outputs: Outputs;
}

export class BlockAddedEvent extends BasicEvent<BlockAddedEventPayload> {}

Our event structure captures:

  • Outputs: New UTXOs created for our monitored address
  • Inputs: UTXOs spent from our monitored address

Step 3: Implement the Balance Modelโ€‹

Now for the core logic - our balance tracking model:

// models.ts
import { v4 as uuidv4 } from 'uuid';
import { Model } from '@easylayer/bitcoin-crawler';
import { ScriptUtilService } from '@easylayer/bitcoin';
import { Money, Currency } from '@easylayer/common/arithmetic';
import { BlockAddedEvent, Outputs, Input } from './events';

const NETWORK: string = process.env.BITCOIN_CRAWLER_BLOCKCHAIN_NETWORK_NAME || 'testnet';
const CURRENCY: Currency = {
code: 'BTC',
minorUnit: 8,
};

export default class BalanceModel extends Model {
// The Bitcoin address we're monitoring
address: string = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';

// Available UTXOs (unspent outputs)
outputs: Outputs = new Map();

// Spent inputs (for tracking outgoing transactions)
inputs: Input[] = [];

constructor() {
super('bitcoin-balance-monitor'); // Unique model identifier
}

async parseBlock({ block }: { block: any }) {
const { tx, height } = block;
const outputs: Outputs = new Map();
const inputs: Input[] = [];

// Process each transaction in the block
for (let transaction of tx) {
const { txid, vin, vout } = transaction;

// Check outputs (incoming transactions)
for (const vo of vout) {
const scriptHash: string | undefined = ScriptUtilService
.getScriptHashFromScriptPubKey(vo.scriptPubKey, NETWORK);

if (!scriptHash || scriptHash !== this.address) {
continue;
}

// Found an output to our monitored address
const value = Money.fromDecimal(vo.value, CURRENCY).toCents();
outputs.set(`${txid}_${vo.n}`, value);
}

// Check inputs (outgoing transactions)
for (const vi of vin) {
if (vi.txid && vi.vout !== undefined) {
inputs.push({
txid,
outputTxid: vi.txid,
outputN: Number(vi.vout),
});
}
}
}

// Emit event with transaction data
await this.apply(
new BlockAddedEvent({
aggregateId: this.aggregateId,
requestId: uuidv4(),
blockHeight: height,
inputs,
outputs
})
);
}

private onBlockAddedEvent({ payload }: BlockAddedEvent) {
const { inputs, outputs } = payload;

// Add new outputs (incoming funds)
outputs.forEach((value, key) => {
this.outputs.set(key, value);
});

// Remove spent outputs (outgoing funds)
inputs.forEach((input) => {
const outputKey = `${input.outputTxid}_${input.outputN}`;
if (this.outputs.has(outputKey)) {
this.outputs.delete(outputKey);
}
this.inputs.push(input);
});
}

// Calculate current balance from available UTXOs
getCurrentBalance(): string {
let totalBalance = 0;
this.outputs.forEach((value) => {
totalBalance += parseInt(value);
});

return Money.fromCents(totalBalance.toString(), CURRENCY).toDecimal();
}
}

Understanding the Model Logicโ€‹

The balance model works by:

  1. Processing each block: The parseBlock method examines every transaction
  2. Identifying relevant transactions: Using script hash comparison to find transactions involving our address
  3. Tracking UTXOs: Maintaining a map of unspent outputs that belong to our address
  4. Handling spending: Removing UTXOs when they're used as inputs in other transactions
  5. Event sourcing: All changes are recorded as events for auditability and replay capability

Step 4: Querying the Balanceโ€‹

To retrieve the current balance, we'll use the transport SDK to query our model:

// query-balance.ts
import { Client } from '@easylayer/transport-sdk';

const AGGREGATE_ID = 'bitcoin-balance-monitor';

// Initialize the client
const client = new Client({
transport: {
type: 'rpc',
baseUrl: `http://${process.env.HTTP_HOST || 'localhost'}:${process.env.HTTP_PORT || 3000}`,
},
});

async function getCurrentBalance() {
try {
const { payload } = await client.request('query', 'balance-query-1', {
constructorName: 'GetModelsQuery',
dto: {
modelIds: [AGGREGATE_ID],
},
});

// The response contains the current state of our model
const balanceModel = payload[0];
console.log('Current Address:', balanceModel.state.address);
console.log('Current Balance:', balanceModel.state.getCurrentBalance(), 'BTC');
console.log('Available UTXOs:', balanceModel.state.outputs.size);

return balanceModel.state;
} catch (error) {
console.error('Error querying balance:', error);
}
}

// Query balance every 30 seconds
setInterval(getCurrentBalance, 30000);
getCurrentBalance(); // Initial query

Advanced Featuresโ€‹

Historical Balance Queriesโ€‹

You can query balance at any specific block height:

const { payload } = await client.request('query', 'historical-balance', {
constructorName: 'GetModelsQuery',
dto: {
modelIds: [AGGREGATE_ID],
filter: {
blockHeight: 850000 // Get balance at specific block
}
},
});

Transaction Historyโ€‹

Retrieve the complete transaction history for the address:

const { payload } = await client.request('query', 'tx-history', {
constructorName: 'FetchEvents',
dto: {
modelIds: [AGGREGATE_ID],
paging: {
limit: 50,
offset: 0
}
},
});

Configuration Optionsโ€‹

Set up your environment variables for optimal performance:

# Bitcoin node connection
BITCOIN_CRAWLER_NETWORK_PROVIDER_SELF_NODE_URL=http://user:pass@localhost:8332

# Or use QuickNode
BITCOIN_CRAWLER_NETWORK_PROVIDER_QUICK_NODE_URLS=https://your-quicknode-url

# Processing configuration
BITCOIN_CRAWLER_START_BLOCK_HEIGHT=0
BITCOIN_CRAWLER_BLOCKS_QUEUE_LOADER_CONCURRENCY_COUNT=4

# Server configuration
HTTP_HOST=localhost
HTTP_PORT=3000

Real-World Considerationsโ€‹

Performance Optimizationโ€‹

  • Batch Processing: The model processes entire blocks at once for efficiency
  • Concurrent Downloads: Configure block download concurrency based on your node's capacity
  • Database Choice: Use PostgreSQL for production environments with high transaction volumes

Error Handlingโ€‹

The Bitcoin Crawler automatically handles:

  • Blockchain Reorganizations: Events are replayed when chain reorganizes
  • Network Interruptions: Automatic reconnection and block gap filling
  • Invalid Transactions: Malformed data is skipped with appropriate logging

Security Best Practicesโ€‹

  • Run the crawler in a secure environment
  • Use environment variables for sensitive configuration
  • Implement proper access controls for the RPC endpoints
  • Regular backup of the event store database

Conclusionโ€‹

You now have a complete Bitcoin address balance monitor that:

  • Processes blockchain data in real-time
  • Maintains accurate UTXO state
  • Handles blockchain reorganizations automatically
  • Provides easy API access to balance information

This foundation can be extended for more complex use cases like:

  • Multi-address portfolio tracking
  • Payment processing systems
  • Blockchain analytics platforms
  • Automated trading systems

The event-sourcing architecture ensures your data is always consistent and auditable, making it perfect for financial applications that require high reliability and transparency.

Ready to start monitoring Bitcoin addresses? Install the Bitcoin Crawler and begin building your blockchain applications today!

Join our developer Community

EasyLayer is 100% open source. Join our Forum to learn from others and get help whenever you need it!

Join our Forum
โ†’
๐Ÿ“ซ

Subscribe to our Newsletter

Once per month - receive useful blog posts and EasyLayer news.