Skip to content

Instantly share code, notes, and snippets.

@melvincarvalho
Created August 11, 2026 11:38
Show Gist options
  • Select an option

  • Save melvincarvalho/fe3154664c64eefb277b9c4556546da9 to your computer and use it in GitHub Desktop.

Select an option

Save melvincarvalho/fe3154664c64eefb277b9c4556546da9 to your computer and use it in GitHub Desktop.
Two websites that never met settled a bet on Bitcoin — the Tidegate/Tavern write-up (testnet4)

Two websites that never met settled a bet on Bitcoin

2026-08-11 · testnet4 · everything below is publicly verifiable

Yesterday a browser game and a dice house — separate sites, separate repos, zero communication between their servers — moved money between themselves correctly. The player carried it. A Bitcoin block rolled the dice. The whole history is signed by one key and notarized on-chain, and you can check every claim in this post yourself.

This is a write-up of the pieces and why the shape matters.

One key is the whole account

Everything runs on a single secp256k1 keypair — the same curve nostr and taproot share. One key is, simultaneously and demonstrably:

role mechanism
identity did:nostr:47779362…1a03 (the x-only pubkey)
authentication NIP-98 signed requests
a Bitcoin address the pubkey is a P2TR witness program: tb1pgamexch…827x
a game account the game (Tideholm) keys players by the did
a state signer every balance move is a Schnorr-signed transition
a transaction signer the anchors below were signed in the browser

No conversion, no derivation paths, no second credential. Funding the address is the entire provisioning step.

The seal: a balance that lives with the identity, not the app

The game lets a player peg gold out of its economy into a sealed balance: a linear chain of signed transitions — {did, prev, delta, next, sig} — the trail. The server stores it but cannot forge it; the signature covers the exact bytes. The player's real balance is wherever the trail says it is, and any app that can verify a Schnorr signature can honor it.

The player's actual trail, as of this writing (six transitions, one day):

[0]    0 +100 →  100   sealed
[1]  100 −100 →    0   pegged back            ⚓ anchor 1
[2]    0 +100 →  100   sealed
[3]  100 +100 →  200   sealed                 ⚓ anchor 2
[4]  200 −100 →  100   staked at the tavern   ⚓ anchor 3
[5]  100 +196 →  296   won — block 147,862 rolled 84

The anchor: the address is the commitment

The trail is anchored to Bitcoin with BlockTrails: each state tweaks the key — tᵢ = SHA256(stateᵢ) mod n, Pᵢ = P₀ + Σtⱼ·G — and spending the output at Pᵢ₋₁ to create Pᵢ is the commitment. No OP_RETURN, no data on chain at all: just addresses, each one a cryptographic consequence of the state history.

Three real anchors, signed non-custodially by a button in the browser:

spends creates tx
1 base (259,951 sat) P(1) + change home 3ae578e4…
2 P(1) (10,000 sat) P(2) c1bb1326…
3 P(2) (9,889 sat) P(3) 9de26665…

Anchors 2 and 3 are the interesting ones: chained advances, where the signing key and destination were reconstructed from nothing but the public key and the trail itself. The recovery story is exactly one line: npub + trail ⇒ every address, forever. Only a small float rides the tweaked chain (~10k sats); the treasury stays at the recognizable base address. Each anchor costs ~111 sats.

The dice: fairness without a trusted party

The tavern is a static gh-pages page — no server. Its classic mode is commit–reveal, which a client-only page famously cannot run honestly (it holds its own seed; its README says so in bold). The Tide mode fixes that with a seed nobody holds:

bet at chain height H, with a player-chosen mark
seed = hash of block H+1        ← does not exist yet; nobody can choose it
roll = 1 + int(sha256(seed | mark)[..13]) mod 100

The entropy doesn't exist at bet time, so there is nothing to commit to and nothing to grind. The mark salts the roll so bets riding the same block get independent dice. The bet in trail entry [5]: stake 100, target "above 50", block 147,862 rolled 84 → payout 196 (1.96×, 2% house edge, priced by a ~230-line pure module with no dependencies, no clock, no network). The bettor holds the mark and can publish it; with it, anyone recomputes the roll from public chain data.

The courier: value between origins with no backchannel

The part we think is genuinely new — or rather, genuinely old (bearer instruments, Chaum, "the browser is the wallet") and finally small:

  1. The game links out: tavern/?did=…&seal=…&return=… — a query string.
  2. The tavern signs stakes and payouts with the player's key (entered once per origin, never leaving the browser) onto a slip — a chained segment of signed transitions in localStorage.
  3. "Settle up" links home: game/?tavern=<base64 slip> — a query string.
  4. The game's single sync endpoint verifies every Schnorr signature server-side, checks the chain (starts at the current balance, never below zero, key must be the did), skips any move already on the trail (idempotent — a slip can be re-presented safely), and applies the rest.

The two servers never exchanged a byte. The player is the transport; the mathematics is the trust. Total integration surface on the game's side: one <a href> and one ~40-line endpoint.

Check it yourself

  • The address is the pubkey: bech32m-encode the x-only key 47779362…1a03 as a v1 witness program, hrp tb → tb1pgamexch…827x.
  • Walk the anchors: each tx above spends the previous trail tip. Derive Pᵢ yourself: state = {"app":"tideholm","did":…,"seq":n,"tip":"sha256:<hash of the canonical trail prefix>"}, tweak, add, encode.
  • Recheck the roll: block 147,862's hash + the bettor's mark → sha256(hash|mark) → first 13 hex chars mod 100, +1 → must be 84. The tavern's verifier does it offline — leave the commitment blank for a block-seed round.

What is honestly not solved

  • Payout authorization. The sync endpoint accepts any self-signed move, so a player could credit themselves — fine for a two-player testnet, disclosed and tracked. The elegant fix is that Tide payouts are provable from public data, so the server can verify the game itself rather than trust a signature. Alternatively apps countersign their payouts.
  • One writer per trail. Linear chains want a single writer; two apps writing concurrently need a rule (or CAS on the coming pod store).
  • Durability. Trails currently live in browsers and one server; the next step moves them to the owner's pod (the store seam is already built), which changes where state lives, not any of the shapes above.
  • Testnet. All of it. That's the point, for now.

The parts

piece what size
tidegate seal, stores, NIP-98, taproot derivation, browser anchorer a handful of small ESM files, gh-pages
tavern wager maths (pure) + static demo/verifier + Tide + sealed mode ~230-line core, 93 tests
blocktrails the reference chained-tweak crypto used as a library
Tideholm the game: one link out, one endpoint in ~2 lines UI + ~40 lines server

No frameworks, no build steps, no dependencies beyond noble/scure crypto. Static pages, one small game server, one keypair, one chain.

Player guide: how to do all of this yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment