Bitcoin Arena

Build a game. Run a market.
Get paid in bitcoin.

Bitcoin Arena is an open engine for games and prediction markets played for real bitcoin. It holds the stakes, checks every result and pays the winners. You bring the game. Tick Tock, Next Block is the first one, and yours can be next.

Test coins only. Bitcoin Arena runs on testnet4 and on private regtest chains today. Mainnet is not live, and nothing here asks for real bitcoin.

Why build here

A game that pays real money usually needs a company: a server, a wallet to hold players' money, a way to stop cheaters and someone to pay out. Bitcoin Arena does all of that for every game on it, so a developer writes only the game.

You never hold players' money

Stakes are locked in a federation of independent nodes (Fedimint). No single node can take them, and neither can you.

Cheating does not pay

Your game's rules run as a deterministic core. Independent nodes replay every run from the player's inputs. A run that does not replay to the same score is not paid.

Payouts are already built

Pick a payout rule in your manifest: the rank curve for games, pari-mutuel for markets. The nodes compute the table, and every node gets the same answer to the millisatoshi.

No server, no approval

A game exists once its developer signs its manifest. Nobody has to approve it. Each node chooses which games it hosts and runs them for you.

Players arrive with a wallet

One seed and one wallet work across every game. A player who has played Tick Tock, Next Block can stake on your game without a new sign-up.

You earn on every paid entry

Half of every player donation goes to the game's developer, on every node that hosts it, for as long as people play.

Who earns what

Every paid entry is split the same way. The rates are fixed in the protocol, the same for everyone. Here is a 10,000 sat stake with the default donation:

90% in play: what the round is won or lost with
5% node fee: to the nodes that ran the round
2.5% to the game developer: half the donation
2.5% to the protocol developers: the other half
5%

Node fee, always

Taken from every paid stake and split three ways: the nodes that replay the runs, the nodes that sign the payouts, and the guardians that hold the money. Free rounds pay no fee.

0–5%

Donation, the player's choice

The default is 5%, and the player can lower it to 0. It is always split half to the game's developer and half to the protocol developers.

The rest

Decides the round

In a game, the best runs win the stakes of the runs that failed. In a market, the right picks win the stakes of the wrong ones.

The incentives, in one line each

Two kinds: games and prediction markets

A game (replayed)

Players play a run, for example a 90-second drive. Your core computes the score from the player's inputs. The nodes replay each run with the same core and sign the score.

Payout: the rank curve. A run that delivers and reaches the minimum score gets its stake back. The best quarter of those runs share the stakes of the runs that failed. Each rank down gets 3/4 of the rank above.

A prediction market (attested)

Players pick one outcome, for example "the bitcoin price closes above 100k". There is no core. The outcome is signed by attestors that you name in the manifest: k of n of their keys, for example 2 of 3.

Payout: pari-mutuel. The stakes of the wrong picks are shared among the right picks, in proportion to what each staked. There is no house and no counterparty.

Your game and its rules are fixed by a manifest, a short signed record. Its hash is the game's id, and every round names the exact manifest it plays, so an update never changes a round already written.

Guide: program a game and sign it

