Bitcoin Arena docs
Bitcoin Arena is an engine for games and prediction markets played for bitcoin. It has no central server and no company holding money: a federation of independent nodes takes the stakes, decides every result and pays the winners. This page explains how to play, where your sats are, how that works, how to run a node, and how to build your own game or market on it.
Play
Open the page, make a wallet, deposit test sats and join a round.
Wallet and federations
Where your sats are, how stakes are locked and how you get paid.
The games
Tick Tock, Next Block · Rock Paper Scissors · Bitcoin Up or Down.
Run a node
Download, set it up, bond it, and watch it earn.
Node API reference
Every request a node answers, generated from its source. Try it live.
Build
Write your own game or prediction market.
Repositories
The platform, the apps and the SDK on GitHub, all on one page.
Glossary
- Round
- One match of a game or one market, with a start, an entry close and a stake. Every round has a 32-byte round id.
- Entry
- A player's place in a round, with their stake.
- Node
- One running
arena-nodeserver (also signer node or seat). Its operator chooses the games it hosts. - Bond
- Bitcoin a node locks on chain to get a seat. Its signed advertisement travels in the same transaction. See Bond and advertisement.
- Census
- The set of bonds that are confirmed and old enough, read from the chain. Committees are drawn from it.
- Committee
- The 7 nodes drawn for one round. They run it and sign its results and payouts; 5 of 7 must agree.
- Federation, guardians
- The Fedimint federation that holds the stakes; its guardians are nodes that each hold one of its keys.
- E-cash
- The federation's notes, held in your browser's wallet under your 24 words or your Nostr key.
- Stake lock
- A contract inside the federation that holds a paid entry's stake until the round's result opens it, or its expiry hands it back. See Stake locks and settlement.
- Settlement
- The one transaction in which a round's committee claims the losing locks and pays the winners, the node fee and the donations.
- Manifest
- The developer's signed record of a game or market: its rules, its client files, how it pays out. Its hash is the game id.
- Replayed game
- The result is a run the nodes replay from the player's inputs (Tick Tock, Next Block).
- Attested game, attestors
- A prediction market: the result is an outcome signed by the people or services the developer named, its attestors (Bitcoin Up or Down).
- Head-to-head game
- Each player hides a move, all moves are revealed after the entry close, and the game's rule decides between them (Rock Paper Scissors). A round is played only when all its players entered.
- Template
- A market manifest whose rounds each ask their own question with their own outcome labels. See Event-market templates.
- Leaderboard
- One game's board across its finished rounds over a window of days. See Leaderboard.
- Donation
- The player's optional 0 to 5 % of a paid stake, split between the game's developer and the protocol developers. Collected with a dev client.
- Protocol reserves
- The network's own fund: 2% of every paid round and 20% of every item on protocol 11, held in the federations under each one's 5-of-7 reserve key. Paid out only to proposals the bonded nodes vote for. See Protocol reserves.
- Objection window
- The 10 minutes after a result is signed in which anyone can prove it wrong. See When a node cheats.
Start playing
Bitcoin Arena runs in your browser. There is nothing to install and no account to make.
- Open the testnet page
Go to testnet.bitcoinarena.io. Each game also has its own link:
?game=nextblock,?game=rock-paper-scissorsand?game=btc-updown-15m. The page finds the nodes, reads their rounds and shows them in the lobby. - Make your wallet
On your first visit the page asks how you want your wallet:
- Create a new wallet. It is ready at once. Back up its 24 words later in Settings.
- Restore a wallet from its 24 words (or a 64-hex seed).
- Login with Nostr: a browser extension (NIP-07, for example Alby or nos2x), a phone signer (NIP-46, for example Amber, by QR code), a
bunker://link, or an nsec. You can also create a new Nostr account there. See Nostr.
Keep your 24 words or your Nostr key safe. They are the only way back into your wallet. Nobody, not even the nodes, can recover it for you. - Deposit test sats
Open Deposit, pick an amount and pay the Lightning invoice from any Lightning wallet: scan the QR code, copy it, or pay it from a wallet you connected with Nostr Wallet Connect. On testnet the page links to a test Lightning wallet with a small faucet, run by the project.
- Join a round
The lobby lists the open rounds with their stake, their federation and its load. It also shows how many nodes agree on each round. Joining a paid round locks your stake in the federation. Play starts only when the nodes have verified the lock.
- Get paid
When the round is settled, winnings and refunds go back into your wallet on their own; they show as pending until they are redeemed. Withdraw any time: paste an invoice from your own Lightning wallet, or let a connected NWC wallet make one.
Settings
The gear opens Settings: your balance and history, Deposit, Withdraw and Move, your name in the lobby (up to 32 characters), your donation (0 to 5 %, 5 % by default, for games before protocol 11; on 11 the developers' 4 % is fixed; see Where the money goes), your seed phrase (show it, check it word by word, restore), the federations you confirmed, and a link to the community on Armada.
Your wallet
Your sats are e-cash of a Fedimint federation, held by your browser. The wallet is the Fedimint client itself, compiled to WebAssembly, and its keys come from your 24 words (one wallet per seed and federation). No server keeps your balance.
Deposit
A Lightning invoice made through a federation gateway. The gateway puts in its own e-cash before it learns the preimage, so it cannot keep your payment. If one gateway does not answer within 10 s, the page asks the next. The smallest deposit is 5 sat, and 1 sat paid is 1 sat credited.
Withdraw
Paste a Lightning invoice; it sets the amount. The federation pays it through a gateway, which is paid only against the preimage. With several federations, a withdrawal comes from the one that holds the most.
Balance
Confirmed and pending. The balance lives in the lobby, never inside a game.
Move
Moves your e-cash from one federation to another, for example when yours is heavily loaded.
Federations
A federation holds the stakes. It is a stock Fedimint federation (mint v2, Lightning v2) whose guardians are bonded Arena nodes. On testnet4 there are two, each with 7 guardians that need 5 of 7 to sign:
| Federation | Guardians | Threshold |
|---|---|---|
| Arena testnet4 (A) | 7, one on each of 7 nodes | 5 of 7 |
| Arena testnet4 B | 7, one on each of 7 more nodes | 5 of 7 |
Together their 14 nodes form the testnet4 census. Every game can be paid through either federation. A deposit goes to the open federation with the lower load. A federation that is not on the page's known list is shown with its guardians, and the page asks you to confirm it before it is used.
Load
A federation's load is the bitcoin it holds divided by what its guardians' bonds cover. On mainnet the page refuses deposits into a federation at a load of 1 or more. On testnet4 it only warns (“Load N: this federation holds X sat, more than the Y sat its guardians' bonds cover”) and refuses nothing. The deposit screen shows each federation's funds and free capacity: green below 70 %, yellow from 70 %, red from 90 %. When yours is above 90 % and another is below 50 %, the page offers to move.
Federation status
| Status | What you can do |
|---|---|
| OPEN | everything |
| DRAINING | no deposits; play, withdraw and move still work |
| CRITICAL | withdraw and move only |
| HALTED | everything is frozen |
| CLOSED | its successor holds everything |
The page hides what a status does not allow, and moves e-cash out of a DRAINING or CRITICAL federation on its own.
Stake locks and settlement
A paid entry does not send your sats to anyone. One federation transaction turns your e-cash into a stake lock (and a fee lock beside it). A lock is a Lightning v2 contract inside the federation with exactly three ways out:
- Claim: the round's settlement key takes it, in the round's one settlement transaction, when you lost.
- Forfeit: the committee hands it back to you (paid only to your refund key), with a payout contract for your winnings when you won.
- Refund after expiry: if nobody acts, the lock runs out and your own client takes it back with your key.
The nodes check every field of the lock with the federation before your entry counts (POST /lock, POST /fee-lock). After the round, the committee signs one settlement transaction together (FROST, 5 of 7), which each node has rebuilt itself. There is no pool wallet.
| Result | What happens to your stake |
|---|---|
| Win or qualify | The lock comes back to you, plus a payout contract. Your client redeems both into the wallet. |
| Lose (or forfeit) | The lock is claimed in the settlement transaction. |
| Void (round did not happen, or a market was disputed) | Every lock is handed back at once. |
| Nobody settles | The lock expires and your client refunds it with your own key. |
Winnings can also be sent straight to a Lightning address: a player names one with POST /payout-address, and the seats co-sign the send (GET /winnings shows its state).
The games
Three games run on testnet4 today. All three are paid through both federations and checked by the same nodes.
Tick Tock, Next Block
Code name nextblock · a replayed skill game · play it
Drive a truck through the next Bitcoin block. Every player of a round gets the same block and the same seed; the best run of the round wins the pot.
- Play: load mempool transactions onto your truck, cross the bitcoin price war front (the closer you drive to it, the more points you score) and deliver to the miners' drop-off before time runs out.
- Checked by replay: your inputs go to every node of the round as you play. Each node replays them with the game's deterministic core and signs the score it gets. The battlefield's BTC price is a sample the committee signs every 5 seconds. Each seat reads Coinbase's exchange ticker (the last trade and its time); a reading older than 5 seconds counts as none. A seat signs a proposed price only within 0.05 % of its own reading, so no seat can pick the price. A scheme in which every seat signs its own reading and the median is taken is being built for a later protocol version.
- Payout: a rank curve. The top quarter of the players who qualified share the losers' stakes, with weights 0.75rank−1; qualifiers below them get their stake back.
- Rounds: a new round starts every 90 seconds. The run lasts 82 seconds. You can join until 45 seconds after the start, but a late run still ends with the round. The rounds overlap, so one is always open.
Rock Paper Scissors
Code name rock-paper-scissors · head to head · play it
Rock Paper Scissors for bitcoin. Both players commit a hidden move, then reveal it; the winner takes the pot, a draw returns both stakes.
- Commit and reveal: your pick is sealed as a hash of the round, your key, your move and a secret salt. After the entry close you reveal it (
POST /reveal); the nodes collect each other's reveals and sign the result. - 8-second rounds: rounds close every 8 seconds, back to back, with several tables per slot. A third player gets a second table.
- Results: a win pays your stake plus your opponent's; a draw returns both stakes; a player who does not reveal forfeits; a round without two players is void and refunded.
- Tables on demand: when the page finds no open table it asks the nodes for one (
POST /h2h-want), and the operator publishes a new grid of rounds in one Bitcoin transaction. On testnet this takes a while, so open tables are usually already there.
Bitcoin Up or Down
Code name btc-updown-15m · a prediction market · play it
Will the bitcoin price be higher at the end of the 15-minute window? Pick Up or Down; the attestors sign the price and the winning side shares the pot.
- A market every 15 minutes, 96 a day.
- The rule: the price to beat is the 60-second time-weighted average price before the window starts; the end price is the same average before it ends. Up wins if the end price is at or above the price to beat (a tie is Up).
- Who decides: three independent attestors, each reading several exchanges and taking the median. 2 of the 3 must sign the same outcome, which anyone then posts to the nodes (
POST /outcome). The nodes check it; they do not set the price. - Trading: signed limit orders in whole cents (0.01 to 0.99), at least 5 shares; a share pays 1 sat if its side wins. The nodes clear orders in batches every half second, at one price every node computes the same way.
- Fee: 1 % (0.5 % nodes, 0.5 % donation), due only on what fills. An order that has not filled can be cancelled at no cost (
POST /cancel). - Void: if the attestors contradict each other, or no outcome stands by the deadline, everything is refunded: every stake and every fee.
Leaderboard
A leaderboard is one game's board across its finished rounds: every node can add up the rounds it holds as final over the last 1 to 90 days (7 by default) and rank the players. Ask any node with GET /season?game=nextblock&days=7.
- The game page shows only the players ahead: a player whose net over the window is 0 or below is not listed, so the board is the players who won sats. A node's answer still carries every row.
- Each row is one player: rounds played, wins (the round's sole best delivered score, or a payout above the stake), net (paid out less staked, in msat), best score and when they last played.
- Rows are ordered by net, then wins, then best score, then the player's key, never by who arrived first.
- A result counts once it is final at that node, and an amount once the round's settlement is accepted. Refunds and void rounds are left out.
- A leaderboard computes no payout; it only sums what the settlements paid. Answers are kept 30 seconds.
- A round is held only by the nodes of its committee, so one node's leaderboard misses rounds. Add
&scope=networkfor the whole network's: the node asks every node it knows for its rounds (GET /season-rounds, same game and days) and counts a round only when at least 3 different nodes hand in exactly the same account of it. A finished round is held by at least 5 nodes, so a real one always reaches 3; no single node, nor two, can put a made-up row on the board. The answer says how many nodes were asked and how many answered.
Nostr
Your Nostr key can be your wallet login, your node's owner key and your name in the community, and your Lightning wallet can connect over Nostr. Everything below is built and in use.
| What | How it works | NIPs |
|---|---|---|
| Login with Nostr (players) | Sign in with an nsec, a browser extension, or a remote signer: a bunker:// link, a NIP-05 name, or a nostrconnect:// QR code for a phone signer such as Amber. You can also create a Nostr account in the page. | NIP-07, NIP-46 |
| Your wallet from your key | The wallet's seed is stored as one app-data event (kind 30078, d = bitcoin-arena/seed), encrypted to your own key. Log in on another device and the wallet comes back. With an nsec the seed is derived from the key itself. An existing seed event is never overwritten. | NIP-78, NIP-44 |
| Profile | The page reads your name and picture, and a new account publishes its chosen name. | kind 0 |
| Nostr Wallet Connect | Paste a nostr+walletconnect:// link once. The page shows the wallet's balance, pays deposit invoices and stakes from it, and withdraws winnings into it by asking the wallet for an invoice. | NIP-47 |
| Node owners | The owner key of a node is a Nostr key (BIP-340, x-only secp256k1). In the owner area you sign in with an extension, a remote signer or an nsec. With an extension or remote signer, every owner request is a signed HTTP-auth event (kind 27235) that names the node, the action and a hash of its arguments; the node checks the id, the signature, a ±120 s window and that it was not used before. | NIP-98, NIP-07, NIP-46 |
| The Arena relay | wss://relay.bitcoin-arena.io keeps only profiles, relay lists, encrypted seeds, the nodes' round results and their badges, and passes on remote-signing, auth and wallet-connect messages. Every other kind is refused, and every id and signature is checked. The page also uses relay.damus.io, nos.lol and relay.primal.net. | NIP-01, 11, 42, 46, 47, 58, 65, 78 |
| Results on Nostr | Once a round's settlement is accepted, a node signs one replaceable event with the round's public result (kind 30762, d = arena:result:<round id>, tagged with the game's name and id): the stake, the settlement transaction per federation, a market's outcome, and every entry's name, key, outcome, score or pick, rank and share paid. It is signed with the node's own key, so anyone can check it. A node publishes it to the relays named in ARENA_NOSTR_PUBLISH (off by default, at most 30 a minute); GET /nostr-results?round= always serves the exact event. The Arena relay keeps the newest one per node and round. | NIP-01, NIP-31 |
| Badges | A node can award badges for what a player did in its rounds: First win (a game's first won staked round), Five rounds (five staked rounds of a game), Perfect run (a delivered Tick Tock, Next Block run of 1000 points or more) and Leaderboard top 3 (first, second or third on a game's weekly leaderboard, Monday to Monday UTC, with at least three players). A round counts once its settlement is accepted, read the same way as the leaderboard. The weekly board is the whole network's, not the node's own: 2 hours 15 minutes after the week closes, the node asks every node it knows for that week's rounds and counts a round only when at least 3 different nodes hand in exactly the same account of it, round keys included (as scope=network does), so every node that reads the same rounds awards the same three players. If fewer than 3 nodes answer, the week is not awarded yet and is asked again every 30 minutes, for up to 2 days. Badges go only to a player's identity key (an x-only Nostr key), never to a round key, and each badge is awarded once per player and game, or per game and week. The node signs one Badge Definition per badge (kind 30009) and one Badge Award per player (kind 8: an a tag naming the definition, a p tag the player, and the round ids that earned it, or the week, the place, the number of nodes that answered and the 3 that had to agree). Off by default: ARENA_NOSTR_BADGES=1 turns it on, and the badges are published with the results to ARENA_NOSTR_PUBLISH. GET /badges?player= serves a player's awards with their definitions, GET /badge-definitions the definitions. The Arena relay keeps both kinds. | NIP-58 |
| Community | The Bitcoin Arena community is on Armada, a Nostr chat app; Settings links to it. Join with the same key. |
How Bitcoin Arena works
Decentralized
There is no server to switch off. Any node can host any game, and a committee of nodes decides each round.
Non-custodial
No company, developer or single node holds players' sats. Stakes sit in locks of a federation (Fedimint) and move only as the round's result says.
Checked, not trusted
Game runs are replayed by independent nodes from the player's inputs. A run that does not replay to the same score is not paid.
No approval
A game exists once its developer signs it. Nobody has to accept it; node operators decide what they host.
Cheating is caught
A node that signs a false result leaves a proof anyone can check. The result is overturned, and the proof costs the node its bond. When a node cheats
When a node cheats
Nodes are not trusted. Anyone can check what a node signs, and a false signature is evidence against it. This section shows what happens when a node cheats.
What a node could try
Each of these cheats leaves a proof anyone can check:
| The cheat | The proof |
|---|---|
| Signing two different results for the same player in one round | The two signed results |
| Signing a result that replaying the player's inputs does not give | The inputs, the nodes' receipts for them, and the replay |
| Signing a payout table that is not what the rules give for these entries and results | The table and its inputs |
| Paying out without the required 5 of 7 result and payout signatures | The settlement record |
| Signing for a round its own operator played in | The player's entry and the node's signature |
| Claiming a fee share for work it did not do | The receipt record |
| Agreeing to a round's payout and then not signing it before the locks run out | Its signed agreement, the transaction and the expiry |
Why one node alone cannot do it
Every round has a committee of 7 seats, and a result or payout counts only when 5 of them sign the same one. Up to 9 nodes are drawn per role, so spares take the seats of nodes that do not show up. The draw comes from a Bitcoin block hash, and your app computes the committee itself. A cheating node is one signature of the five needed. To change a result, five nodes would have to lie together.
How it is caught: replay and the objection window
After a result is signed, it waits 10 minutes before it is final. In that time anyone can object: the player, another player, a node that is not in the committee, a spectator or a watchtower. An objection is evidence: the player's inputs, the receipts the committee's nodes signed for them, and the result that replaying them gives. Any node checks it the same way, by running the game again with the round's own rules (POST /objection; tools/check-objection.mjs does the same check outside any node).
- A valid objection stops the result. The whole committee replays the inputs again, and the replay decides. The new result gets its own 10-minute window.
- If the committee cannot get 5 of 7 to agree, the entry is void and the stake goes back to the player.
- An objection that does not check is refused with its reason, and nothing is stored.
The player's app sends the run's inputs to all the round's nodes as it plays, and each node signs a receipt for them. So the evidence is not only on the player's phone.
What evidence is kept
Nodes keep every proof they hold and hand it to anyone who asks (/slash-proofs, /equivocation). A proof has no deadline. An objection that arrives after the 10 minutes can no longer change a final result: the payment goes ahead. It is still a proof against every node that signed the false result.
What happens to the cheater
Every node puts up a bond on Bitcoin to get a seat. The bond has a slash path that only a separate set of arbitration keys can use, and only with a proof:
- The arbitration keys sign a transaction that takes the bond. It names the proof's hash on chain, so anyone can fetch the proof later and check it again. A slash without a valid proof is itself public evidence against the arbitration set.
- Half the bond goes to the players the cheat harmed. Of the other half, 75% goes evenly to the other active nodes (not the round's own seats) and 25% to the developers. Nothing is burned.
- A spent bond is no longer a bond, so the node drops out of the census and out of every future committee. It would need a new bond to come back.
Arbitration touches bonds only. It can never move a player's stake, delay a round or change a result.
What the other nodes and your app do
- Honest nodes replay every run themselves and sign only what their own replay gives. They will not co-sign a false result. They check and store objections, and replay again when one is valid.
- Your app works out the committee and checks every signature itself, and sends your inputs to every node of the round as you play.
The limits
Bitcoin cannot replay a game, so a slash needs the arbitration set to confirm that the proof checks. So you trust the arbitration set, but only a little: it is separate from the committees, cannot touch stakes, and cannot hold a bond forever. In the design, 30 days after a bond confirms its owner can always take it back.
Your money never depends on slashing. Three things protect it even if arbitration failed completely: the 5-of-7 rule, stakes that go back to you by default if nothing is signed, and the objection window. Slashing only punishes a node that was caught.
/slash-owing), and no bond is taken. Mainnet is not live.What a node does
4 % of every paid round goes to the nodes that did the work. Every node checks the others: see When a node cheats.
Details
A node is a small Node.js server. Together the nodes are Bitcoin Arena; there is no other server. A node:
- lists the games it hosts and the rounds it holds;
- takes players' entries and the proof that their stake is locked;
- replays the runs of a replayed game, or checks the attestation of a market;
- signs results and the payout table with the other nodes of the committee;
- as a guardian, holds one key of the federation that keeps the stakes;
- as a liquidity provider, opens incoming Lightning channels to new nodes so they can take deposits.
A node is checked by the others in everything it signs. See When a node cheats.
For that work the nodes of a round share the round's node fee (4% of every paid stake on protocol 11, 5% before it). From protocol 16 it is four pools of 1 % each: the nodes that replay the runs (game / verify), the nodes that sign the payouts (settlement), the federation that holds the money (custody: its guardians ⅔, its two seated gateways ⅓), and the liquidity providers (by weight). Rounds of earlier versions pay three pools of 1⅓ % (no liquidity pool), and a round with no eligible liquidity provider gives that quarter to custody. A node operator gets a share of the fees of every round their node serves, and one node can hold several roles.
Liquidity providers
Details
A new node's gateway can only take deposits through incoming Lightning liquidity, which a new node does not have. A liquidity provider (protocol 16, the fourth service) is a node that opens incoming channels to new nodes: each new node gets a channel of L = 100,000 sat (testnet4) from one or two providers, picked by a fair draw (each provider at most once).
- The claim. The provider's node serves a signed claim at
GET /liquidity-claim: its Lightning node, the channels it gave, and its free coins with one signed proof per coin (the coin's address key signs the claim message: compact ECDSA, P2TR by the BIP-86 tweak; at most 50 coins). A stated balance without a proof counts nothing. - The minimum. Free coins plus the channels it already gave must reach 5 × L on testnet4 (500,000 sat) and 10 × L on mainnet, so giving liquidity never removes a provider's share. That sum is its weight.
- The seating. Each guardian checks the claims and signs the providers it counts per federation and gateway epoch (
GET /liquidity-seating?federation=&epoch=). The liquidity pool (1 % of each round) is split among them by weight. - Topping up. The owner page's Liquidity card shows the free coins, the channels given and the minimum; its New address button asks the node's provider service for a fresh address of its LND wallet (
POST /owner/liquidity/new-address, signed by the owner).
Seats and committees
Drawn by bond, seeded by a Bitcoin block. A node that does not show up is replaced by a reserve.
Details
Every round has a committee: 7 seated nodes, drawn for that round, of which 5 must sign the same result and the same payout. The draw is weighted by bond and seeded by a Bitcoin block: committee_seed = H(census roots ‖ round id ‖ hash of the beacon block), where the beacon block is the first block after the round's committee time. The page computes the committee itself and checks every signature against it. Game and verification seats are drawn only from nodes that host the game.
Up to 9 nodes are drawn, at most 7 are seated, and a round opens only if at least 5 sign that they are present before its start. A node that does not show up is replaced by a reserve. Read the draw of any round from a node: GET /committee, GET /roles.
Bond and advertisement
Details
A node gets a seat by posting a bond on Bitcoin: a Taproot output with three ways to spend it.
- Key path: the node and the arbitration set together (MuSig2), for a cooperative spend.
- Path A: the node alone, after the bond's maturity height (template 2 writes it into the bond: confirmation + 4,320 blocks on mainnet, + 144 on testnet4).
- Path B: k of m arbitration keys, only with a proof of cheating (the slash path; see When a node cheats).
Maturity is not an expiry: a mature, unspent bond still counts. The node's signed advertisement travels in the witness of the bond's funding transaction, and the bond output commits to its hash. It names the node's roles (game_verify, settlement, guardian_capable, each opt-in), its operator, its endpoint, its payout key, the federations it is a guardian of, the games it hosts and the protocol versions it speaks. Every bond also pays a little to one shared marker address, so a light client (the homepage's live games list, for one) finds every node on Bitcoin and checks each proof and signature itself. Changing roles or games is an advertisement update, not a new bond.
The census
Details
The census is the set of bonds that are confirmed and old enough, read from the chain. Every node computes it the same way; the committees are drawn from it. Ask a node for a round's census with GET /census (the evidence a client needs to draw the committee itself), for the stake ceiling that census supports with GET /census-health, and for the bond-weighted medians of what its nodes declare with GET /census-median. Anyone can object to a round's census (POST /census-objection); if the objection holds, the round takes no new entries there. A spent bond leaves the census, and with it every future committee.
Who posts the rounds
One of the two grids is played; a stake never sits in both.
Details
Every day's rounds are posted by the nodes themselves, the day before, by a rule every node computes the same way. Each UTC day has a duty order: the nodes of the census at the day's anchor block, sorted by a hash of the network, the day and each node's key. Any node shows the first three at GET /duty, and the Health page shows who posted each day.
- The primary grid. The first node posts the next day's rounds of every app in every open federation, one transaction per app, from 00:00 UTC. If it has not by 01:00, the second node does; by 02:00, the third.
- The backup grid. The second node posts a second grid of the same day from 02:00 UTC (the third from 03:00 if it has not), never the node that posted the primary. It uses its own coins, an older census block and a later committee time, so one reorg or one bad coin cannot take both.
- One is played. In each round window the primary is played while its committee can be drawn; otherwise the backup. Nodes and pages offer only the played one, and a stake never sits in both.
- When a posting dies (a reorg, a fork the network left, a coin spent elsewhere) or a day cannot be drawn 4 hours before it starts, the node on duty posts an emergency grid for the rest of the day.
- Paying. Your node pays from its Bitcoin Core wallet, confirmed coins first, at 0.1 sat/vB (at most 1 sat/vB if it has to raise the fee). The fee comes back to your node's key from the series' node fee. A node without a wallet, without confirmed coins, or reading the chain through Electrum skips its turn and says why on its owner page; the next node posts instead.
- Turning it off.
duty_post=offinarena.conf. The default,auto, posts while the wallet holds confirmed coins.duty_walletnames the wallet,duty_fee_rateandduty_max_fee_rateset the fees.
Run a node
Your computer checks the games, pays out the winners and earns 4 % of every paid round. Setting it up takes about 15 minutes.
Follow the setup
Open Bitcoin Arena. Your browser shows the setup; each step is checked before it is saved.
Bond 50,000 sat
Free testnet4 coins: the setup can get them for you. The bond is locked for a while, then yours again.
Watch it earn
Your owner page shows what your node does and earns. It updates itself.
The setup, step by step
Every step in detail
- Bitcoin
Your own Bitcoin Core (RPC address and cookie, or user and password), an Electrum server from the built-in list (mempool.space, c3-soft), or Install Bitcoin Core for me. The last one checks your free disk against what mainnet (about 950 GB, 20 GB pruned) and testnet4 (about 25 GB, 6 GB pruned) need and warns when both do not fit, installs Bitcoin Core 31.1 (checked against bitcoincore.org's checksums) as a service, and shows the sync with an ETA. Bookmark that page: a mainnet sync can take days. Setup goes on once testnet4 is synced. Every connection is checked by the genesis block and against a second source.
- Roles
Game / verify (replays a round and signs who won) and settlement (co-signs paying the winners) start ticked. Custody (one of a federation's 10 guardians, maybe one of its 2 gateways) and liquidity provider (opens incoming channels to new nodes) are chosen on purpose: each needs your own Bitcoin Core (LND cannot run on an Electrum server), LND and coins. Each role is one line with what it needs as chips; the details are behind "more". The page shows how the 4 % node fee is split into four pools of 1 %. Custody adds the Lightning, Federation and Bond rules steps.
- Seed
24 words, shown once; three of them are asked back before anything is written. They are your node's wallet and your owner login.
- Owner
Your 24 words or your Nostr key. The node is bound to it before it first starts, so no claim code is ever needed.
- Reach
How others reach your node: direct on port 8470 (two Arena nodes connect back to test it; if they cannot, the wizard says whether your firewall or your router blocks it and what to open), a permanent Cloudflare tunnel on your own free account and domain, Tailscale Funnel (a fixed
*.ts.netname, no domain, no open port), a workers.dev name on your own free Cloudflare account (no domain, no open port; free up to 100,000 requests a day), or a temporary tunnel whose name changes on every restart. The direct test picks the default: direct when it reached your computer; when it did not and your computer is on a tailnet (Tailscale running and logged in), Tailscale Funnel, and the step says your computer is not reachable directly from the internet; otherwise the permanent tunnel. Setup never opens a port on your router and never marks a computer it could not reach as directly reachable. - Install
The node runs as a service (systemd, launchd or a logon task), started again after a reboot if you want, and is checked at its public address. Then you are logged in once as its owner.
- Lightning (custody only)
Set up LND here: LND v0.21.5-beta.rc1, its download checked against the hash its signed manifest names, on testnet4 against your Bitcoin Core, as a service; its wallet is made from your 24 words, and its password, kept on your machine, unlocks it at every start. Or connect your LND (REST address, admin macaroon, tls.cert). Other Lightning nodes reach it at your node's onion address over the bundled Tor when no port 9735 is open (measured by the wizard), so no router setup is needed. Then the gateway: the Fedimint gateway of the release on that LND, its e-cash wallet also from your 24 words; it joins federations by invite code (in the wizard, or later with
./bitcoinarena-install --join-federation <invite>). Channel liquidity is yours to add.Gateway seats (protocol 15). Every federation has two: two of its own guardians host its Lightning. To stand, your node must be one of the federation's guardians and serve a signed claim (
GET /gateway-claim) with at least 100,000 sat inbound and 100,000 sat outbound on its public channels (inbound from outside the federation counted apart), a peg-in of at least 50,000 sat (testnet4; mainnet 250,000 / 250,000 / 2,000,000) and 95 % uptime. Seats are drawn per epoch of 144 blocks (about a day) by ticket from the eligible claims; each guardian signs its seating (GET /gateway-seating) and a seating holds when 7 of the 10 sign the same one. A seated gateway keeps its seat unless a challenger routed 25 % more volume for the federation.GET /gateway-seatson a guardian shows its ranking and why a claim is ineligible. A gateway seat earns one third of the federation's custody pool, shared by the two: on protocol 16 (custody 1 %) about 0.17 % of every round's volume each, against 0.07 % for a guardian of ten (a seat holder is a guardian too: 0.23 % in all; on earlier versions 0.22 % and 0.09 %), plus its routing fees.The guardian, automatically. A custody node runs its federation guardian in setup mode and advertises it at its public address
/guardian. When ten such nodes that guard no OPEN federation are in the census (bonds 6 deep), they found a federation of their own (protocol 14, the cohort): the earliest trigger draws the ten, and they have 18 blocks to hand in their setup codes. A founding that fails (void) is passed over and the next trigger draws again; a node whose federation has no record on the seats yet counts as guarding it, so it is not drawn twice. The node's custody agent then publishes its advertisement update naming the federation, has its gateway join it, and sends the peg-in you set aside in the bond step. A custody node's deposit is therefore the bond, about 5,000 sat for that update, and the peg-in (50,000 sat by default). - Federation (custody only)
No choice to make: your node's guardian seat is drawn, never invited - in a new federation with 9 other new nodes, or in the successor of a federation that shrinks (its remaining guardians first, the missing seats drawn). A full federation takes no new guardian. An invite code only lets your gateway serve a federation (
bitcoinarena-install --join-federation). - Bond rules (custody only)
Why a bond, and that switching off without pressing LEAVE on the owner page slashes it. You type I UNDERSTAND.
- Bond
The wizard shows your node's own wallet address and the amount with its parts: the bond (50,000 sat on testnet4) and about 2,000 for fees; a custody node adds the advertisement update (5,000), the peg-in (50,000) and its gateway's channels (an outside channel of 103,000 and a ring channel of 153,000: 363,000 sat in all); a liquidity provider adds its deposit (500,000). Free testnet4 coins: coinfaucet.eu. Once the deposit arrives it posts the bond and says the lock: the block it unlocks at, how many blocks that is, and an estimated date. You can also post it later from the same address.
Notes for Windows, macOS and Linux
- Windows:
bitcoin-arena-node-setup-<version>-windows-x64.exe. It installs into your user folder (no administrator) and adds Bitcoin Arena to the Start menu and the desktop. Windows SmartScreen warns about an unsigned program: choose “More info”, then “Run anyway”. If Windows Firewall asks whethercloudflaredmay use public and private networks, choose Cancel: your node's tunnel only connects out and works either way. - macOS:
bitcoin-arena-node-<version>-darwin-arm64.dmg(Apple silicon) or-darwin-x64.dmg(Intel). Drag Bitcoin Arena to Applications. The first time, right-click it and choose Open: the app is not signed with an Apple Developer ID yet. - Linux:
bitcoin-arena-node_<version>_amd64.debor_arm64.deb. Install it withsudo apt install ./bitcoin-arena-node_*.deb, then open Bitcoin Arena from your menu or runbitcoin-arena-node-setup. With a desktop andzenityyou get a small window; without one, the terminal shows the link.
Updates are automatic. Your node fetches each new release, checks its signature, backs itself up, and goes back by itself if the new release does not start. To update right away: bitcoinarena update.
More about updates
Auto update (on by default): your node updates itself. A few hours after a newer release is published, the node's Auto update service fetches its own package. It checks the package against the Arena release key and updates in place the same way as by hand: it waits out a market's seating window, backs the node up, and rolls back by itself if the new release does not come up. auto_update=0 in arena.conf turns it off; bitcoinarena update updates now. Releases are numbered 2026.xx.yy, a fix 2026.xx.yy.zz; the protocol a round runs on is a separate number (protocol 1, 2, … 16).
Updates: the owner page's Release card says when a newer release is out (it reads testnet.bitcoinarena.io/release/latest.json every six hours and believes it only when the release key signed it). ./bitcoinarena-install --update <new release> checks the release's signature, backs your node up, moves it to the new release with the same settings, and goes back to the old one by itself if the new one does not come up. It works on Linux (systemd), macOS (launchd; the bundled Tor is signed for the new release) and Windows (logon tasks, or the Startup folder without administrator rights), moves every service of the node, and the settings' paths into older releases with them. A node on a temporary quick tunnel gets a new tunnel name at every restart: the update starts the tunnel first and writes its new name into the node's settings before the node starts, so its public address and guardian endpoint stay right. On Windows it waits up to five minutes for the node to answer (a Windows node starts slower), starts the services so they keep running after the window that ran the update is closed, and if it has to roll back it always starts the old release again, even when a file the node held cannot be restored at once. Several nodes on one machine: start the installer for the second node once with --home <folder> --instance <name> --port-offset <n> (name a-z, 0-9, up to 12; n 0-20000). Its services get their own names (arena-node--<name>), every fixed port (seat, LND, gateway, Tor, liquidity provider, wizard) moves by n, and its command is bitcoinarena-<name>. Both values are written into its arena.conf, so nothing has to be given again. Each node needs its own LND, because a gateway cannot share one. Checking a release yourself: SHA256SUMS.sig is an Ed25519 signature by the key published at testnet.bitcoinarena.io/release-key.txt (ce8d1ee7…c892). With access to the source, the same release is built with tools/release/build-release.sh <version> <linux|darwin|windows> <x64|arm64> out (the same commit gives the same bytes).
Advanced: run a node by hand
You need a computer that is always on with a public HTTPS address (a tunnel works), Node.js 24, the Bitcoin Arena source (bitcoin-arena/bitcoin-arena; every repository on Repositories), and a Bitcoin node (bitcoind) on the same network, testnet4 today, so your node reads the chain itself. For the guardian role you also need fedimintd and a seat in a federation. On testnet4, about 50,000 test sats is the practical floor for a bond.
# start a node on port 31221 with its database in node.db node packages/arena-node/src/main.js 31221 node.db # the same, hosting Tick Tock, Next Block (sets ARENA_APPS to nextblock when it is not set) node packages/nextblock/bin/node.mjs 31221 node.db
The node listens on 127.0.0.1 unless you set ARENA_BIND; put a reverse proxy or a tunnel in front of it. Back up node.db: it holds the node's key. The node is configured by environment variables:
| Setting | What it does |
|---|---|
ARENA_BIND | The address to listen on (default 127.0.0.1). |
ARENA_BITCOIND_URL, ARENA_BITCOIND_COOKIE | Your bitcoind's RPC address and cookie (set both). The network follows this node. |
ARENA_APPS | The games this node hosts: a comma-separated list of modules that each export GAME. Games installed from the owner area live in <db>.plugins (ARENA_PLUGINS_DIR). |
ARENA_SERIES_FILE, ARENA_GRID_SERIES_FILE | The published round series (and grids) this node writes rounds for. |
ARENA_PEERS | The other nodes this node talks to directly. |
ARENA_STAKE_LOCK_BIN, ARENA_FEDERATION_CLIENT_DIR, ARENA_FEDERATION_INVITE | The federation client the node uses to check and settle stake locks, and the federation it joins. |
ARENA_DONATION_NETWORK | testnet4 or regtest: which donation recipients the node uses. |
ARENA_PRICE_FEED=1 | Sign the BTC price samples Tick Tock's battlefield uses. |
ARENA_LND_DIR, ARENA_LND_RPC, ARENA_LND_NETWORK, ARENA_LND_GATEWAYS, ARENA_LNCLI | The Lightning node and gateways for deposits and payouts, when this node runs them. |
ARENA_TLS, ARENA_TLS_CERT, ARENA_TLS_KEY | Serve HTTPS directly instead of behind a proxy. |
ARENA_DEV_CLIENT_DIR | A dev client's directory, shown on the dashboard's dev-client card. |
ARENA_FOUNDING=1 | Take part in foundings: the node's guardian waits in setup mode and is drawn into a new federation (genesis, a successor, or a cohort of ten new nodes). A node that holds no game rounds still sees every federation the chain names. |
ARENA_RECORD_BASIS=chain | Build and check federation records from the census read off this node's own chain first, not from the newest game round it holds. For a node that runs no game series; without it, a node that holds no round at all already falls back to the chain. |
Manage your node: the owner page
Everything about your node is on its owner page: whether it runs, what it earns, its roles, its bond, its uptime, its services and its updates.
From the app
Open Bitcoin Arena on the computer your node runs on and click Owner page.
From any browser
testnet.bitcoinarena.io/owner, with your node's key: the setup opens it for you at the end. Bookmark it; it finds your node wherever it runs.
Sign in
With your 24 words or your Nostr key, once per browser. Nothing leaves your browser: the page signs each request itself.
The public dashboard (technical): what /dashboard shows
Open /dashboard on any node (/dashboard.json is the same as data). It shows what the node earned today, this month and this year (counted as earned once a settlement is accepted, otherwise as owed), a 30-day chart of its fees by role, the rounds it checked with each player's outcome and a replay link, its settlements and federations, the Lightning hosts and gateways it runs, and a dev-client card. Everything comes from the node's own store. The testnet page links the dashboards of its nodes.
The owner area in detail: signing in, each section, how requests are signed
Open /dashboard/owner on your node (the dashboard links it as “Owner area”). There are no passwords and no cookies: every request you make is signed with your owner key, valid for 2 minutes and only once.
- Sign in
Create 24 words, restore them, or log in with Nostr (extension, remote signer or nsec). The owner key is a Nostr key; see Nostr.
- Claim your node
A node without an owner prints a claim code (
XXXX-XXXX-XXXX) in its log and writes it to<db>.owner-claim. Enter it once; from then on only your key can act for the node (POST /owner/claim). - Run it
- This node: its key, whether it is a guardian, the games it hosts.
- Services: every service of the node, the same view as
bitcoinarena statuson its machine. For each one: running or stopped, and what it answers (LND synced, the gateway's federations, the public address reaching this node, the guardian's API). Check again reads them anew (POST /owner/services, signed). - Standing: its seats in recent censuses, and its bond's margin over the floor (
/owner/standing,/owner/seatings). - Bond: the bond the census names for your node, and its lock in words: the block it unlocks at, the blocks to it and an estimated date. From that block on, Withdraw sends the whole bond to an address you name; your node leaves the census from the next census height (
/owner/bond,/owner/bond-withdraw). A larger bond, once unlocked, in one step:./bitcoinarena-install --rebond <sat>on the node's machine shows the plan (the old bond plus the wallet's coins become one new bond with a new lock),--yesposts it. - Leave or stay: leaving stops new rounds; the node serves out the rounds it holds, and a guardian posts a departure notice to every seat. Leaving is never a slash (
/owner/leave,/owner/stay). - Donations: what the seat verified for the protocol and game donation sets (
/owner/donations). - Games from other seats: install, update or remove a game from another seat's address. It is checked in your browser, checked again by the seat, tried in a sandbox, and goes live without a restart. Auto-update and the source seat are settings (
/owner/plugins/*,/owner/settings). - Networks: the chain, donation sets, series and federation invite a node runs with; install, update or remove (
/owner/network/*). - Sign a game or market: your developer key comes from the same 24 words, so you can sign a manifest right there.
Through the API only: the node's half of a cooperative bond spend with the arbitration set (/owner/musig/nonce, /owner/musig/partial), and operator tokens (/owner/operator/mint|list|revoke) for /operator/status and /operator/vote, which hands back a vote payload that you broadcast yourself. The node never moves money for a vote.
Dev clients and donations
Donations are paid in the round's settlement as payout contracts to the developers' keys. A developer collects them with the dev client, packages/arena-donor:
arena-donor init # make the developer's keys arena-donor once # collect once arena-donor loop # collect every few minutes arena-donor status # what is waiting arena-donor game-key # the Ed25519 game developer key arena-donor sign <dir> # sign a game's manifest
It asks every seat what it verified for its key (GET /donations?key=), redeems the payout contracts in each federation with the same WebAssembly client the page uses, run in Node, and withdraws to the developer's own Lightning node. It then tells the seats what it read (POST /donations/read, signed by the key). On testnet4 the donation key set names two dev clients (dev a and dev b), split 2:1, which run as services that collect every 5 minutes.
From the owner area. A node's owner can also run dev clients on the node itself: set one up with a new key or an imported developer key (an imported key's wallets are recovered from the federation), see its donations split into the protocol developers' part and each game's part, withdraw to a Lightning invoice or Lightning address, or remove it (its directory and key are kept). The node restarts a client that stops, and refuses a second client on one key. These are owner-signed requests: GET /owner/dev, POST /owner/dev/setup, /owner/dev/withdraw and /owner/dev/remove. The public dashboard lists every dev client with the protocol/game split.
The node API
Every node answers the same HTTP requests at its own address, for example https://tn4.bitcoin-arena.io/guardian-1/games. The full reference is a page of its own: the node API reference, an OpenAPI 3 document generated from the node's source (openapi.json) and shown with Swagger UI. You can try the read-only requests live against the public testnet4 nodes there.
- Requests are open to any page (CORS is open). The few that change something need a signature (a player's, a node's, an owner's) or an operator token; the reference marks them.
- Round ids, entry ids and keys are hex strings in the query:
?round=<64 hex>. - Answers are JSON, or binary when they are a protocol record (a round contract, a manifest, an attestation). Binary records are encoded and decoded with
packages/arena-protocol. - Ask several nodes, not one: every node of the committee answers for itself, and the page compares them.
For game and market builders
List your game, read its rounds, follow results, report a market's outcome.
For pages and players
The lobby, joining, stake locks, winnings, leaderboards and results.
For operators
The dashboard, the owner area, the census and the node's health.
Between nodes
Signing ceremonies, shares and gossip between the nodes of a committee.
Build and earn
A game played for 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, 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. Every node computes the same table 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.
Players already have a wallet
One seed and one wallet work across every game, so a player of one game can stake on yours without a new sign-up.
You earn on every paid entry
2% of every paid stake on protocol 11 (half the player's donation before it) goes to the game's developer, on every node that hosts it, for as long as the game is played.
There are two kinds: games, whose runs the nodes replay, and prediction markets, whose outcome named attestors sign. Start here:
Build a game
Rules, client, donation key and a signed manifest.
A head-to-head game
Hidden moves, revealed together, like Rock Paper Scissors.
Build a market
Outcomes, attestors and a pari-mutuel payout.
Run a node and earn
A bond, a seat and 4% of every paid stake for the work (5% before protocol 11).
Run a devnet
A chain, a federation and seven nodes on your own machine, in one command.
Run a devnet
A devnet is the whole network on your own machine: a private Bitcoin chain (regtest), a Fedimint federation of seven guardians (5 of 7), seven Arena nodes that are each one guardian, two Lightning gateways, and a free and a paid round of Tick Tock open in the lobby. One command starts it, one stops it. Nothing in it touches testnet4 or mainnet, and it needs no test coins: it mines its own.
What you need: Node.js 24 with the repository's packages installed; Bitcoin Core (bitcoind, bitcoin-cli); the Fedimint build the network runs (fedimintd, fedimint-cli, gatewayd, gateway-cli) with arena-stake-lock built beside it (packages/arena-node/tools/stake-lock); and lnd with lncli for the two gateways. Without lnd the devnet runs free rounds only and says so.
export BITCOIN_BIN=<dir with bitcoind> FEDIMINT_BIN=<dir with fedimintd and arena-stake-lock> LND_BIN=<dir with lnd> node packages/arena-node/tools/devnet.mjs up # start everything, print the links node packages/arena-node/tools/devnet.mjs status # what runs, its ports, the chain tip, the rounds node packages/arena-node/tools/devnet.mjs play # one player through a free round, to its signed result node packages/arena-node/tools/devnet.mjs replay # that run replayed here, checked against every node's result node packages/arena-node/tools/devnet.mjs down # stop everything it started
- up
Starts the chain and mines it to maturity, founds the federation (each node's guardian takes part in the key ceremony), starts the seven nodes, joins a federation client for each, starts two lnds with a gateway each and registers both at every guardian, then bonds the seven nodes on the chain and publishes a free round and a paid round. It prints the node addresses, the federation invite, both round ids and a lobby link. Measured on a macOS: 75 s from an empty directory, 32 s after a
down(the federation and the bonds are kept), 6 s when everything already runs. - status
Each process and whether it still runs, each node and its guardian, the chain's height, every port that answers, and the rounds node 1 lists.
- play
Opens a fresh free round, joins it as a new player, plays the run with the round's own core (a driver steers the truck), sends the inputs and signed marks to all seven nodes as the page does, waits for the round to close, and prints each node's signed result with its signature checked: outcome, score and transcript hash, how many of the seven agree, and whether that matches the run it played. Measured: 229 s for a Tick Tock round (an 82 s run), 157 s for the template's 10 s game, every one of the seven nodes signing the same result.
- replay
[round entry]The replay simulator: fetches a run's inputs and signed marks from a node, replays them here with the node's own replay code in the core's sandbox, and compares the outcome, score and transcript hash with every node's signed result. With no arguments it replays the last
play. - down
Stops exactly what
upstarted, by the process ids it wrote (each in its own process group, so a node's guardian goes with it), and checks that none of its ports still answers. The directory is kept, andupstarts on it again.
| Setting | What it does |
|---|---|
DEVNET_DIR | Where everything lives: chain, node stores and guardians, clients, logs, process ids (default ./devnet-data). |
DEVNET_BASE_PORT | Every port is derived from it (default 24000): bitcoind RPC base+1, the runner's control door base+5, lnds base+10 and base+20, gateways base+30 and base+40, the seven nodes base+100 to base+160 in steps of 10, and each node's guardian at node+1000 to node+4000. All must stay below 32768. up refuses a port another program holds. |
DEVNET_APPS | More games for the nodes to host, as modules that export GAME (see below). Kept for every later command. |
DEVNET_PAID_STAKE_MSAT | The paid round's stake (default 10,000,000 msat). |
DEVNET_BLOCK_SECONDS | A block every N seconds once the rounds are up (default 30; 0 for none). |
DEVNET_PAGE, DEVNET_PAGE_DIR | The page the printed links open, or a built page directory the devnet serves on base+3. |
Your own game on the devnet
The game template is a whole replayed game called coin-count: each tick the player presses a number from 0 to 9, some ticks count three times, and the score is the sum. One command copies it into a directory of yours, makes your developer keys with the dev client and signs its manifest:
node packages/arena-node/tools/devnet.mjs new-game ./my-game my-game
# {"step":"new game","name":"my-game","gameId":"…","core":"…","hostedAs":true,…}
DEVNET_DIR=./my-devnet DEVNET_APPS=./my-game/game.js node packages/arena-node/tools/devnet.mjs up
node packages/arena-node/tools/devnet.mjs play --game my-game
node packages/arena-node/tools/devnet.mjs replay
Three files are yours to change:
core.js, the rules: pure integer arithmetic over the seed and the inputs, run by every node in a sandbox with no network, files, clock or randomness. Its id is the SHA-256 of the file.body.mjs, the manifest: the core and its config by hash, the run length (runTicksMax), the payout rule, your client pages and your donation set. Sign it again after any change:node packages/arena-donor/bin/arena-donor.mjs sign ./my-game/dev ./my-game/body.mjs. The signed manifest's hash (gameIdOf) is your game id; a new version names the old id as its parent.game.js, what a node loads: the signed manifest, the core and its configs, and your donation set. A node checks all of it at start and refuses a game that does not match.
up of a devnet directory. Each node's bond lists the games it hosts and is posted once, so a game added later is hosted but no committee is drawn for its rounds: start it on a new DEVNET_DIR. Not built yet: the devnet's gateways have no channels and no e-cash, so the paid round is published and listed but a deposit into it does not go through; play drives free rounds only; and the printed browser link was not tested.Implement a game
A game is four things: its rules (a small core every node can run and get the same result), its client (the web page the player sees), its donation key (where your share of the donations goes) and its manifest (your signed record of all of it). The example below is a whole game called coin-count: each tick the player presses a number, and the score is the sum.
- Write the rules as pure arithmetic
Nodes on different machines must reach the same score bit for bit. So the core may use only integers (or fixed-point), the round's seed and the player's inputs: no clock, no
Math.random, no network, no 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 file. A config (a set of rule numbers) is named by its hash too.
- Write the client: what the player sees
Your client is a web page at
games/<game name>/index.htmlbeside the Bitcoin Arena page. The page runs it only if its SHA-256 is in your manifest, so a client changed by one byte is refused. It gets the engine aswindow.parent.ARENA_GAME; 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
- Make your donation key: where your share is paid
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
The same tool later collects your donations from the federation and pays them to your own Lightning node.
- Make your developer key
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
This Ed25519 key signs your game and every later version of it. Keep it safe; a node never sees it.
- Write the manifest
// 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), }; - Sign it
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.
- Hand it to the nodes
A node loads a game from a module that exports
GAME, listed in itsARENA_APPS. It checks every piece against the manifest at start and refuses a game that does not match.// coin-count/game.js 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 decides alone; the game appears in the lobby of every page whose nodes host it. Check it with
GET /gameson their nodes. - Test on your own machine
devnet.mjs new-gamemakes this whole game from the template, signed with your own keys, and a devnet hosts it on seven nodes and plays it end to end.packages/arena-node/tools/games-regtest.mjsserves a built page beside two nodes, so you see the game picker and your client. - Ship updates
An update is a new manifest with the same key,
version+ 1 and the old game id asparentManifestId. Rounds already written keep the manifest they named.
Payout rules: the rank curve or a top-N split
A replayed game's manifest picks how its winners are paid:
- The rank curve (the default, Tick Tock's): the top quarter of the qualifiers share the losers' stakes with weights 0.75rank−1, and qualifiers below them get their stake back.
- A top-N split (
PAYOUT_RULE.TOP_SPLIT): fixed shares for places 1 to N, 1 to 10 shares in basis points that never increase and sum to 10,000.[10000]is winner takes all,[5000, 3000, 2000]pays three places. Places are ranked by score, then by key; each place is weighted by its share and its own net stake. With fewer qualifiers than shares, the unused shares go to the places filled in proportion (two players under 50/30/20 get 62.5 % and 37.5 %); nothing is left over for anyone else.
A manifest with a top-N split must require protocol version protocol 4, so a node that cannot pay it is never seated for its rounds. Existing manifests keep their bytes and game ids.
Implement a head-to-head game
In a replayed game every run is checked on its own, so one player's score can never depend on what another player did. A head-to-head game is for the games where it does: each player picks a move in secret, all moves are shown together, and the game's rule decides who won. The engine hides the moves, reveals them, checks them and moves the money; your game only says which move beats which. The example is the real Rock Paper Scissors (packages/rock-paper-scissors).
| Result type | The result comes from | Example |
|---|---|---|
REPLAY (1) | each player's own run, replayed by every node | Tick Tock, Next Block |
ATTESTED (2) | an outcome signed by the attestors the game names | a prediction market |
HEAD_TO_HEAD (3) | all players' revealed moves together, decided by the game's rule | Rock Paper Scissors |
A round, step by step
- Hide. Before entry close the player enters with a commitment instead of a move:
H(round id ‖ session key ‖ move ‖ salt), with a fresh 32-byte salt. Nobody can read the move from it, and nobody can copy it: it names the entry's own session key. - Wait for the other players. A round seats exactly the game's
players. A further entry is refused ("that round already seats … 2 players"). - Reveal. Between entry close and the round's end, each player sends its move and salt to every node (
POST /reveal). A node takes it only if it hashes to that entry's commitment. - Decide. At the round's end every node pulls the other nodes' reveals (
GET /reveals), hands all the moves to the game's rule, and signs each player's result, exactly as it signs a replayed run. Same moves, same rule, so every node signs the same result. - Pay. The result goes through the normal settlement, unchanged:
| What happened | Result the nodes sign | Money |
|---|---|---|
| You won | DELIVERED, score 2 | your stake back plus the losers' stakes |
| A draw | DELIVERED, score 1 | everyone keeps their own stake |
| You lost | FAILED | your stake goes to the winner |
| You did not reveal | FORFEIT | your stake goes to the winner (a lone revealer wins) |
| Fewer players than the game needs entered | VOID for everyone | not played: every stake comes back |
Build it
- Write the rule
One function: it gets the moves of the players who revealed and says WIN, DRAW or LOSS for each. Like a core it must be pure: same moves, same answer, on every machine.
// plugin/decide.js - Rock Paper Scissors: 0 rock, 1 paper, 2 scissors export const DECIDE = { name: 'rock-paper-scissors decide v1', decide(moves) { // [{ key, move }] -> { [key]: 'WIN' | 'DRAW' | 'LOSS' } const shown = [...new Set(moves.map((m) => m.move))]; const out = {}; if (shown.length !== 2) { for (const m of moves) out[m.key] = 'DRAW'; return out; } const beats = (a, b) => (a - b + 3) % 3 === 1; const winning = beats(shown[0], shown[1]) ? shown[0] : shown[1]; for (const m of moves) out[m.key] = m.move === winning ? 'WIN' : 'LOSS'; return out; }, };Its id is the SHA-256 of its file, and the manifest names it in
cores[], so a node installs and checks it just like a replay core. - Write the manifest
export const BODY = { gameName: 'rock-paper-scissors', version: 1, parentManifestId: NO_PARENT, resultType: RESULT_TYPE.HEAD_TO_HEAD, cores: [{ hash: sha256(decide.js), configs: [sha256('rock-paper-scissors-config:v1')] }], runTicksMax: 0, // no run payoutRule: PAYOUT_RULE.RANK_CURVE, rankNumerator: 3, rankDenominator: 4, minScore: 2, // a win scores 2, a draw 1 players: 2, // 2 to 16: a round is played only with all of them moveCount: 3, // 2 to 32 moves revealWindowMs: 120000n, // 10 s to 1 h: the longest a round may give for reveals roundPeriodMs: 0n, clients: [sha256(games/rock-paper-scissors/index.html)], gameDonationSetId, };Sign it and hand it to the nodes exactly as in Implement a game (
arena-donor sign, a module exportingGAME, listed inARENA_APPS). - Write the client
Besides
rows,identityandback, a head-to-head client gets these fromwindow.parent.ARENA_GAME. The salt, the stake and the keys stay the engine's.const A = window.parent.ARENA_GAME; A.headToHead; // { players, moveCount } - or null for another kind of game A.hide(roundId, move); // enter the round with this move hidden A.hidden(roundId); // this tab's hidden move in that round (kept in the tab until revealed) A.hiddenAll(); // every move this tab hid in this game - a round leaves the rows at its entry close A.reveal(roundId); // after entry close: send the move and salt to every node A.reveals(roundId); // the moves the nodes hold: { [entryId]: move } A.result(roundId); // the round as the nodes hold it: entries, each signed outcome and how many nodes signed itShow the players
A.result, the signed result, not a verdict your page worked out itself. Rock Paper Scissors' client shows "waiting for an opponent (1 of 2)" before entry close, reveals at entry close, and then "You win - signed by 2 nodes". - Keep it apart from other games
A page shows one game's rounds at a time: on nodes that host Tick Tock, Next Block and Rock Paper Scissors, the truck's lobby lists only its own rounds, and Rock Paper Scissors loads none of the truck.
- Try it on your own machine
ARENA_PAGE=<a page with games/rock-paper-scissors/index.html> node packages/rock-paper-scissors/tools/demo-regtest.mjs ./work # a private test chain, two nodes hosting both games, one free round always open; prints the lobby link
Open the link twice in two browser profiles (two players) and add
&game=rock-paper-scissors.
Implement a prediction market
A market is a round where players pick one outcome and the right picks share the stakes of the wrong ones. It uses the same custody, nodes and settlement as a game, but there is no core and no run: the result is the outcome reported by the attestors you name.
- Choose the question, the outcomes and who reports the result
2 to 16 outcomes, for example
['up', 'down']. Then 1 to 16 attestors: Ed25519 keys of the people or services that report the outcome (for example three price-feed operators), and how many of them must sign the same outcome (for example 2 of 3). Players see who decides before they stake. - Write the manifest
The same as a game's, with these differences:
export const BODY = { gameName: 'btc-up-down', version: 1, parentManifestId: NO_PARENT, resultType: RESULT_TYPE.ATTESTED, // the result is an outcome, not a replayed run payoutRule: PAYOUT_RULE.PARI_MUTUEL, // the right picks share the wrong picks' stakes outcomes: ['up', 'down'], attestors: [KEY_A, KEY_B, KEY_C], // 32-byte Ed25519 public keys attestThreshold: 2, // 2 of the 3 must sign the same outcome cores: [], runTicksMax: 0, // nothing to run clients: [hex('…sha256 of games/btc-up-down/index.html…')], gameDonationSetId: donationSetId(DONATION_SET), // … roundPeriodMs and the rest as for a game };Sign it and hand it to the nodes exactly as for a game (steps 4, 6 and 7 above). Your client shows the outcomes; the page's own pick sheet takes the stake and the pick.
- Players pick and stake
An entry into a market round carries a pick, the index of one outcome. Nodes refuse a pick that is out of range, and any pick once they hold a valid result for that round.
- Report the outcome
After the entry close, your attestors sign the round's outcome and anyone posts it to the nodes. What they sign is the id of
{ round id, outcome index, observed time in ms }:import { createPrivateKey, sign } from 'node:crypto'; import { attestationBodyId, encodeAttestation } from './packages/arena-protocol/src/attested.js'; const body = { roundId, outcome: 0, observedAtMs: BigInt(Date.now()) }; // 0 = 'up' const digest = Buffer.from(attestationBodyId(body)); const signatures = [attestorA, attestorB].map((a) => ({ key: a.publicKey, // 32 bytes signature: new Uint8Array(sign(null, digest, a.privateKey)), // Ed25519, 64 bytes })); const attestation = encodeAttestation({ ...body, signatures }); for (const node of committeeNodes) await fetch(`${node}/outcome?round=${roundIdHex}`, { method: 'POST', body: attestation });Each node checks that the attestation is for this round, that the outcome exists, and that enough of the named attestors signed it. It answers why when it refuses.
- Follow it
GET /outcome?round=…answers the round's state:OPEN(no result yet),ATTESTED(one outcome, with the payout table) orDISPUTED. The round settles once a result has stood for the objection window (600 s) with nothing contradicting it. - If attestors contradict themselves
Two valid attestations of different outcomes for one round are the proof that the attestors disagreed. The round is
DISPUTEDand pays out on neither outcome; the stakes go back to their players by the refund path.
How a market pays out (pari-mutuel)
- A market's fees are its own, set in its manifest: 0.5% to the nodes and 0.5% donation by default, at most 5% each and 10% together. Its net stake in the pool is the stake less those fees: a 1 sat pick at 1% stakes 0.99 sat on the odds.
- The fees are due when an order fills, for winners and losers alike; a pick fills at the round's entry close. Before that, an order can be cancelled at no cost: the player signs
POST /cancel, and its stake and fee locks come back at once. The fees sit in the fee lock until the market resolves; then the nodes take their part and the donation is paid to the protocol developers' and the market developer's keys. - Every entry that picked the winning outcome gets its net stake back plus its share of the losing pool:
losing pool × its net stake ÷ all winning net stakes, rounded down; the millisats left over go one each to the winners. - If nobody picked the winning outcome, or the market is void, everything goes back: every stake and every fee. A market is void if its attestors contradict each other, or if no outcome stands by its deadline.
- There is no house and no counterparty: the wrong picks pay the right ones.
Bitcoin Up or Down, every 15 minutes
The first market follows Polymarket's BTC Up or Down 15m. There is one market per quarter hour with two outcomes, Up and Down. Up wins if the time-weighted average BTC price over the window is at or above the price at its start (the price to beat); a tie is Up. The attestors apply that rule to the prices they read (upDownOutcome in packages/arena-protocol/src/up-down.js) and sign the outcome.
Trading uses signed limit orders. Prices are in whole cents (0.01 to 0.99), an order buys at least 5 shares, and a share pays 1 sat if its side wins. An Up buyer at 0.73 and a Down buyer at 0.27 together fund a share. The nodes clear each batch of orders at one price that every node computes the same way (clearBatch in order-book.js). The fee is charged on the part of an order that filled, when it fills; what has not filled can be cancelled at no cost.
The example market btc-close (three outcomes, three attestors, 2 of 3) lives in packages/arena-node/test/plugins/market/ with its own client, and packages/arena-client/tools/market-live.sh runs it on a private test chain end to end.
Event-market templates
A market manifest can be a template: one signed set of rules (attestors, threshold, fees, deadline) whose rounds each ask their own question. The template names a category, the most outcomes a round may have (2 to 16) and how long after the entry close a round must resolve. Each round's contract then carries its event: the question (1 to 280 bytes), 2 to 16 unique outcome labels (1 to 64 bytes each) and when it resolves.
- A template must name its market terms, require protocol version
protocol 3or later, and run on no grid. Rounds are written one by one. - Nodes refuse a template round without an event, an event on any other game's round, too many labels, or a resolution time outside the template's window.
- The attestors sign an outcome index into the round's own labels.
GET /roundsrows carry theevent;GET /roundnames it in thex-question,x-outcomesandx-resolve-at-msheaders;GET /outcomeanswers with the question and labels. The page shows the question and labels from the contract it checked.
Markets on games
A market can be on a game round: spectators back BULLS or BEARS of one Tick Tock round, and that round's own result, signed by 5 of its 7 nodes, decides the market. No attestor and no outside source. Built in protocol version protocol 6; it runs once nodes advertise that version.
- The manifest names the source game, the kind of result it reads (a Tick Tock round's result: BULLS, BEARS or FLAT) and which outcome each result pays, or a refund. It names no attestors, and no attestation is taken for it.
- Each round of the market names the one source round it is on. Its entries must close before that round starts, and the source round must end by the market's resolution time; otherwise the market is void and every stake goes back.
- FLAT (or any result the manifest maps to a refund) gives every stake back at once; so does no signed result by the deadline.
- The nodes of a market round fetch the source round's signed result themselves once it has ended, check it against the committee they draw for it, and keep it as the market's outcome: the market then settles like any other (pari-mutuel), after the objection window. A node that does not hold the source round fetches its contract from other nodes and checks it against the round's id. Anybody can also hand a node the result at
POST /game-outcome;GET /outcomeshows it with its signatures. Rock Paper Scissors is not a source yet: it signs each player's result, not a table winner.
Scalar markets
A market can ask for a number - a price, a count - and pay by where it lands between two bounds, instead of one outcome per bucket. Built in protocol version protocol 7; it runs once nodes advertise that version.
- The manifest names the bounds
low<high(whole numbers, may be negative) and their unit, and its two outcomes are the sides LONG and SHORT. A player picks a side. - The attestors (k of n, as for any market) sign the value they observed in a value attestation of its own kind - an outcome attestation is refused for a scalar market, and a value attestation for any other. Hand it in at
POST /outcome, as an outcome attestation. - The value is clamped to the bounds. LONG's share of the whole pool (the net stakes of both sides) is
(value - low) ÷ (high - low), SHORT's the rest; each side's share is split over its own players by their net stakes. Every amount is whole millisats, rounded down, the leftovers to named places; nothing is kept back. - If only one side has players there is no counterparty, and every stake goes back, its fee too. No value by the deadline, or two different values signed, makes the market void: every stake back.
Where the money goes
Every paid entry is split by the protocol version the game requires. Versions run side by side: a round keeps the split of its own version.
| Part of a paid stake | Protocol 11 (reserves) | Before 11 | Goes to |
|---|---|---|---|
| In play | 90% | 90% or more | the round: what is won or lost |
| Node fee | 4% | 5% | the nodes that ran the round: from protocol 16 four pools of 1 % (game / verify, settlement, custody, liquidity providers); before, replay, settlement and guardians a third each |
| Game developer | 2% | 2.5% donation | the game's developer |
| Protocol developers | 2% | 2.5% donation | the protocol developers |
| Protocol reserves | 2% | - | the network's own fund (see Protocol reserves) |
On protocol 11 the whole split is fixed: the developers' 4% is no longer a donation the player lowers. Before 11 the developers' 5% is a donation, 5% by default, down to zero. A 100 sat entry on protocol 11 pays the nodes 4 sat, the developers 2 + 2 sat, the reserves 2 sat, and puts 90 sat in play. A prediction market charges its own manifest's fees instead: 1% by default (0.5% nodes, 0.5% donation), due when it resolves (see Implement a prediction market).
Items (an add-on bought in a game, such as Tick Tock's larger truck) on protocol 11: 40% of the list price to the nodes and 20% to the protocol reserves, both fixed, plus the developers' share the player chooses (40, 30, 20, 10 or 0%, half game, half protocol). The player pays 60% plus that share: a 500 sat item costs 500 sat at the 40% default and 300 sat at 0%.
Protocol reserves
The protocol reserves are the network's own fund. No company and no single key holds them, and they are spent only on what the bonded nodes vote for.
- Where they are: in the federations. Each paid entry and item locks its reserve share in its own federation to that federation's reserve key, a 5-of-7 key its seven guardian seats made together. No seat holds the key alone; 5 of the 7 must sign to move anything.
- How you see them: every node answers
GET /reserveswith the total, per federation, the payments in and every payment out. bitcoinarena.io shows the live total under the fees once five nodes name the same one, and the node dashboard shows what each round paid in. - When a federation is drained (it is closing or has lost guardians), its reserves move on their own to a healthy federation: its successor, or else the open federation with the lowest id. No vote is needed for that move; every node records it.
The referral program
A website that brings players earns 10% of the nodes' fee on every stake they place. It comes out of the nodes' part: the player's stake and fee are the same with or without it. On a 10,000 sat stake the nodes' fee is 400 sat (4%), the website gets 40 sat and the nodes keep 360.
- A program is a key. Each entry can name a website's payout key (33 bytes, signed into the entry, so no node can swap it). At settlement that key is paid its share in the round's federation, beside the other payouts. From an order book: 10% of the nodes' part of each filled order's fees.
- Run one from your node. In the owner area, Referrer: name a program and your node makes its key. You can have several, each earns on its own. Its name is signed with its own key and shown on the explorer's Metrics page and on Health.
- Bring players. Link to
https://testnet.bitcoinarena.io/?ref=<key>(the page keeps it 30 days), or host a game on your site in an iframe:/?game=rock-paper-scissors&ref=<key>. The demo page does exactly that. - Withdraw over Lightning. Your node collects the program's payouts in the federation. In Referrer, Withdraw pays a Lightning invoice or a Lightning address.
- Where it is counted: every node answers
GET /referral-feeswith what each program earned and its name.
The 10% is a network fee like the others: a passed fee proposal can set it between 0 and 40% for rounds after it takes effect. It was 20% until 2026-10-25. Step by step: Referral Program.
Funding proposals: how the reserves pay out
Anyone can propose a payout, not only a developer: a player, a developer or a node operator, for any project and to any Lightning address. A proposal names its purpose, the amount in sat and the Lightning address (LNURL) to pay, and is signed by its author's key. Anyone can comment on it, in threads; comments carry no weight.
- Signal (1 week, 1008 blocks)
Nodes signal support. The proposal goes on to the vote if the nodes supporting it are at least 15% of the nodes that served a round in the 72 hours (432 blocks) before the week ends. Nothing is paid on a signal.
- Vote (2 weeks, 2016 blocks)
Bonded nodes vote YES, NO or ABSTAIN on Bitcoin, weighted by bond and age (rules 1 and 2 below). It passes when 60% of the votes say YES and YES is at least 20% of everyone who could vote. It is decided once its last block is 6 deep.
- Payout
After the 2-week window (rule 5), the federation that holds the reserves pays the proposal's Lightning address, signed by 5 of its 7 guardian seats. If the payment fails, the money returns to the reserve key. Every node records the payout with its proof (the payment's preimage and the federation transaction).
Five rules, so nobody can rent a majority. Bonding a lot of bitcoin for a few weeks to vote yourself a payout does not work:
- Who votes: bonds at least 3 months old when the proposal is published, that stay locked at least 3 months after the vote. A bond past its lock date still serves rounds but no longer votes.
- How much they count: bond × age: 1× under a year, 2× from one year, 3× from two. Nodes that have been in the game longer outweigh new money.
- When it passes: 60% of the votes say YES, and the YES votes are at least 20% of everyone who could vote.
- How much it can take: at most 10% of the reserves per proposal, and 25% in any 3 months.
- Time to say no: nothing is paid for 2 weeks after the vote, and NO votes in that time stop it. Above 1% of the reserves, it is paid in 4 monthly parts, each one stoppable the same way.
The signal counts only nodes with a bond at least 3 months old that served rounds in 3 of the last 4 weeks. No vote can pass in a network's first 3 months.
Review and discuss: the Project proposals forum lists every proposal the nodes hold, with its stage and its replies. Reply or propose there with the same key you play with, or with your nsec.
To post a proposal, sign it and send it to any node: POST /funding with { "kind": "proposal", "hex": ... }; the nodes pass it on to each other. GET /funding lists every proposal with its comments, signals and vote. A node owner can propose, comment and signal from the owner area.
Technologies used
What Bitcoin Arena is built with, and what each part is for.
| Technology | What it is used for |
|---|---|
| Bitcoin (testnet4 today) | Node bonds and their advertisements, funding votes, and the block hashes that seed each committee draw. |
| Bitcoin Core, Electrum | How a node reads the chain: its own Bitcoin Core, or an Electrum server for a node without custody. |
| Taproot, MuSig2 | The bond output: a MuSig2 key path for the node and the arbitration set, and two script paths. |
| Fedimint 0.12 (mint v2, Lightning v2) | The federations that hold the stakes as e-cash. A stake lock is a Lightning v2 contract. The network runs a patched Fedimint build. |
| Lightning Network, LND | Deposits and withdrawals through federation gateways, and the channels of liquidity providers. |
| Rust, WebAssembly | The wallet in your browser: the Fedimint client and the stake-lock code, compiled to WebAssembly. The dev client runs the same build in Node.js. |
| FROST with BIP-340 Schnorr signatures | The committee's 5-of-7 signature on a round's settlement and payouts. |
| Ed25519 | Developer keys that sign manifests, attestors' signatures, the page's session keys, and the release signature. |
| Nostr | Login, the wallet seed backup, Nostr Wallet Connect, node owner keys, results and badges, and the Arena relay. |
| Node.js 24 | The node. Game cores run in a worker thread inside a node:vm sandbox. |
SQLite (node:sqlite) | The node's store and the Arena relay's store. |
| React 19, esbuild | The owner page, the node dashboard, the homepage, the explorer, the installer wizard and parts of the game page. |
| Unity WebGL | The graphics of Tick Tock, Next Block. |
| Cloudflare Workers, Pages, D1 | bitcoinarena.io, these docs and the explorer run on Workers; the explorer keeps its data in D1. The testnet page is on Pages. |
| Cloudflare Tunnel, Tor | How a node is reached without an open port: cloudflared for its public address, Tor for its LND's onion address. Both ship in the installer. |
| Installer packages | A Windows .exe, a macOS .dmg and a Linux .deb; the node runs under systemd, launchd or a Windows logon task. |
| OpenAPI 3, Swagger UI | The node API reference. |
FAQ
Where are my sats?
As Fedimint e-cash in your browser's wallet, under your 24 words or your Nostr key, in one of the federations. No company holds them. While you play, your stake sits in a lock in the federation that only the round's result, or its expiry, can open.
I lost my 24 words. Can anyone recover my wallet?
No. Without your words (or the Nostr key you logged in with) the wallet and every lock it would refund are gone. Back up your words in Settings.
How do I get testnet sats?
The testnet page links a test Lightning wallet with a small faucet (1,000 sat at a time, rate-limited). Pay the Deposit invoice from it, or from any testnet4 Lightning wallet. For a node's bond you need on-chain testnet4 coins: coinfaucet.eu sends them free.
What is the smallest deposit?
5 sat. 1 sat paid is 1 sat credited.
Why does my federation show a load above 1?
It holds more bitcoin than its guardians' bonds cover. On testnet4 that is a warning only. On mainnet the page would refuse deposits into it and point you to another federation.
Federation A or B?
You do not have to choose: a deposit goes to the open federation with the lower load, and every game can be paid through either. A federation the page does not know is shown with its guardians and must be confirmed by you first.
Are there free rounds?
No. There are only paid rounds. Free rounds are for development: on a devnet, and for a newly signed game before its paid rounds open.
Who can ask for money from the protocol reserves?
Anyone. Post a funding proposal with what it is for, the amount and your Lightning address. It is paid only after the nodes signal it (15% within a week), the bonded nodes vote for it (2 weeks: 60% YES, and YES at least 20% of everyone who could vote) and nobody stops it in the 2 weeks after. Review and discuss proposals at testnet.bitcoinarena.io/proposals. See Funding proposals.
What is taken from a paid stake?
On protocol 11: 4 % to the nodes, 2 % to the game's developer, 2 % to the protocol developers and 2 % to the protocol reserves, all fixed. Before 11: a 5 % node fee and your donation, 0 to 5 % (5 % by default). Up or Down charges 1 % instead, only on what fills. See Where the money goes.
What if a node cheats?
One node cannot decide anything: 5 of the round's 7 seats must sign the same result, every run is replayed, and anyone can object for 10 minutes. If the committee cannot agree, the entry is void and your stake comes back. See When a node cheats.
What if nobody settles my round?
Your lock expires and your own client takes it back with your key. Nobody has to act for you.
Can I join a round that has started?
Yes, while entries are open. The run still ends with the round, so you have less time.
Rock Paper Scissors: what if my opponent does not reveal?
They forfeit and you win. If nobody joins your table, the round is void and your stake is refunded.
Who decides Bitcoin Up or Down?
Three named attestors; 2 of 3 must sign the same outcome. A tie is Up. If they disagree, or no outcome stands by the deadline, every stake and fee is refunded.
Do I need Nostr?
No. A wallet with 24 words works without it. Nostr lets you log in with a key you already have, bring your wallet to another device, and pay from your own wallet over Nostr Wallet Connect.
How do I claim my node?
Open /dashboard/owner on it and enter the claim code from the node's log or from <db>.owner-claim.
Is this on mainnet?
No. Bitcoin Arena runs on Bitcoin testnet4. Test sats have no value.