Fast Bitcoin address balance lookup library using memory-mapped address_map.bin.
This library provides instant local lookups for Bitcoin address balances using a memory-mapped binary file built from Bitcoin Core's chainstate database. It's optimized for simplicity and speed when checking small to medium batches of addresses.
Use this library when:
- You're checking less than ~250k addresses
- You want simple, straightforward code
- Startup overhead matters more than per-lookup speed
- You prefer direct lookups over complex data structures
For typical wallet scanning (99% miss rate):
| Addresses | Total Time | Per Address | Use Case |
|---|---|---|---|
| 100 | 0.05ms | 0.5µs | Small batches |
| 10,000 | 5ms | 0.5µs | Medium batches |
| 1,000,000 | 500ms | 0.5µs | Large batch |
Lookup speed: ~500ns per address (binary search on mmap'd file)
btc-balance/
├── pkg/btcbalance/ # Library
│ ├── address.go # Bitcoin address parsing (all types including Taproot)
│ └── balance.go # Address map reader for balance lookups
├── cmd/btc-balance/ # CLI tool (query + build)
│ └── main.go
├── example.go # Usage example
├── go.mod
└── README.md
# Install btc-balance (includes query and build commands)
go install github.com/seed-safe/btc-balance/cmd/btc-balance@latestOption A: Download pre-built index (fastest)
Note: The index file
address_map.bin(2.1 GB) is not included. Download from releases or generate it yourself.
# Create data directory
mkdir -p data
# Download compressed index from releases (1.8 GB compressed, 2.1 GB uncompressed)
curl -L https://github.com/seed-safe/btc-balance/releases/download/v0.1.0/address_map.bin.gz -o data/address_map.bin.gz
# Decompress
gunzip data/address_map.bin.gz
# Verify
btc-balance infoOption B: Build from Bitcoin Core chainstate (most current)
Requires Bitcoin Core with a synced chainstate database:
export BITCOIN_DATADIR=/path/to/bitcoin/datadir
btc-balance build
# This creates data/address_map.bin (~2.1 GB)# View index information (date, block height, number of addresses)
btc-balance info
# Output:
# Address Map Information
# =======================
# Addresses: 56795328
# Created: 2025-10-11 12:53:56 CDT
# Block Hash: ed968cf315...# Query an address (uses data/address_map.bin by default)
btc-balance query --key bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
# Output: balance_sats=123456
btc-balance query --key 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
# Output: balance_sats=5438000000
btc-balance query --key bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
# Output: no-balanceInstallation:
go get github.com/seed-safe/btc-balanceGetting the index file:
Before running your code, you need address_map.bin:
- Production: Download from Releases (1.8 GB compressed, 2.1 GB uncompressed)
mkdir -p data curl -L https://github.com/seed-safe/btc-balance/releases/download/v0.1.0/address_map.bin.gz -o data/address_map.bin.gz gunzip data/address_map.bin.gz
- Testing: Use a sample file or build a subset yourself
Specifying the index location:
// Option 1: Default (checks data/address_map.bin, then ./address_map.bin)
addrMap, _ := btcbalance.OpenAddrMap("")
// Option 2: Explicit path
addrMap, _ := btcbalance.OpenAddrMap("/var/lib/btc-balance/address_map.bin")
// Option 3: Environment variable (best for production)
// export BTC_BALANCE_INDEX=/var/lib/btc-balance/address_map.bin
addrMap, _ := btcbalance.OpenAddrMap("")Example code:
package main
import (
"fmt"
"log"
"github.com/seed-safe/btc-balance/pkg/btcbalance"
)
func main() {
// Open address_map.bin (mmap'd, instant startup)
// Looks for: explicit path > BTC_BALANCE_INDEX env var > ./address_map.bin
addrMap, err := btcbalance.OpenAddrMap("") // Empty = use default locations
if err != nil {
log.Fatal(err)
}
defer addrMap.Close()
// Your address list
addresses := []string{
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", // Taproot
}
for _, addr := range addresses {
// Convert address to scripthash
scripthash, err := btcbalance.AddressToScripthash(addr)
if err != nil {
fmt.Printf("%s: invalid address\n", addr)
continue
}
// Lookup balance
balance, found := addrMap.Lookup(scripthash)
if !found || balance == 0 {
fmt.Printf("%s: no balance\n", addr)
} else {
fmt.Printf("%s: %d sats (%.8f BTC)\n", addr, balance, float64(balance)/1e8)
}
}
}// Address parsing (supports all Bitcoin address types including Taproot)
func AddressToScripthash(address string) ([32]byte, error)
func ScriptFromAddress(address string) ([]byte, error)
// Open address_map.bin (do once at startup)
func OpenAddrMap(path string) (*AddrMapReader, error)
// Get exact balance (fast binary search, ~500ns)
func (r *AddrMapReader) Lookup(key [32]byte) (balance uint64, found bool)
// Close when done
func (r *AddrMapReader) Close() error- P2PKH (legacy):
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa - P2SH:
3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX - P2WPKH (bech32):
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 - P2WSH (bech32):
bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 - P2TR/Taproot (bech32m):
bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr - Hex scripts:
0x76a914...
- Small wallet recoveries - Checking a few dozen to thousands of addresses
- Quick balance checks - One-off lookups in scripts
- Development/testing - Simple code, easier to understand
- Local-first applications - No API dependencies, works offline
Before (slow):
for _, addr := range addresses {
balance, err := fulcrumClient.GetBalance(addr) // 100ms per call!
if balance > 0 {
process(addr, balance)
}
}After (fast):
addrMap, _ := btcbalance.OpenAddrMap("address_map.bin")
defer addrMap.Close()
for _, addr := range addresses {
sh, _ := btcbalance.AddressToScripthash(addr)
balance, found := addrMap.Lookup(sh) // 0.5µs lookup!
if found && balance > 0 {
process(addr, balance)
}
}200,000x faster than API calls!
Binary file format:
- Magic:
AMAP(0x50414D41) - Version: 1
- Records: Fixed 40-byte records
[32]scripthash || uint64 balance - Ordering: Sorted by scripthash for binary search
- Loading: Memory-mapped for instant startup
- Integrity: CRC32 checksums
- Metadata: Source block height, creation timestamp
Size: ~2.1 GB (57M unique addresses × 40 bytes)
# Clone the repo
git clone https://github.com/seed-safe/btc-balance
cd btc-balance
# Build example (quick test)
go build # creates ./example binary
# Build CLI tool
go build -o btc-balance ./cmd/btc-balance
# Run tests
go test ./pkg/btcbalance/...Rebuild periodically to stay current:
# Stop Bitcoin Core (optional, for consistency)
bitcoin-cli stop
# Rebuild the index
export BITCOIN_DATADIR=/path/to/bitcoin/datadir
./chainstate-scan
# Restart Bitcoin Core
bitcoind -daemon
# This updates address_map.bin with latest UTXO setRecommended update frequency:
- Daily for active trading/monitoring
- Weekly for general use
- After major balance changes in your address set
The library uses Electrum-style scripthashes:
scripthash = SHA256(scriptPubKey)
This is computed automatically by AddressToScripthash() - you just pass in the address string.
- No cgo dependencies
- Cross-compiles easily
- Static binaries
- Builds with
CGO_ENABLED=0
MIT