Below, a whole game called coin-count: each tick the player presses a number, and the score is the sum. Real games are bigger, but they have exactly these parts.

  1. Write the core: your rules as pure arithmetic The core is the referee. Nodes on different machines must reach the same result bit for bit, so it may use only integers (or fixed-point), the round's seed and the player's inputs. It must not read a clock, use Math.random, the network or files. Every core has the same small interface:
    // core.js - coin-count
    export const CORE = {
      name: 'coin-count',
      RUN_TICKS: 600,                                   // the longest run a round may ask
      runTicksFits: (t) => Number.isInteger(t) && t >= 1 && t <= 600,
      INPUT_WORDS: 5,                                   // a tick carries five input numbers
      start: (seedWord, config, { runTicks }) => ({ seed: seedWord | 0, sum: 0, ticks: 0, length: runTicks }),
      eventsOf: () => ({ count: { liquidations: 0, loads: 0, samples: 0, tapes: 0 }, before() {} }),
      step(st, words) { if (st.ticks < st.length) { st.sum += words[0] | 0; st.ticks++; } },
      stateHash: (st) => sha256(`coin-count:${st.seed}:${st.sum}:${st.ticks}:${st.length}`),
      result: (st) => ({ outcome: st.ticks < st.length ? null : 'DELIVERED', score: st.sum }),
    };
    The core's id is the SHA-256 of its source file. A config (a set of rule numbers) is named by its hash too.
  2. Write the client: what the player sees Your client is a web page. Put it at games/<game name>/index.html beside the Bitcoin Arena page. The page loads it only if its SHA-256 is listed in your manifest (clients[]). A client changed by one byte is refused and does not run. Your client runs inside the page and gets the engine as window.parent.ARENA_GAME: your game's rounds, the player's identity and join. Stakes, locks, the wallet and the seed stay the engine's, so your client never touches a key.
    const A = window.parent.ARENA_GAME;
    A.rows();          // this game's open rounds: id, line, joinable, entry close
    A.identity();      // the player's name and key fingerprint (the same in every game)
    A.join(roundId);   // join, through the engine's own join and stake
    A.back();          // back to the list of games
  3. Make your donation key: this is where you get paid The dev client arena-donor makes the key that receives your half of the donations. It also collects them from the federation and pays them to your own Lightning node.
    node packages/arena-donor/bin/arena-donor.mjs init ./my-game-dev --label "coin-count dev"
    # prints recipientPk: the key your game's donation set names
  4. Make your developer key One Ed25519 key signs your game and every later version of it. Keep it safe: whoever holds it can publish updates of your game. A node never sees it.
    node packages/arena-donor/bin/arena-donor.mjs game-key ./my-game-dev
    # writes ./my-game-dev/game-dev.pkcs8 (readable only by you) and prints your public key
  5. Write the manifest The manifest names everything a node and a player check: your cores and configs, the longest run, the payout rule, the minimum score, your client files and your donation set. Write it as a small module:
    // body.mjs - coin-count, version 1
    import { RESULT_TYPE, PAYOUT_RULE, NO_PARENT } from './packages/arena-protocol/src/manifest.js';
    import { donationSetId } from './packages/arena-protocol/src/donation.js';
    const hex = (h) => Uint8Array.from(Buffer.from(h, 'hex'));
    export const DONATION_SET = [{ recipientPk: hex('02…your recipientPk…'), label: 'coin-count dev', shareBps: 10000 }];
    export const BODY = {
      gameName: 'coin-count', version: 1, parentManifestId: NO_PARENT,
      resultType: RESULT_TYPE.REPLAY, payoutRule: PAYOUT_RULE.RANK_CURVE, rankNumerator: 3, rankDenominator: 4,
      minScore: 1, runTicksMax: 600, roundPeriodMs: 0n,
      cores: [{ hash: hex('…sha256 of core.js…'), configs: [hex('…sha256 of your config…')] }],
      clients: [hex('…sha256 of games/coin-count/index.html…')],
      gameDonationSetId: donationSetId(DONATION_SET),
    };
  6. Sign it The dev client signs the body with your key, checks the result exactly as a node will, and prints the game id.
    node packages/arena-donor/bin/arena-donor.mjs sign ./my-game-dev body.mjs
    # {"step":"signed","game":"coin-count","version":1,"gameId":"…","file":"./my-game-dev/coin-count-manifest.hex"}
    That id is your game. Change one byte of the manifest and the id changes too.
  7. Hand it to the nodes A node loads a game from a module that exports GAME: the signed manifest, your core with its configs, and your donation set. A node checks all of it at start and refuses a game whose pieces do not match its manifest.
    // coin-count/game.js - what a node operator adds to ARENA_GAMES
    export const GAME = { name: 'coin-count', title: 'Coin Count', manifest, gameId,
      cores: new Map([[CORE_HASH, { name: 'coin-count', hash: CORE_HASH, url: CORE_URL, adapter: CORE,
        configs: new Map([[CONFIG_HASH, {}]]) }]]),
      donationSet: DONATION_SET };
    Then ask node operators to host it. Each operator decides alone, and the game appears in the lobby of every page whose nodes host it.
  8. Test it on your own machine packages/arena-node/tools/games-regtest.mjs starts a private test chain with two nodes that host two games, keeps a free round of each open and serves the page, so you see the game picker, your client and a join. No coins are real there.
  9. Ship updates An update is a new manifest: the same key, version + 1 and the old game id as parentManifestId. Rounds already written keep the manifest they named.

