Bitcoin Beacon over Meshtastic: Broadcast Chain State Off-Grid
Quick answer
A Bitcoin beacon is a self-hosted node that periodically broadcasts the chain’s vital signs — block height, tip hash and a fee-rate snapshot — as a plain Meshtastic text message over LoRa radio. Three bitcoin-cli calls (getblockcount, getbestblockhash, estimatesmartfee) feed one meshtastic --sendtext broadcast on a schedule. Anyone on the mesh channel stays informed about the Bitcoin network with no internet, no carrier and no account — the message just has to fit Meshtastic’s DATA_PAYLOAD_LEN of 233 bytes.
Bitcoin’s failure mode is rarely the network itself — it is your connection to it. An ISP outage, a grid failure or plain censorship leaves a node operator blind: is the chain advancing? What is the tip? What would a transaction cost right now? A beacon answers those questions for an entire neighbourhood mesh from one node you control. This is defensive infrastructure: it moves no coins and signs nothing. If you want to move actual transactions over a mesh, that is a different (and harder) job — see Bitcoin over Meshtastic: signing and broadcasting transactions off-grid and Can I send bitcoins with mesh networks?.
What the protocol actually allows
Every value below is a literal from the Meshtastic sources (as of 2026-07-22,
master) — nothing here comes from a summary. Meshtastic is a moving target; re-check these constants
against the release you run.
| Fact | Literal value | Source |
|---|---|---|
| Maximum text payload | DATA_PAYLOAD_LEN = 233 bytes — “ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is outside of this envelope” | meshtastic/protobufs → mesh.proto, enum Constants |
| CLI length guard | if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN: raise "Data payload too big" | meshtastic/python → mesh_interface.py, sendData() |
Port used by --sendtext | TEXT_MESSAGE_APP = 1 — “ENCODING: UTF-8 Plaintext”; sendText() defaults to portNum = TEXT_MESSAGE_APP | portnums.proto; mesh_interface.py — see the PortNum registry |
| Broadcast addressing | BROADCAST_ADDR = "^all" (Python CLI default destination); on the wire #define NODENUM_BROADCAST UINT32_MAX | meshtastic/python → __init__.py; meshtastic/firmware → src/mesh/MeshTypes.h |
| Hop limit | “Maximum number of hops. This can’t be greater than 7. Default of 3. Attempting to set a value > 7 results in the default” | config.proto → LoRaConfig.hop_limit |
| Hops on the wire | hop_start: “Sent via LoRa using three bits in the unencrypted header” | mesh.proto → MeshPacket.hop_start |
In short: one broadcast text of up to 233 UTF-8 bytes reaches every node within (by default) 3 hops of your beacon, relayed by the mesh itself. That is far more room than a chain-state snapshot needs.
What you need
- A self-hosted Bitcoin Core node with
bitcoin-cliworking locally. The beacon only reads — no wallet required. - A Meshtastic node the machine can reach — over USB serial (
--port /dev/ttyUSB0) or WiFi/Ethernet (--host <ip>). New to Meshtastic? Start with Meshtastic for Bitcoiners. - The Meshtastic Python CLI (
pipx install meshtastic) andjqfor parsingbitcoin-cliJSON. - A dedicated secondary channel for the beacon (
--ch-index 1or higher), so recurring chain data does not clutter the primary channel everyone chats on.
The beacon script
Three read-only RPCs, one message, one hard byte guard. estimatesmartfee returns BTC/kvB, so multiplying
by 100,000 yields sat/vB; a fresh or fee-data-poor node can return no estimate, which the script degrades to
n/a instead of failing.
#!/usr/bin/env bash
# btc-beacon.sh — broadcast Bitcoin chain state over a Meshtastic LoRa mesh
set -euo pipefail
MESH_ARGS=(--host 127.0.0.1) # or: MESH_ARGS=(--port /dev/ttyUSB0)
CH_INDEX=1 # dedicated beacon channel — not 0 (the primary channel)
HEIGHT=$(bitcoin-cli getblockcount)
TIP=$(bitcoin-cli getbestblockhash)
FEE_BTC_KVB=$(bitcoin-cli estimatesmartfee 3 | jq -r '.feerate // empty')
if [ -n "$FEE_BTC_KVB" ]; then
# estimatesmartfee reports BTC/kvB; 1 BTC/kvB = 100000 sat/vB
FEE_SATVB=$(awk -v f="$FEE_BTC_KVB" 'BEGIN{printf "%.1f", f * 100000}')
else
FEE_SATVB="n/a"
fi
# Compact snapshot: height, last 12 hex chars of the tip hash, ~3-block fee, UTC time
MSG="BTC ${HEIGHT} tip ..${TIP: -12} fee3 ${FEE_SATVB} sat/vB $(date -u +%H%MZ)"
# Hard guard: TEXT_MESSAGE_APP payloads must fit DATA_PAYLOAD_LEN (233 bytes)
BYTES=$(printf '%s' "$MSG" | wc -c)
if [ "$BYTES" -gt 233 ]; then
echo "beacon message is ${BYTES} bytes (> 233), refusing to send" >&2
exit 1
fi
meshtastic "${MESH_ARGS[@]}" --ch-index "$CH_INDEX" --sendtext "$MSG"
The byte budget
The example message above renders as, for instance:
BTC 909453 tip ..1a2b3c4d5e6f fee3 4.2 sat/vB 1840Z
That is 51 bytes (pure ASCII, so bytes = characters) — under a quarter of the 233-byte
DATA_PAYLOAD_LEN budget. Even a paranoid variant carrying the full 64-character tip hash plus a
full date —
BTC 909453 tip 00000000000000000001c8a4e2f0b7d64a5f3e9b12c07d8f6a4b3e2d1c0f9a8b fee3 4.2 sat/vB 2026-07-22 1840Z
— is 112 bytes, still comfortably inside the limit. The truncated-tail form is usually the right trade: the last hex characters of a block hash are the distinctive ones (the leading characters are zeros by construction), 12 of them are plenty for two humans to compare beacons, and shorter packets spend less airtime. Stick to ASCII — the limit is bytes, and multi-byte UTF-8 characters eat the budget faster than they look.
Schedule it with systemd
A oneshot service plus a timer beats a cron line here: you get logs in journalctl, jitter via
RandomizedDelaySec so co-located beacons don’t collide, and no beacon storm after a reboot.
# /etc/systemd/system/btc-beacon.service
[Unit]
Description=Bitcoin chain-state beacon over Meshtastic
After=network.target
[Service]
Type=oneshot
User=bitcoin
ExecStart=/usr/local/bin/btc-beacon.sh
# /etc/systemd/system/btc-beacon.timer
[Unit]
Description=Run the Bitcoin Meshtastic beacon every 20 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=20min
RandomizedDelaySec=90
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now btc-beacon.timer
systemctl list-timers btc-beacon.timer
Be a polite radio neighbour
- Cadence: think in tens of minutes, not seconds. Bitcoin averages one block every ~10 minutes; a 20–30 minute beacon tracks the chain closely enough for an emergency channel while leaving airtime for humans. LoRa at long-range presets is slow — see the modem preset reference for what one packet costs on air.
- Respect your region’s radio rules. Frequency bands, duty-cycle and power limits differ by region and are enforced in the firmware per region code — we keep the literal per-region table (transcribed from the firmware’s
RadioInterface.cpp) in the LoRa regions and channels dataset rather than restating it here. - Use a secondary channel. A beacon on
--ch-index 0spams every default-config handset in range. A named secondary channel makes the beacon opt-in: subscribers add the channel, everyone else’s radios still relay the packets. - Leave the hop limit at its default of 3 unless your topology genuinely needs more; the firmware caps it at 7 and silently reverts higher values to the default.
Receiving the beacon — and catching up after being off
Receivers need nothing special: any Meshtastic device with the beacon channel configured shows each snapshot as a normal text message — phone app, handset screen, or a headless node you script against. The envelope every beacon packet rides in is documented field-by-field in the MeshPacket & Data reference.
A beacon pairs naturally with a Store & Forward server: because beacons
are plain TEXT_MESSAGE_APP broadcasts, an S&F server on the same channel stores them, and a handset that
was powered off can rejoin and replay recent history (by default up to 25 stored messages from the last 4 hours) —
turning “what did I miss?” into a one-request answer that includes the latest chain state.
What a beacon is not
- Not a wallet, not a transaction relay. It reads three RPCs and writes one text. Moving signed transactions over a mesh — and getting them to a node that can broadcast them — is covered in Bitcoin over Meshtastic.
- Not consensus. A beacon reports one node’s view of the chain. Trust it the way you trust that node’s operator — ideally, run your own beacon and compare. Two independent beacons agreeing on a tip hash is a strong signal; that is why the message includes hash characters and not just a height.
- Not private. Chain state is public data, which is exactly why it is safe to broadcast. Do not extend the beacon to anything wallet- or balance-shaped.
Why this matters for sovereignty
The point of running a node is verifying instead of trusting — but verification you can only reach through a single ISP is verification with a landlord. A LoRa beacon is the cheapest possible off-ramp from that dependency: one node operator with a radio keeps a whole mesh chain-aware through an outage, the same way mesh emergency comms keep people connected when infrastructure fails. It is one layer of the broader D-Central sovereignty stack — Bitcoin and mesh radio are the same fight.
Related
Mesh networking hub · Bitcoin over Meshtastic: transactions off-grid · PortNum registry · MeshPacket & Data field reference · Store & Forward reference · LoRa regions and channels · Solar-powered Meshtastic node build.
Credit where it is due: the mesh layer is the Meshtastic project’s work — firmware, protobufs and the Python CLI (GPL-3.0) — and the node layer is Bitcoin Core (MIT). This page glues their tools together with a shell script; constants quoted as of 2026-07-22.
Related products, repair, and setup paths
- Bitcoiner sovereignty hub
- the plebs sovereign stack
- Nostr for Bitcoiners
- run your own Nostr relay
- getting started with Meshtastic
- Bitcoin over Meshtastic mesh networks
- open-source hardware tools directory
- off-grid Bitcoin mining
Last reviewed July 22, 2026.
