Own node kit

Run your own node

The app reads the chain through an Esplora-style HTTP API. By default that is a public explorer. Point it at your own node and every number on every page comes from blocks you validated yourself, with nobody in between.

WhyWhat it takesdocker-compose.ymlCORS and TLSPoint the app at itCheck it is really your nodeStart on signet

Why bother

A public explorer is a convenience with three weaknesses. It can lag (a burn you just sent is not there yet), it can lie (by mistake or on purpose, and you would not know), and it sees you: every topic you open and every address you look at is a request in its logs. Your own node fixes all three. The app was written so that switching costs one URL.

The kit below is three containers: bitcoind validates and stores the chain, electrs (the Esplora fork) indexes it and speaks the HTTP API the app expects, and a small reverse proxy adds TLS and the CORS header a browser needs to call it from another origin.

What it takes

mainnetsignet
bitcoind, full chain with txindex=1≈ 750 GB and growing, ~6 h to 3 days to sync depending on disk and bandwidth≈ 3 GB, minutes
electrs index (Esplora fork, address history for every address)≈ 500 GB more, 1 to 3 days after bitcoind is synced≈ 1 GB, minutes
memory8 GB is comfortable; the first index pass likes more2 GB
bandwidththe initial download once, then a few GB a monthnegligible
Figures are as of 2026 and go up. Use an SSD; the index is unusable on spinning disks. txindex=1 is not required by electrs, but the verification page uses getrawtransaction on your node and that needs it.

docker-compose.yml

One file, three services. Replace YOUR_RPC_PASSWORD, node.example.com and the app origin, then docker compose up -d. Pin the image tags to versions you have checked; the ones below are examples, not endorsements.

# docker-compose.yml · bitcoind + electrs (Esplora HTTP API) + Caddy (TLS, CORS)
services:
  bitcoind:
    image: bitcoin/bitcoin:29
    restart: unless-stopped
    command: >-
      bitcoind -printtoconsole -server=1 -txindex=1
      -rpcbind=0.0.0.0 -rpcallowip=172.16.0.0/12
      -rpcuser=bv -rpcpassword=YOUR_RPC_PASSWORD
      # add -signet to run the signet kit instead (see below)
    ports: ["8333:8333"]
    volumes: ["bitcoind:/home/bitcoin/.bitcoin"]

  electrs:
    image: mempool/electrs:latest        # Blockstream's Esplora fork of electrs: /address/…/txs, /tx, /blocks/tip/height
    restart: unless-stopped
    depends_on: [bitcoind]
    command: >-
      electrs -vv --network bitcoin
      --daemon-rpc-addr bitcoind:8332 --cookie bv:YOUR_RPC_PASSWORD
      --daemon-dir /bitcoind --db-dir /db
      --http-addr 0.0.0.0:3000
      # --network signet --daemon-rpc-addr bitcoind:38332 for the signet kit
    volumes: ["bitcoind:/bitcoind:ro", "electrs:/db"]

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on: [electrs]
    ports: ["80:80", "443:443"]
    volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "caddy:/data"]

volumes: { bitcoind: {}, electrs: {}, caddy: {} }

CORS and TLS: the Caddyfile

The app is a page on one origin calling an API on another, so the browser asks the API for permission first (a CORS preflight) and the API must answer with the app's origin. Caddy also gets a certificate for you, and the page, if it is served over HTTPS, may only call an HTTPS API. Put this next to the compose file:

# Caddyfile · https://node.example.com/api/… → electrs
node.example.com {
  handle_path /api/* {
    header Access-Control-Allow-Origin  "https://burningtake.example"   # the origin the app is served from; "*" if you do not care
    header Access-Control-Allow-Methods "GET, POST, OPTIONS"
    header Access-Control-Allow-Headers "content-type, accept"
    header Access-Control-Max-Age       "86400"
    @preflight method OPTIONS
    respond @preflight 204
    reverse_proxy electrs:3000
  }
  respond 404
}

Two shortcuts. If you open the app from a file on disk or from http://localhost, the browser lets it call http://127.0.0.1:3000 directly, so you can skip Caddy and set --cors "*" on electrs instead. And if the app is served by the same host as the API, there is no cross-origin call at all.

Point the app at it

On the front page

The Data source row at the bottom of the app has an explorer selector. Pick Custom Esplora URL and enter the API base, for example https://node.example.com/api. The choice is saved in this browser, per network, and every page reads through it from then on: the boards, the burner wallet's balance, the broadcast.

Or in the URL

Any page accepts ?endpoint=https://node.example.com/api once; it is saved the same way. ?endpoint= (empty) goes back to the default explorer. Combine with ?net=signet for the signet endpoint.

What it has to answer

The Esplora routes the app uses: GET /address/<addr>/txs/chain[/<last txid>] and /address/<addr> for the boards, GET /address/<addr>/utxo for the wallet, POST /tx to broadcast, GET /blocks/tip/height for the tip. Any server that speaks Esplora (Blockstream's, mempool.space's, this electrs) works.

Check the app is really talking to your node

Read the Data source row

Under the selector, the app prints the base URL it is reading through. It should be your host, without the word default.

Watch the network tab

Open the browser's developer tools, Network, then open any topic. Every request that carries an address or a txid must go to your host: node.example.com/api/address/bc1q…/txs/chain, nothing to mempool.space or blockstream.info. Filter by address to see only those. The font and QR library requests are the only other ones a page makes.

Ask the node the same question

bitcoin-cli scantxoutset start '["addr(<topic address>)"]' returns the total ever burned into a topic. It should match the topic page to the sat, once the mempool is out of the picture.

Pull the plug

Stop electrs (docker compose stop electrs) and open the wallet chip. The balance line must say the explorer at your URL is unreachable; the app never falls back to a public explorer on its own. Topics you already opened keep showing from the cache, with the block height they were last synced at.

Start on signet

Signet is the same software on a test chain: gigabytes instead of terabytes, minutes instead of days, and free coins from a faucet to burn. Add -signet to bitcoind, --network signet and --daemon-rpc-addr bitcoind:38332 to electrs (signet's RPC port, where mainnet uses 8332), and open the app with ?net=signet&endpoint=https://node.example.com/api. Everything else, from the derivation of a topic address to the receipt of a burn, is identical. When the signet kit does what you expect, the mainnet one is a tag and a bigger disk away.

The endpoint is saved per network. A signet node configured while on signet is not used on mainnet, and the other way round, so a mistake there cannot send the app to the wrong chain.