Real stakes need the page's list. A page lets players stake on a replayed game only if the game is on its list of known games, with its cores and configs. That list ships with the page, so a game you have just signed plays for free first. Markets work differently, as the next section shows.

A prediction market instead

A market is the same manifest with resultType: RESULT_TYPE.ATTESTED and payoutRule: PAYOUT_RULE.PARI_MUTUEL. It has no core and no run. Instead it names:

The round settles once the threshold has signed one outcome and the objection window has passed. If two valid reports disagree, the round settles on neither. A market that is not on the page's list can still take stakes: the page shows its developer key and manifest id, and the player must confirm a second time. Bitcoin Arena's own test market btc-close has its own client, loaded by its hash, exactly as described in step 2.

Run a node and earn

A node is a small server program, arena-node. Together, the nodes are Bitcoin Arena: there is no central server. Every paid round is run by a committee of nodes drawn by lot from all bonded nodes, and those nodes share the round's 5% fee.

How a node works

1. BondLocks a bond on the Bitcoin chain and publishes an advertisement: its address, its roles and the games it hosts.
→
2. CensusEvery bond that is old enough counts. Committees are drawn by lot from the census, using the hash of a Bitcoin block.
→
3. WorkLists rounds, takes entries, replays runs, signs results and payouts, and holds the stakes as a guardian.
→
4. PaidIts share of the fee is in the round's settlement, paid by the federation to its key.
RoleWhat the node doesShare of the 5% fee
Game / verifyReplays every run of the round with the game's core and signs the score. It is drawn only from nodes that host that game.one third, split among the seats
SettlementComputes the payout table and signs it, so the federation pays it.one third, split among the seats
GuardianHolds a key of the Fedimint federation that keeps the stakes (for example 5 of 7). one third, split among the round's guardians

An example: in a round with 7 seats in each role and 100,000 sats staked, the fee is 5,000 sats. Each pool gets about 1,667 sats, so one seat earns about 238 sats per role, or about 714 sats for a node that sits in all three. A node earns this in every round it serves, around the clock, and its dashboard (/dashboard) shows what it earned per day, month, year and game.

The bond: what keeps nodes honest

What you need

Start it

# a node hosting Tick Tock, Next Block, on port 31221, with its store in node.db
ARENA_BITCOIND_URL=http://127.0.0.1:48332 \
ARENA_BITCOIND_COOKIE=$HOME/.bitcoin/testnet4/.cookie \
ARENA_BIND=0.0.0.0 \
ARENA_GAMES=packages/nextblock/plugin/nextblock.js \
node packages/arena-node/src/main.js 31221 node.db

The node keeps its key in its store (node.db). Back the store up: it is your node's identity and what its bond is tied to.

What works today, and what comes next

Works todayNot built yet
  • Seven bonded testnet4 nodes with a 5-of-7 federation, running paid test rounds
  • Games and markets as signed manifests; nodes choose what they host
  • The lobby lists the games, and each game loads its own client by hash
  • The 5% fee and the donation split, computed by every node alike
  • The dev client: donation key, collect, withdraw to Lightning, developer key, signing
  • A private two-game test chain for developers
  • Mainnet: not live
  • Publishing a manifest on the Bitcoin chain: today nodes load it from the game's module
  • A tool for a new operator to post a testnet4 bond: the seven bonds were posted by the project
  • Automatic slashing: offences are recorded today, the payout from a bond comes before mainnet
  • A tool for an operator to withdraw node fees: the dashboard shows them
  • A sandbox that isolates a core's process: today a node runs only cores its operator installed, in a worker

Get the code: the Bitcoin Arena source is not public yet. This page will link to it when it is.