1What you need
- Rust (stable, via rustup). The workspace has no
rust-toolchainfile; this page used cargo 1.99.0 / rustc 1.99.0 on Linux. - Node 20 or newer for the SDK script in step 7 (this page: v20.20.2). The SDK is plain ES modules with no
npm install. - Outbound HTTPS to
services.divi.domains: the node reads Divi testnet checkpoints from the public RPC. No Divi node of your own, no testnet DIVI. - About 600 MB of disk for the release build; the chain itself is a 45 KB SQLite file at the start.
If you want to know why a node needs Divi at all, read What a Divi sidechain is first.
2Get the source
The repository is DiviDomains/divi-games on GitHub. On 2026-10-21 it is still private (an unauthenticated git clone gets a 404), so ask for access on games.divi.domains or clone it with an account that has been added. Once you have it:
git clone git@github.com:DiviDomains/divi-games.git cd divi-games ls
ARCHITECTURE.md assets bins bots Cargo.lock Cargo.toml content crates deploy docs scripts site tests tools web
The parts this page touches:
| Path | What it is |
|---|---|
bins/divi-gamechain/ | The node: config, HTTP API, sequencer loop, Divi RPC client, anchor and bridge tasks, the house task. |
bins/divi-games-site/ | The web site that fronts a node (play pages, explorer, courses). Not needed here. |
crates/dg-core, dg-chain | Addresses, signatures, transactions, blocks, the SQLite store and the sequencer. |
crates/dg-system, dg-games, dg-market, dg-td | The modules: tokens, faucet, HTLC, bridge, certificates, tic-tac-toe, blackjack, the market and tower defense. |
web/sdk/divigames.js, web/vendor/ | The JavaScript SDK and the two noble crypto libraries it imports. |
ARCHITECTURE.md | The design document. Worth an hour. |
3Build it
Build the node alone (the site binary and the WASM tower-defense crate are not needed):
cargo build --release -p divi-gamechain
The build for this page also added -p divi-games-site (the two binaries share almost every dependency) and compiled 230 crates in 1 min 36 s on a 32-core Linux box; expect several minutes on a laptop. The tail of that build log:
Compiling divi-games-site v0.1.0 (…/divi-games/bins/divi-games-site)
Compiling dg-chain v0.1.0 (…/divi-games/crates/dg-chain)
Compiling divi-gamechain v0.1.0 (…/divi-games/bins/divi-gamechain)
Finished `release` profile [optimized] target(s) in 1m 36s
The binary is target/release/divi-gamechain. Put it on your PATH or use the full path; the commands below assume it is on the path.
divi-gamechain --help
Divi Games side-chain node
Usage: divi-gamechain [OPTIONS] <COMMAND>
Commands:
run Serve the API and produce blocks
keygen Create the sequencer key if there is none, and print its address (never the key)
info Print the chain head
replay Re-execute every block from genesis and check it against the stored headers
help Print this message or the help of the given subcommand(s)
Options:
--config <CONFIG> TOML config file; every field is optional [env: DG_CONFIG=]
--data-dir <DATA_DIR> Overrides `data_dir` from the config file [env: DG_DATA_DIR=]
-h, --help Print help
-V, --version Print version
4Make the sequencer key
Your node needs one Divi testnet key: it signs every block and the house's own game moves. keygen makes it inside the data directory and prints only the address. We keep everything for this node under one folder, ~/tut-i/run-the-gamechain/ on this page's machine.
mkdir -p ~/tut-i/run-the-gamechain && cd ~/tut-i/run-the-gamechain divi-gamechain --data-dir data keygen
/home/bshuler/tut-i/run-the-gamechain/data/sequencer.key created: xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM
Run it a second time and nothing changes: it is safe to put in a start script.
divi-gamechain --data-dir data keygen /home/bshuler/tut-i/run-the-gamechain/data/sequencer.key exists: xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM ls -la data drwx------ 2 bshuler bshuler 4096 Oct 21 19:55 . -rw------- 1 bshuler bshuler 53 Oct 21 19:55 sequencer.key
data/ is created with mode 0700 and sequencer.key with 0600. It holds one WIF (a testnet one starts with c). Never cat it into a terminal, a log or a chat. The node refuses to start if the file is readable by other users (step 9), and it refuses a mainnet key outright.
5Write the config
The config is a TOML file and every field is optional: leave one out and its default applies. The file below lists every field with its default, except data_dir and listen, which we set for this node. Unknown fields are an error (deny_unknown_fields), which catches typos like blockms; you will see that error in step 9.
# gamechain.toml: every field with its default. Leave a line out and the default applies.
data_dir = "/home/bshuler/tut-i/run-the-gamechain/data"
listen = "127.0.0.1:53920"
block_ms = 2000
heartbeat_ms = 60000
divi_rpc = "https://services.divi.domains/api/testnet/rpc/"
divi_poll_ms = 30000
certificate_issuers = []
[anchor]
enabled = false
every = 300
fee_sat = 1000000
poll_ms = 5000
[bridge]
enabled = false
address = ""
confirmations = 6
poll_ms = 30000
lookback = 2000
| Field | Default | Meaning |
|---|---|---|
data_dir | $XDG_DATA_HOME/divi-gamechain, else ~/.local/share/divi-gamechain | Where chain.db, sequencer.key and house.secret live. --data-dir or DG_DATA_DIR overrides it. |
listen | 127.0.0.1:7410 | HTTP address of the API. run --listen overrides it. Keep it on localhost and put nginx in front for the public. |
block_ms | 2000 | Block interval while the mempool is not empty. |
heartbeat_ms | 60000 | An empty block after this long without one. |
divi_rpc | public testnet RPC | Where checkpoints come from. Point it at your own divid -testnet if you run one. |
divi_poll_ms | 30000 | How often to ask Divi for a new checkpoint (tip minus 6). |
certificate_issuers | [] | Addresses allowed to issue certificates. Empty means the sequencer only. |
[anchor] | off, every 300, fee 1 000 000 sat, poll 5 s | Writes DGCH anchors to Divi testnet. Needs testnet DIVI on the sequencer address. See Anchors and the tDIVI bridge. |
[bridge] | off, address "", 6 confirmations, poll 30 s, lookback 2000 | Mints TDIVI for deposits to address. Same tutorial. |
The chain id, divi-games-testnet-1, is not configurable: it is part of genesis and goes into every signed transaction.
6Run it
Start the node in the background and keep its PID. The default log filter is divi_gamechain=info,tower_http=warn; set RUST_LOG to change it.
cd ~/tut-i/run-the-gamechain nohup divi-gamechain --config gamechain.toml run --listen 127.0.0.1:53920 > node.log 2>&1 < /dev/null & echo $! > node.pid
Within a few seconds the log shows the chain opening, the API coming up, the house task making its three opening moves, and the first Divi checkpoint arriving:
2026-10-21T19:55:49.025576Z INFO divi_gamechain: chain opened chain=divi-games-testnet-1 sequencer=xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM height=0 2026-10-21T19:55:49.026420Z INFO divi_gamechain: serving listen=127.0.0.1:53920 2026-10-21T19:55:49.035510Z INFO divi_gamechain::house: house action submitted kind="house_commit" pending=0 2026-10-21T19:55:49.036125Z INFO divi_gamechain: block height=1 txs=1 2026-10-21T19:55:49.036895Z INFO divi_gamechain::house: house action submitted kind="faucet" pending=0 2026-10-21T19:55:49.217545Z INFO divi_gamechain: Divi checkpoint reachable height=359936 2026-10-21T19:55:51.028374Z INFO divi_gamechain: block height=2 txs=1 2026-10-21T19:55:51.029568Z INFO divi_gamechain::house: house action submitted kind="fund" pending=0 2026-10-21T19:55:53.028319Z INFO divi_gamechain: block height=3 txs=1
- height=0: a fresh data directory. Genesis names your sequencer address and is stored in
chain.db. - house: the blackjack dealer is the sequencer itself. At start it commits a shuffle secret (block 1), claims 1000 GOLD from the faucet (block 2) and funds the house (block 3). Blocks come every 2 s because the mempool was non-empty.
- Divi checkpoint reachable: 359936 is six below Divi testnet's tip at that moment. Block 1 was made before this line, so its checkpoint is empty; from block 2 on every header carries one.
- After that, with nothing to do, the node makes one empty heartbeat block a minute. Those do not show in the log;
/v1/infoshows the height climbing by one a minute.
Ask it who it is:
curl -s http://127.0.0.1:53920/v1/info
{"chain_id":"divi-games-testnet-1",
"divi":{"hash":"e7848cc57438c6bab71aa721b1eb71b6c6e4df277984f79308ba75d7c7a86b69","height":359936},
"divi_seen":{"hash":"e7848cc5…c7a86b69","height":359936},
"head_hash":"8997f9183857e66b9cdf185d5244ab73bda00cf5833419d53a8220a8963da8ae",
"height":3,"mempool":0,
"modules":["backend","blackjack","bridge","certificate","htlc","market","system","tictactoe","towerdefense"],
"network":"testnet",
"sequencer":"xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM",
"time_ms":1792612553027,"tokens":["GOLD","TDIVI"]}
Same shape as the public chain at https://games.divi.domains/chain/v1/info, different sequencer, your own history. The other routes are /v1/blocks, /v1/blocks/{height}, /v1/tx/{txid}, /v1/account/{addr}, /v1/tokens, /v1/games/{game}, POST /v1/tx and the WebSocket /v1/ws.
7Claim GOLD from your own faucet
Now be a player. The SDK is web/sdk/divigames.js in the repo; it imports ../vendor/noble-hashes and ../vendor/noble-secp256k1 by relative path, so copy the whole web/ folder next to your script:
cp -r ~/divi-games/web ~/tut-i/run-the-gamechain/web
claim.mjs makes a player key the first time (saved to player.key, mode 0600, never printed), then sends a faucet action and waits for the receipt:
// claim.mjs: make a player key, claim 1000 GOLD from your own node's faucet, show the account. import fs from 'node:fs'; import { Key, Chain } from './web/sdk/divigames.js'; const KEYFILE = 'player.key'; // the player's WIF, mode 0600, never printed let key; if (fs.existsSync(KEYFILE)) { key = Key.fromWif(fs.readFileSync(KEYFILE, 'utf8')); } else { key = Key.generate('testnet'); fs.writeFileSync(KEYFILE, key.toWif() + '\n', { mode: 0o600 }); } console.log('player ', key.address); const chain = new Chain('http://127.0.0.1:53920'); const info = await chain.info(); console.log('node ', info.chain_id, 'height', info.height, 'sequencer', info.sequencer); const txid = await chain.send(key, { type: 'faucet', token: 'GOLD' }); console.log('txid ', txid); const receipt = await chain.waitForReceipt(txid); console.log('receipt ', JSON.stringify(receipt)); const account = await chain.account(key.address); console.log('account ', JSON.stringify(account));
node claim.mjs
player y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9
node divi-games-testnet-1 height 5 sequencer xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM
txid 70b4d72e4ee1fe885e95f15a2746445a86ce736b09b491e738f6e7487b851e5e
receipt {"error":null,"events":[{"data":{"amount":"100000","to":"y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9","token":"GOLD"},"kind":"faucet"}],"height":6,"index":0,"ok":true,"txid":"70b4d72e…b851e5e"}
account {"address":"y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9","balances":{"GOLD":"100000"},"next_nonce":1,"nonce":1}
chain.send reads your next nonce, builds the body, signs it with a Divi signed message and POSTs it; waitForReceipt polls /v1/tx/<txid> every 500 ms (for up to 30 s) until the block that holds it is made. "100000" is 1000.00 GOLD. The node stores exactly what you signed:
curl -s http://127.0.0.1:53920/v1/tx/70b4d72e4ee1fe885e95f15a2746445a86ce736b09b491e738f6e7487b851e5e
{"body":"{\"v\":1,\"chain\":\"divi-games-testnet-1\",\"from\":\"y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9\",\"nonce\":0,\"action\":{\"type\":\"faucet\",\"token\":\"GOLD\"}}",
"receipt":{… "height":6,"index":0,"ok":true …},
"sig":"H8L7sulqOcZlYT4tWfzx4apX2/YLJvfkvcbKuEaeqWnjW4RyXpueEtTfn1RneQZTqsBwrcYdrPR5HIaxWkf5+jQ="}
The second run reuses player.key and the faucet says no: the cooldown is 1800 blocks per address. The transaction is still included, with "ok":false and an error, and it uses up nonce 1. That is how the chain treats every valid-but-failed action.
node claim.mjs
player y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9
node divi-games-testnet-1 height 30 sequencer xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM
txid 66cd0ac51de1d821c35f887505f86ed4e20d54f9bcbe32d01849155abd85b0fd
receipt {"error":"conflict: faucet cooling down; next claim at height 1806","events":[],"height":31,"index":0,"ok":false,"txid":"66cd0ac5…85b0fd"}
account {"address":"y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9","balances":{"GOLD":"100000"},"next_nonce":2,"nonce":2}
8info and replay
Two subcommands open the data directory directly, without the API, and work whether the node is running or not. These were run right after the first faucet claim, at height 6.
divi-gamechain --config gamechain.toml info
chain divi-games-testnet-1 sequencer xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM height 6 head 827a8052645732b484d0d7ae5c29551f01d2f43b79f8e2e2c04aea3e34124fdc divi 359936 e7848cc57438c6bab71aa721b1eb71b6c6e4df277984f79308ba75d7c7a86b69
divi-gamechain --config gamechain.toml replay
ok: 7 blocks, 4 txs, 11 state keys, state root 872a6e779d5bb9c06fff0cd658f84431fe687860b9a1b33775e43d2d28adb77d
replay starts from genesis with an empty state, re-executes every transaction in every block in a fresh in-memory store, and compares each block's tx_root and state_root with the stored header. 7 blocks is genesis plus heights 1 to 6; the 4 transactions are the house's three and your faucet claim. The 11 state keys (select key from kv in chain.db) are two nonces, two GOLD balances (yours and the blackjack house's), two faucet cooldown stamps, the two token records, the issuer list, the house's blackjack state and the tower-defense week. If anything had been edited in chain.db by hand, this is where it would show.
9Stop, restart, and the errors you will meet
Stop it by PID. The node catches SIGINT or SIGTERM, logs stopped and exits; the port closes.
kill $(cat node.pid); sleep 2 tail -1 node.log 2026-10-21T19:58:31.307551Z INFO divi_gamechain: stopped ss -ltn | grep 53920 || echo 'port 53920 closed' port 53920 closed
Everything the node knows is in data/: chain.db (SQLite in WAL mode, so you will also see chain.db-wal and chain.db-shm), sequencer.key and house.secret (the dealer's shuffle secret, also 0600). Back up the folder, not the database alone.
Errors, each one provoked on purpose
Key readable by others. The node checks the mode of sequencer.key before it reads it:
chmod 644 data/sequencer.key
divi-gamechain --config gamechain.toml run
Error: /home/bshuler/tut-i/run-the-gamechain/data/sequencer.key is readable by other users (mode 644); chmod 600 it
chmod 600 data/sequencer.key
A mainnet key. A WIF for a mainnet address (D…) is refused, so the chain can never be run against real DIVI by mistake. (This page made a throwaway mainnet WIF into a scratch directory with the SDK, Key.generate('mainnet'), and deleted it afterwards.)
divi-gamechain --data-dir data-mainnet info
Error: data-mainnet/sequencer.key holds a mainnet key; the side-chain only runs on testnet
A different key with an existing database. Genesis names the sequencer. Copy chain.db somewhere, make a new key beside it, and the node refuses the pair:
mkdir -m 700 data-other && cp data/chain.db data-other/ divi-gamechain --data-dir data-other keygen data-other/sequencer.key created: xxmj4xkdWnJc9KA1TdX6DYaqcmJU1tZha4 divi-gamechain --data-dir data-other info Error: config: sequencer key is xxmj4xkdWnJc9KA1TdX6DYaqcmJU1tZha4 but genesis names xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM
The same guard covers genesis itself: the store keeps the genesis it was created with, and a database whose genesis does not match what the node computes is refused with genesis does not match the one this data directory was created with.
A typo in the config.
printf 'listen = "127.0.0.1:53920"\nblockms = 500\n' > bad.toml
divi-gamechain --config bad.toml info
Error: parsing bad.toml
Caused by:
TOML parse error at line 2, column 1
|
2 | blockms = 500
| ^^^^^^^
unknown field `blockms`, expected one of `data_dir`, `listen`, `block_ms`, `heartbeat_ms`, `divi_rpc`, `divi_poll_ms`, `certificate_issuers`, `anchor`, `bridge`
Restart
A two-line start.sh is enough for a workstation (production gets a systemd unit; see step 10):
#!/bin/sh
cd ~/tut-i/run-the-gamechain
nohup divi-gamechain --config gamechain.toml run --listen 127.0.0.1:53920 >> node.log 2>&1 < /dev/null &
echo $! > node.pid
./start.sh; sleep 4; tail -3 node.log
2026-10-21T19:58:33.425560Z INFO divi_gamechain: chain opened chain=divi-games-testnet-1 sequencer=xxD8ohHuhAGjmxATudgBqfrhwbY6WJR7jM height=6 2026-10-21T19:58:33.425664Z INFO divi_gamechain: serving listen=127.0.0.1:53920 2026-10-21T19:58:33.658647Z INFO divi_gamechain: Divi checkpoint reachable height=359936
It opens at height 6, where it stopped, with the same sequencer, and the house does not repeat its opening moves. Your balance survived:
curl -s http://127.0.0.1:53920/v1/account/y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9
{"address":"y1baNdNhw5ZkcVoCSTUCANM84E1mhw3cR9","balances":{"GOLD":"100000"},"next_nonce":1,"nonce":1}
Never run two nodes from one data directory or with one sequencer key: they would sign two different chains with the same identity. One directory, one key, one process. If you want a second chain, make a second directory and run keygen there.
10Where to go next
- Deploy your game: the production path, a systemd unit for
divi-gamechainand nginx in front of it, plus the site binary. - Anchors and the tDIVI bridge: turn on
[anchor]and[bridge]once the sequencer address holds testnet DIVI. - The JavaScript SDK: everything
ChainandKeycan do. - Build tic-tac-toe: write a game module and play it on the node you just started.
- What a Divi sidechain is: the checkpoint, anchors and state roots you have just seen, explained.
Stuck? Ask the Divi chatbot, or see all tutorials.