I started last night at 9pm asking a search engine which MCP servers could show me live price data for an obscure Bitcoin fork I’ve been poking at. I ended it at midnight with a working self-custody web wallet for that fork, running on 127.0.0.1:8787, keys encrypted in the browser, 166 pinned sighash test vectors all green.
The build itself was fun — Deno server, pure-TypeScript signing core, a tiny web UI, Docker if you want it. That’s not the interesting part. The interesting part is a single byte that exists for exactly one reason: so this wallet cannot accidentally spend somebody’s real Bitcoin.
The problem nobody puts in the README
The fork I’m playing with is a BLAKE2b descendent of Bitcoin. Different proof-of-work, different chain, different coins. But because it forked, it kept the same addresses, the same keys, the same transaction format.
Which means: a transaction you sign for this fork is, byte for byte, a transaction that is valid on Bitcoin too.
If a wallet signs an input the ordinary way and you broadcast it to the wrong network — or an attacker rebroadcasts it on the wrong network — the matching UTXO on the other chain gets spent. People lose real BTC to this exact mistake every time somebody launches a fork and doesn’t think about replay. It’s not theoretical. It’s the first thing that happens.
So any wallet for a Bitcoin fork has one job before all the other jobs: make sure the bytes it signs are not valid on Bitcoin.
The one-byte trick
Here is the fix the upstream project came up with, and the fix I kept when I forked. In Bitcoin, every signature carries a trailing “sighash flag” byte that says which parts of the transaction the signature committed to. Bitcoin accepts a small handful of values. The high bits are reserved — Bitcoin rejects anything in that space.
So the fork invented a new flag — call it SIGHASH_UNIFIED — and put it in those reserved high bits:
export const SIGHASH_ALL = 0x01;
export const SIGHASH_NONE = 0x02;
export const SIGHASH_SINGLE = 0x03;
export const SIGHASH_UNIFIED = 0x20;
export const SIGHASH_ANYONECANPAY = 0x80;
export const SIGHASH_ALL_UNIFIED = SIGHASH_ALL | SIGHASH_UNIFIED;
Every input this wallet signs carries 0x01 | 0x20 = 0x21 as its sighash flag. The digest that gets signed also bakes that flag in, so the signature itself is only valid against that digest with that flag. If anyone takes the signed transaction and tries to broadcast it to Bitcoin, Bitcoin sees 0x21 in the sighash byte, says “I don’t know what that is,” and refuses.
A single byte is the entire wall between the two chains.

The signing core enforces it from the inside, so a buggy client cannot forget. And the server enforces it from the outside:
// POST /api/v1/tx — relay guard, roughly:
for (const input of tx.inputs) {
const sig = input.witness[0];
if (sig[sig.length - 1] !== 0x21) {
throw new BackendRejection("refusing to relay a replayable signature");
}
}
Belt and braces. The wallet will not sign a replayable transaction. The relay will not broadcast one even if a client somewhere produced it. The test suite has 166 pinned vectors from the reference implementation plus a live-chain check that re-derives the digest from already-confirmed signatures on-chain. Green across the board.
The second trick: the backend has to prove which chain it’s on
A wallet that signs locally still has to ask someone about balances, UTXOs, and tip height. The default here is an Esplora/mempool-compatible server, and the fork has a couple of public ones. If a wallet points at the wrong backend by accident — say somebody mistypes a URL and ends up at a Bitcoin mempool instance — the UI happily shows them Bitcoin balances and would gladly build Bitcoin-shaped transactions off that view.
So the server does a one-time handshake with every backend before it trusts anything it says:
export const FORK_CHECKPOINT = {
height: 961_640,
hash: "0000000000000050c1e5f69672f459293be14f46e5a494e7a8c8541396f18eeb",
} as const;
That’s the first block on the fork. Any Esplora-shaped backend that returns a different hash at that height is serving a different chain, gets marked unverified, and is never used again until it changes its mind. A Bitcoin backend fails this check instantly because block 961,640 on Bitcoin has a completely different hash.
Combined, the two guarantees stack:
- The wallet only signs bytes Bitcoin will reject.
- The server only trusts a backend that proves it’s on the fork at a known height.
To spend real Bitcoin by accident, both of those have to fail together.
What I actually built in three hours
Fork-and-ship, in order:
- 9:04 PMLooking for market dataChasing MCP servers that can surface this fork's price. There basically aren't any.
- 11:18 PMForked the MIT walletKept the signing core, replaced the server, API and UI.
- 11:40 PMReplay protection portedSIGHASH_UNIFIED, 166 pinned vectors passing, relay guard enforcing 0x21.
- 12:12 AMFork checkpoint handshakeBackend has to return the expected hash at block 961,640 before it's trusted.
- 12:28 AMDocker, CORS, rate limitsA single container that boots a wallet on 8787. HTTPS and your own backend before you expose it.
The wallet is a Deno server that builds the UI, serves it as static files, exposes a REST API, holds no keys, and refuses to relay anything that could burn you on the wrong chain. Everything key-related — mnemonic, signing, address derivation — runs in the browser.
If you want to run it yourself, it’s a docker build away, and the README has the full configuration surface. If you want to send real money through it, read the “before offering it to the public” section first — public backends see every address you look up, HTTPS matters for the clipboard, and nothing beats a Knots BLAKE2b node you control.
The takeaway worth chewing on
The signing-flag trick is the kind of design decision I love, because it moves safety down a layer. A wallet UI can confuse users. A config file can be wrong. A warning banner can be ignored. But if the signed bytes themselves are invalid on the chain you’re trying to protect, no amount of user error can reach across to it.
If you’re ever building for a forked chain — or anywhere two systems share a wire format but shouldn’t share messages — look for a reserved field, a version byte, a flag the other side already rejects. Put your “do not cross” line there, not in the UI.
A single byte can be a very good fence.