Runbook: Bitcoin Operations (spec 061)¶
The bitcoin module of the relay-gateway proxies public Bitcoin data
(balances/UTXOs, fees, broadcast, tx status, Stamps) behind
/v1/bitcoin/:network/* with quotas, TTL caches, and a killswitch. It is
stateless — no wallet data, no keys — and optional: disabled, the frontend
hides/degrades every Bitcoin surface honestly.
Config (relay-gateway env — see .env.example)¶
| Var | Default | Notes |
|---|---|---|
BTC_ENABLED |
false |
master switch; false ⇒ all routes 503 bitcoin_disabled |
BTC_ESPLORA_URL |
https://mempool.space/api |
mainnet Esplora-compatible upstream |
BTC_ESPLORA_TESTNET_URL |
https://mempool.space/testnet4/api |
testnet4 upstream |
BTC_STAMPS_URL |
unset | Stamps indexer (stampchain.io-compatible); unset ⇒ stamps endpoint degraded: true |
BTC_MAX_FEE_RATE |
500 |
sat/vB clamp on fee responses |
BTC_QUOTA_PER_IP / BTC_QUOTA_GLOBAL |
polymarket parity | read quotas; writes (broadcast) are tighter |
BTC_KILLSWITCH |
false |
ops kill: all routes 503 bitcoin_killed |
BTC_TIMEOUT_MS / BTC_RETRIES |
5000 / 1 |
upstream tuning; broadcasts are never retried |
Boot fails loudly on malformed URLs or nonsensical clamps when
BTC_ENABLED=true — a bad deploy dies visibly, not silently.
Config (SPA build — where the client points)¶
| Var | Default | Notes |
|---|---|---|
VITE_BITCOIN_GATEWAY_URL |
unset | base URL the SPA's Bitcoin client calls; wins over VITE_RELAYER_URL |
VITE_RELAYER_URL |
unset | shared relay-gateway host; used by Bitcoin only as the fallback |
Build-time only (Vite inlines them), so changing either needs a rebuild, not a
restart. Precedence is VITE_BITCOIN_GATEWAY_URL → VITE_RELAYER_URL → unset;
a blank value counts as unset, so a deployment that sets only
VITE_RELAYER_URL is unaffected by this variable existing (issue #1263). Set
the Bitcoin-specific name when a build should reach the Bitcoin proxy without
also turning on the other gateway-backed clients (Predict, Perps, Collect,
Solana RPC, intent relay, bridge quotes).
It does not run the other way. Because blank falls through, there is no
value of VITE_BITCOIN_GATEWAY_URL that leaves Bitcoin unconfigured while
VITE_RELAYER_URL is set. To build with the Bitcoin client dark — which is
what a "gateway unavailable" e2e row needs — leave both names unset.
With neither set the client is unconfigured and every Bitcoin surface says so;
it never renders a zero balance. Note this is a client-side switch only:
pointing a build at a gateway whose BTC_ENABLED is false still gets 503
bitcoin_disabled, and turning the client off does not stop the gateway
serving anyone else.
Playbooks¶
Kill Bitcoin now (incident): set BTC_KILLSWITCH=true and restart (or the
global runtime killswitch if the whole gateway is implicated). Frontend
surfaces degrade to honest unavailable states; member funds are unaffected
(keys are client-side; self-custody continues in any external wallet via the
same standard derivation paths).
Swap upstream (rate-limited / down / self-host): point
BTC_ESPLORA_URL(+_TESTNET_) at any Esplora-compatible API — public
alternatives (blockstream.info) or self-hosted mempool/electrs. No code
change; the client-side fee-dialect fallback handles both mempool.space
fees/recommended and Esplora fee-estimates shapes.
Stamps indexer degraded/down: no action is strictly required — the
frontend fails SAFE (all unverified coins are protected; members see a
degraded-recognition banner and reduced spendable balance, never a lost
Stamp). Restore or swap BTC_STAMPS_URL to clear it. Degraded results cache
only 30s, so recovery is fast.
Fee spikes: quotes are clamped to BTC_MAX_FEE_RATE and expire client-side
after 60s; raise the clamp only deliberately (it caps what members can be
quoted, and the confirmed quote is a hard signing ceiling).
Quota exhaustion (429s): raise BTC_QUOTA_* or lower portfolio poll
pressure; balance lookups batch ≤50 addresses per call and cache 15s, so
sustained 429s usually mean abuse, not organic load.
Testnet4 notes: 'bitcoin-testnet' is testnet4 (testnet3 is deprecated).
Faucets: mempool.space testnet4 faucet. If testnet4 resets or the upstream
drops it, swap the testnet URL (signet is the fallback candidate — one env
change, but note coinType stays 1' so derived addresses are unchanged).
Monitoring¶
- 502
upstream_unavailablerate — upstream health (portfolio shows stale, sends block honestly; prolonged ⇒ swap upstream). - 400
broadcast_rejectedreasons — surfaced verbatim to members; spikes may indicate fee-estimation drift (checkBTC_MAX_FEE_RATEvs mempool). - stamps
degraded: trueratio — indexer health (drives protected-balance UX).