1Your account is an address
Divi Games has no accounts table with passwords in it. The thing the gamechain keeps balances and nonces for is a Divi pay-to-public-key-hash address: the same kind of address a Divi wallet shows you. Whoever holds the private key behind the address can play as it, and nobody else can.
An address is made from a public key in three steps: hash the 33-byte compressed public key with SHA-256 then RIPEMD-160 (hash160), put a one-byte network version in front, and encode the result in Base58Check.
x or yD)The public gamechain is divi-games-testnet-1, so it only accepts testnet addresses. A D address is rejected with signer address is on the wrong network (you will see the node's wording in step 6).
You never log in. Each request you send already carries a signature over its own contents, so the node authenticates every single action on its own. There is no login session, no cookie and no token to steal. (The game pages can give a fresh key the right to play as you for a while; that is a session key, and it is also a signed, on-chain thing.) Lose the key and the account is gone; copy the key and someone else has it.
2What gets signed
Divi Games reuses the message signing that every Divi wallet already has (the signmessage / verifymessage RPCs). The signer never signs the raw text. It signs this 32-byte digest:
hash = SHA256( SHA256( varstr("DarkNet Signed Message:\n") || varstr(message) ) )
varstr(s) is the Bitcoin-style string encoding: a compact-size length prefix followed by the UTF-8 bytes. The prefix counts bytes, not characters. The magic string DarkNet Signed Message:\n (with the newline) is Divi's; Bitcoin's is different, so a Bitcoin wallet's signature over the same text will never verify on Divi.
The signature itself is 65 bytes, sent as Base64:
| Byte | What it is |
|---|---|
| 0 | Header: 27 + recid + 4. recid (0–3) says which of the possible public keys made the signature; the + 4 means the key is compressed. Divi wallets always use compressed keys, so the header is 31, 32, 33 or 34. |
| 1–32 | r |
| 33–64 | s, normalised to the low half of the curve order ("low-s") |
The signature is deterministic (RFC 6979): signing the same message with the same key gives the same bytes every time. That is why the gamechain can use a transaction's body as the only input to its txid, as you will see in step 5.
3Four ways to sign
Any of these produces a signature the games accept. They all compute the same hash and output the same 65-byte format.
The IronDivi browser extension
A page asks the extension to sign through the injected provider window.irondivi. The user sees a confirmation popup with the message text and clicks Approve. To try it, install the extension from its release page: download the zip, unzip it, open chrome://extensions, turn on Developer mode and click Load unpacked. Get testnet wallets has the steps.
// In a web page. The provider is only there if the extension is installed. const divi = window.irondivi; const { address } = await divi.connect(); // user approves the site once const sig = await divi.signMessage('hello from the page'); // sig is the same Base64 string signmessage would give for the account's first address
signMessage throws Not connected. Call connect() first. unless connect() has succeeded for this site. Without a second argument it signs with the account's first address, the one connect() returned. Pass another address of the same account as signMessage(message, address) to sign with that one; an address the wallet does not hold fails with Address is not in this wallet. We checked this against the source of the 0.0.1 release (src/injected/provider.ts and src/background/service-worker.ts), not against ARCHITECTURE.md, which shows only signMessage(message).
The full provider API, with transaction signing, is in Browser extension API. No WalletConnect or Reown anywhere: Divi uses its own injected provider.
The IronDivi Telegram wallet
The wallet bot @divi_wallet_bot signs a message in one command. A first word testnet or mainnet picks the network for that one signature; without it the bot uses the network you chose with /network.
/sign testnet hello from Telegram
The bot answers Message ready to sign. Nothing has been signed. with a Sign in Wallet (Testnet) button. It opens the wallet, where you pick the address under Sign With Address and tap Approve & Sign. Copy Signature copies the same Base64 string signmessage gives. The bot itself never signs: the key stays in the wallet. A message can be up to 512 characters.
The command line: divi-cli or irondivi-cli
Both the C++ node and IronDivi expose the classic RPC pair. The address must be one your wallet holds the key for. This is a real run against an IronDivi testnet node (a test build of 0.2.6, still syncing, which does not matter: signing is local; the 0.2.6 release signs and verifies the same way):
$ irondivi-cli --conf ~/tut-i/irondivi/irondivi.conf --testnet getnewaddress y88nGdSSs2CWE13K6aZSNqRbVfw7EB5pEY $ irondivi-cli --conf ~/tut-i/irondivi/irondivi.conf --testnet signmessage y88nGdSSs2CWE13K6aZSNqRbVfw7EB5pEY "hello from irondivi-cli" H9mNaZMcmI7YHd5Ehp8vLjZ7XUfhyM2fI9i6yulGdKeucxTFvHrvxFGP4NvmkQ8syLSIPNwpLMrbHXBmic47ezM= $ irondivi-cli --conf ~/tut-i/irondivi/irondivi.conf --testnet verifymessage y88nGdSSs2CWE13K6aZSNqRbVfw7EB5pEY H9mNaZMcmI7YHd5Ehp8vLjZ7XUfhyM2fI9i6yulGdKeucxTFvHrvxFGP4NvmkQ8syLSIPNwpLMrbHXBmic47ezM= "hello from irondivi-cli" true
With the C++ node the commands are divi-cli -testnet signmessage <address> "<message>" and divi-cli -testnet verifymessage <address> <signature> "<message>". The Sign and verify a message tutorial walks through them.
The JavaScript SDK: Key.signMessage
divigames.js carries a small key class so a page or a Node script can sign without any wallet. It is what the play pages at games.divi.domains use for guest players.
import { Key } from './sdk/divigames.js';
const key = Key.generate('testnet'); // or Key.fromWif(wif)
key.address; // 'y...'
key.signMessage('hello from divigames.js'); // Base64, 65 bytes
Where to get the SDK, how it is laid out and how to put a game on your own page is The JavaScript SDK. The next step uses it to show the whole round trip.
4How a signature is checked
The verifier never needs your public key in advance. From the hash and the 65-byte signature it recovers the public key (the header's recid says which candidate), hashes it with hash160, and compares the result with the 20 bytes inside the address. Equal means the signature came from that address's key.
This script makes a testnet key (saved to a 0600 file that is never printed), signs a message, and checks the signature three ways: with the SDK's own verifyMessage, with Divi Core's verifymessage on the public testnet RPC, and finally the SDK checks the signature irondivi-cli made above. The scripts in this tutorial sit next to a copy of sdk/ and vendor/ from the games repo; Node 20 or newer.
// sign.mjs: make (or load) a testnet key, sign a message, verify it three ways. // node sign.mjs import fs from 'node:fs'; import { Key, verifyMessage, recoverMessageSigner, messageHash, bytesToHex, hash160 } from './sdk/divigames.js'; const RPC = 'https://app.irondivi.com/api/testnet/rpc/'; const KEYFILE = 'player.wif'; // Load the key from a 0600 file, or make a new one. The WIF is 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(), { mode: 0o600 }); } console.log('address ', key.address); console.log('public key', bytesToHex(key.publicKey)); const msg = 'hello from divigames.js'; const sig = key.signMessage(msg); console.log('message ', JSON.stringify(msg)); console.log('hash ', bytesToHex(messageHash(msg))); console.log('signature ', sig); const raw = Buffer.from(sig, 'base64'); console.log('header byte', raw[0], '= 27 + recid', (raw[0] - 27) & 3, '+ 4 (compressed)'); // 1. The SDK's own check: recover the public key, hash it, compare with the address. const recovered = recoverMessageSigner(msg, sig); console.log('recovered pubkey matches', bytesToHex(recovered) === bytesToHex(key.publicKey)); console.log('hash160 of it ', bytesToHex(hash160(recovered))); console.log('verifyMessage(genuine) ', verifyMessage(key.address, msg, sig)); console.log('verifyMessage(tampered) ', verifyMessage(key.address, msg + '!', sig)); // 2. Ask Divi Core on the public testnet RPC. async function verifymessage(address, signature, message) { const res = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '1.0', id: 1, method: 'verifymessage', params: [address, signature, message] }), }); return (await res.json()).result; } console.log('RPC verifymessage(genuine) ', await verifymessage(key.address, sig, msg)); console.log('RPC verifymessage(tampered)', await verifymessage(key.address, sig, msg + '!')); // 3. A signature made by irondivi-cli (`signmessage y88n... "hello from irondivi-cli"`) checks out in the SDK too. const cliAddr = 'y88nGdSSs2CWE13K6aZSNqRbVfw7EB5pEY'; const cliSig = 'H9mNaZMcmI7YHd5Ehp8vLjZ7XUfhyM2fI9i6yulGdKeucxTFvHrvxFGP4NvmkQ8syLSIPNwpLMrbHXBmic47ezM='; console.log('SDK verifies irondivi-cli signature', verifyMessage(cliAddr, 'hello from irondivi-cli', cliSig)); console.log('RPC verifies irondivi-cli signature', await verifymessage(cliAddr, cliSig, 'hello from irondivi-cli'));
What it printed:
$ node sign.mjs address yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg public key 03ba246333bfc83f3197af465a580bf30da59a4f56bbbcb0bbce2c7f12f1628dfb message "hello from divigames.js" hash 3367a99d55b1d1d761c304ba0d5b8bcfddb5a9832fdc3969e4cc6aaae6732425 signature IETg6/wqTeBN1cgZWowKg2vw9KBRg/p29EvHCx2Z6xvQYkvOcOWePBBypcT/zQUnnqJcr4isdbfntDPM3VUFQp4= header byte 32 = 27 + recid 1 + 4 (compressed) recovered pubkey matches true hash160 of it cca782a90b24e5c69bf8126ab6202c48fbccb19e verifyMessage(genuine) true verifyMessage(tampered) false RPC verifymessage(genuine) true RPC verifymessage(tampered) false SDK verifies irondivi-cli signature true RPC verifies irondivi-cli signature true
Three independent implementations (the SDK in JavaScript, Divi Core in C++, IronDivi in Rust) agree on every signature. Changing one character of the message flips both verifiers to false, because the recovered key no longer hashes to the address.
You can run the RPC check yourself without any script; the public testnet node answers plain JSON-RPC over HTTPS:
$ curl -s https://app.irondivi.com/api/testnet/rpc/ -H 'content-type: application/json' \
-d '{"jsonrpc":"1.0","id":1,"method":"verifymessage","params":["yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg","IETg6/wqTeBN1cgZWowKg2vw9KBRg/p29EvHCx2Z6xvQYkvOcOWePBBypcT/zQUnnqJcr4isdbfntDPM3VUFQp4=","hello from divigames.js"]}'
{"error":null,"id":1,"result":true}
5A game transaction, signed by hand
A gamechain transaction is just a message signature over a JSON string. The envelope you POST is {"body": "<json string>", "sig": "<base64>"}, where body is:
| Field | Value |
|---|---|
| v | 1 |
| chain | "divi-games-testnet-1". A body for another chain is rejected; a signature cannot be replayed on a different gamechain. |
| from | Your testnet address. The signature must recover to it. |
| nonce | Your account's next nonce: 0 for a fresh address, then up by one for every transaction that gets into a block, successful or not. Stops replays. |
| action | An object with a type: faucet, transfer, game and so on. |
| memo | Optional, at most 140 characters. |
The node signs and hashes the exact bytes you sent. It never parses and re-serialises the body, so key order and whitespace are whatever you produced; just sign the same string you send. The transaction id is hex(SHA256(body)): the body alone, not the signature, and no magic prefix. The body may be at most 32 KiB.
This script builds a faucet claim for the key from step 4 without using the SDK's Chain helper, so every byte is visible. It reads the nonce from GET /v1/account/<address>, signs, computes the txid, POSTs, waits for the receipt and reads the account again.
// faucet.mjs: build a gamechain tx by hand, sign it, submit it, read the receipt. // node faucet.mjs import fs from 'node:fs'; import { createHash } from 'node:crypto'; import { Key, messageHash, bytesToHex } from './sdk/divigames.js'; const CHAIN = 'https://games.divi.domains/chain'; const key = Key.fromWif(fs.readFileSync('player.wif', 'utf8')); // 1. The account's next nonce. const before = await (await fetch(`${CHAIN}/v1/account/${key.address}`)).json(); console.log('account before', JSON.stringify(before)); // 2. The body is a JSON *string*. These exact bytes get signed; the node never re-serialises them. const body = JSON.stringify({ v: 1, chain: 'divi-games-testnet-1', from: key.address, nonce: before.next_nonce, action: { type: 'faucet', token: 'GOLD' }, }); console.log('body', body); // 3. Sign the body like any other message. txid = sha256 of the body bytes. const sig = key.signMessage(body); const txid = createHash('sha256').update(body, 'utf8').digest('hex'); console.log('message hash', bytesToHex(messageHash(body))); console.log('sig ', sig); console.log('txid', txid); fs.writeFileSync('last-tx.json', JSON.stringify({ body, sig })); // 4. Submit the envelope {body, sig}. const res = await fetch(`${CHAIN}/v1/tx`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ body, sig }), }); console.log('POST /v1/tx ->', res.status, await res.text()); // 5. Wait for the receipt, then look at the account. for (;;) { const t = await (await fetch(`${CHAIN}/v1/tx/${txid}`)).json(); if (t.receipt) { console.log('receipt', JSON.stringify(t.receipt)); break; } await new Promise((r) => setTimeout(r, 500)); } const after = await (await fetch(`${CHAIN}/v1/account/${key.address}`)).json(); console.log('account after ', JSON.stringify(after));
The real run, on the public gamechain:
$ node faucet.mjs
account before {"address":"yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg","balances":{},"next_nonce":0,"nonce":0}
body {"v":1,"chain":"divi-games-testnet-1","from":"yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg","nonce":0,"action":{"type":"faucet","token":"GOLD"}}
message hash 4ab51701ed969655b5ad48162d90564fe7c2f4d34c2fd218191b726c5ed85fac
sig H3qETc788nPiNGAhejO/SMAQj6krfIg9zv+DV5T23L96XuaHt8/9sqyAh1PyX59NWKu4lBH8DFpWHb/9tUHltaY=
txid e93743a519e068875c732a692d8dfa7657215460c61fbe80f129b621ffb61cde
POST /v1/tx -> 200 {"txid":"e93743a519e068875c732a692d8dfa7657215460c61fbe80f129b621ffb61cde"}
receipt {"error":null,"events":[{"data":{"amount":"100000","to":"yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg","token":"GOLD"},"kind":"faucet"}],"height":1430,"index":0,"ok":true,"txid":"e93743a519e068875c732a692d8dfa7657215460c61fbe80f129b621ffb61cde"}
account after {"address":"yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg","balances":{"GOLD":"100000"},"next_nonce":1,"nonce":1}
Read it top to bottom: the node accepted the envelope (200 with the same txid we computed locally), put it in block 1430 as transaction 0, the faucet paid 100000 base units (1000.00 GOLD, two decimals), and the account's nonce moved from 0 to 1. The transaction is still there: /chain/v1/tx/e93743a5….
nonce counts transactions already in blocks. next_nonce also counts yours waiting in the mempool, so it is the one to put in your next body if you send several in a row. The SDK's Chain.send() does exactly this for you; this tutorial avoids it on purpose.
6What the node rejects, and what it says
Four ways a transaction goes wrong, sent to the live node right after the faucet claim. Each one is a correctly formed envelope with exactly one thing off.
// errors.mjs: what the live node says to a wrong nonce, a tampered body, a wrong chain id, and a replay. // node errors.mjs import fs from 'node:fs'; import { Key } from './sdk/divigames.js'; const CHAIN = 'https://games.divi.domains/chain'; const key = Key.fromWif(fs.readFileSync('player.wif', 'utf8')); const { next_nonce } = await (await fetch(`${CHAIN}/v1/account/${key.address}`)).json(); async function submit(label, body, sig) { const res = await fetch(`${CHAIN}/v1/tx`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ body, sig }) }); console.log(`${label}: HTTP ${res.status} ${await res.text()}`); } const mk = (nonce, chain = 'divi-games-testnet-1') => JSON.stringify({ v: 1, chain, from: key.address, nonce, action: { type: 'transfer', to: key.address, token: 'GOLD', amount: '1' } }); // a. Correct signature, but the nonce is 5 too high. let body = mk(next_nonce + 5); await submit('wrong nonce ', body, key.signMessage(body)); // b. Signed one body, sent another (amount changed after signing). body = mk(next_nonce); const sig = key.signMessage(body); await submit('tampered body ', body.replace('"amount":"1"', '"amount":"100000"'), sig); // c. Correct signature over a body for another chain id. body = mk(next_nonce, 'divi-games-mainnet-1'); await submit('wrong chain id', body, key.signMessage(body)); // d. Replay: the faucet tx from faucet.mjs, submitted again. const last = JSON.parse(fs.readFileSync('last-tx.json', 'utf8')); await submit('replayed tx ', last.body, last.sig);
$ node errors.mjs
wrong nonce : HTTP 400 {"error":"nonce: expected 1, got 6"}
tampered body : HTTP 400 {"error":"bad tx: invalid signature"}
wrong chain id: HTTP 400 {"error":"bad tx: invalid transaction: wrong chain id"}
replayed tx : HTTP 409 {"error":"duplicate tx"}
The order of checks on the node is fixed, and it decides which message you get when more than one thing is wrong:
- Shape (
bad tx: invalid transaction: …, HTTP 400): body over 32 KiB, not JSON,vnot 1,wrong chain id,signer address is on the wrong network, missingaction.type, memo over 140 characters. - Signature (
bad tx: invalid signature, HTTP 400): the key recovered from the signature does not hash tofrom. Tampering with one byte of the body lands here, because the signature was over different bytes. - Duplicate (
duplicate tx, HTTP 409): the same txid is already in a block or in the mempool. Since the txid is the hash of the body, resending the same body is always a duplicate. - Nonce (
nonce: expected N, got M, HTTP 400): not the account's next nonce. - Mempool full (HTTP 503): more than 10,000 waiting transactions. We did not reproduce this one on the public node; it comes from the sequencer source.
Everything that passes these checks gets a receipt in the next block (one every 2 seconds while there is something to include). A receipt can still be ok: false with an error string when the action itself fails, for example faucet cooling down; next claim at height N if you claim GOLD twice within 1800 blocks. The nonce is consumed either way.
7Linking an address on the site
The site at games.divi.domains identifies people through Telegram and lets each of them attach up to 10 addresses. To prove you hold an address, the site shows you one sentence and asks for a signature over it, made with any of the four methods in step 3:
games.divi.domains wants to link <address> to telegram:<your telegram id> nonce:<challenge number>
The nonce is a fresh challenge id from the site's database. It is valid for one hour and for one use, so a signature someone copies from you cannot be sent again. The site runs the same recovery check as the gamechain and answers That signature does not match the address and message. on failure.
// link.mjs: the sentence games.divi.domains asks you to sign to link an address, and how the site checks it. // node link.mjs import fs from 'node:fs'; import { Key, verifyMessage } from './sdk/divigames.js'; const key = Key.fromWif(fs.readFileSync('player.wif', 'utf8')); const telegramId = 123456789; // your Telegram user id const nonce = 42; // the site's challenge number, shown with the sentence const sentence = `games.divi.domains wants to link ${key.address} to telegram:${telegramId} nonce:${nonce}`; console.log('sentence ', sentence); const sig = key.signMessage(sentence); console.log('signature', sig); // The site runs the same check (dg_core::crypto::verify_message), then marks the nonce used. console.log('site accepts ', verifyMessage(key.address, sentence, sig)); console.log('old nonce replayed ', verifyMessage(key.address, sentence.replace('nonce:42', 'nonce:43'), sig));
$ node link.mjs sentence games.divi.domains wants to link yFdxP45jC2kma8snwQMNWFBqZnfQQ6kWGg to telegram:123456789 nonce:42 signature IBy1ds4zv5KD8ZpNx+GByEPCwqzi5PeP9v8Bjel4hVG1Nbi/fYqIbDE1a8FqNSVq8J9qAnOjBMl+ADm5+PIaFZU= site accepts true old nonce replayed false
The Telegram id and nonce here are made up so the sentence can be shown; the site fills in the real ones when you click Get message to sign. The check is the same.
With the Telegram wallet this is one command. The site shows the sentence as /sign testnet games.divi.domains wants to link <address> to telegram:<id> nonce:<n>, and if you let the site's bot message you when you logged in with Telegram, the bot also sends you that command. Tap it to copy it, send it to @divi_wallet_bot, sign, and paste the signature back on the site.
This is the whole pattern. Whether it is a game move, a faucet claim or a profile link, "signing in with Divi" is always: the service writes down exactly what it wants you to agree to, you sign it, and the service recovers your public key and compares it with your address.
8Play a game as your wallet (session keys)
Signing every move in your wallet would mean a wallet prompt for every move. So the game pages ask your wallet to sign once. The page makes a brand-new key in your browser, the session key. Your wallet signs one sentence that says this key may play as your address until a set time. From then on the session key signs each move, with no more prompts, and the moves count for your address.
The sentence is fixed. The time is UTC, to the second:
Divi Games session: key <session key address> may play as this address on divi-games-testnet-1 until <YYYY-MM-DDTHH:MM:SSZ>
The SDK builds it for you with sessionText(key, until), where until is in unix seconds. Here is the whole thing, run on this machine with the SDK from games.divi.domains/sdk/:
// session.mjs: the text your wallet signs to let a fresh key play as you, and the tx that starts the session. // node session.mjs import fs from 'node:fs'; import { Key, sessionText, signTx, verifyMessage } from './sdk/divigames.js'; const owner = Key.fromWif(fs.readFileSync('player.wif', 'utf8')); // your wallet const key = Key.generate('testnet'); // the session key: new, never used before const until = Date.UTC(2026, 10, 22) / 1000; // 2026-11-22 00:00 UTC, in unix seconds const text = sessionText(key.address, until); const sig = owner.signMessage(text); // the one prompt in the wallet console.log('owner', owner.address); console.log('key ', key.address); console.log('text ', text); console.log('owner signed it ', verifyMessage(owner.address, text, sig)); console.log('key signed it ', verifyMessage(key.address, text, sig)); // The KEY sends the session tx from its own address, nonce 0. const tx = signTx(key, { nonce: 0, action: { type: 'session', owner: owner.address, until, sig } }); console.log('body ', tx.body);
$ node session.mjs
owner y2azF67bUK4EpMqzN1znCEjcrxjayFvzop
key yBvgVDuasdx7hGU1aJDJDqcNANgHjhnb76
text Divi Games session: key yBvgVDuasdx7hGU1aJDJDqcNANgHjhnb76 may play as this address on divi-games-testnet-1 until 2026-11-22T00:00:00Z
owner signed it true
key signed it false
body {"v":1,"chain":"divi-games-testnet-1","from":"yBvgVDuasdx7hGU1aJDJDqcNANgHjhnb76","nonce":0,"action":{"type":"session","owner":"y2azF67bUK4EpMqzN1znCEjcrxjayFvzop","until":1795305600,"sig":"H8Zhr73fDVrZTweHc7ZVvQ2TN7ehmAFDRc7X3f5F6FcxKzyzTQEfish35qcRCnl9s3hzYbwuZaGrNx7CSMXORN0="}}
Your addresses will differ: both keys are new each run. The script only builds the transaction. Send it with POST /v1/tx exactly like the faucet claim in step 5. Note who sends what: the session key sends the session transaction from its own address, and the sig inside it is your wallet's signature of the sentence.
The node checks the session action like this. Each failure is a receipt with ok: false and this error:
| Rule | Receipt error if broken |
|---|---|
owner is a testnet address | invalid: `owner` is not a Divi testnet address |
| The key is not its own owner | invalid: a key cannot be its own session owner |
| The key never had a session before (one key, one session, ever) | conflict: this key already had a session; use a fresh key |
until is after the block time and at most 30 days after it | invalid: `until` must be after the block time and at most 30 days ahead |
sig is the owner's signature of the exact sentence | not allowed: `sig` is not the owner's signature of the session text |
On success the receipt has a session.started event with key, owner and until. The session key's account then shows it: GET /v1/account/<key> has "session": {"owner": …, "until": …, "ended": false}. Every other account shows "session": null, your own included.
What a session key may do
gameandfaucetactions run as you. The move is yours and faucet GOLD lands on your address.session_endruns as the key itself (see below).- Anything else is refused with
not allowed: session keys can only play games. That includestransfer,burnand a bridge withdrawal. A session key can never move your money.
The key still uses its own nonce, starting at 1 after the session transaction. These rules hold while the session is live: not ended, and the block time is before until. After that the key is just an ordinary empty address again.
Ending a session
- Log out: the session key sends
{"type":"session_end"}. - Revoke: you send
{"type":"session_end","key":"<session key address>"}from your own address. Use this if the browser that holds the key is lost.
Either way the receipt has a session.ended event with key, owner and by. Anyone else gets not allowed: only the session key or its owner may end a session, and a second end gets conflict: session already ended. An ended key cannot be bound again; make a new one.
On the game pages
The Tic-tac-toe, Tower Defense and Blackjack pages do all of this for you. Without a wallet they play with a guest key the page keeps in your browser. Under Play as your wallet there are three buttons:
- Browser Wallet: the IronDivi extension connects, then asks you to approve the sentence.
- Web Wallet: the IronDivi Web Wallet opens in a pop-up. Unlock it, check the sentence and approve it. If no message shows after you unlock, press Send the request on the game page. The page gives up after 5 minutes.
- Telegram: the page shows
/sign testnet <sentence>with a Copy button. Send it to @divi_wallet_bot, sign in the wallet the bot opens, copy the signature there, and paste it into the page.
The page asks for 30 days minus one hour, so the node's 30-day limit is never hit by a slow clock. When it says Connected. You now play as …, the session is on chain. The session (including the session key) is kept in this browser's local storage, so it survives a reload. Anyone who can use this browser can play as you until it ends, but cannot spend your coins.
Disconnect forgets the session in the browser and sends session_end from the key, so the key is worthless afterwards. If the session was ended somewhere else (you revoked it, or logged out in another tab), the page says Your wallet session has ended. Connect again to play as your wallet. the next time it opens, and goes back to the guest key. A session that has run out (or has less than a minute left) is dropped without a message.
When the chain runs a bridge, a connected page also shows a tDIVI panel: deposit (with your address as the memo) and withdraw. A withdrawal is signed by your wallet, not the session key, because a session key may not move funds. How deposits and withdrawals work is in Anchors and the bridge. On 2026-10-23 the live chain runs no bridge, so the panel stays hidden.
If something goes wrong
| You see | What it means |
|---|---|
verifymessage returns false for a signature you know is right | The message is not byte-for-byte the same: a trailing newline, a different quote style, or a shell that changed \n. Compare messageHash on both sides. |
unsupported signature header from the SDK | Header byte outside 31–34: the signature is from an uncompressed key or not a Divi/Bitcoin-style signature at all. All Divi wallets sign with compressed keys. |
Not connected. Call connect() first. | The extension has not approved your site. Call await window.irondivi.connect() and use the address it returns. |
bad tx: invalid signature | The signature is over different bytes than the body you sent, or made with a key for a different address than from. |
nonce: expected N, got M | Read next_nonce from /v1/account/<address> right before signing, and send transactions from one address one at a time (or track the nonce yourself). |
duplicate tx (409) | You resent the same body. If you meant a new transaction, the nonce (and so the body and txid) must differ. |
signer address is on the wrong network | A mainnet D address on the testnet gamechain. Make a testnet key: Key.generate('testnet'). |
conflict: this key already had a session; use a fresh key | Every session needs a brand-new key. Make one with Key.generate('testnet') and sign a new sentence for it. |
not allowed: `sig` is not the owner's signature of the session text | The wallet signed a different sentence (another key, another time, or a changed character), or a different address than owner signed it. Build the sentence with sessionText and sign exactly that. |
invalid: `until` must be after the block time and at most 30 days ahead | Pick a time in the future and no more than 30 days away. Leave an hour of slack, as the game pages do. |
not allowed: session keys can only play games | A session key tried to transfer, burn or withdraw. Sign that transaction with your wallet instead. |
| The Web Wallet pop-up shows no message after you unlock it | Press Send the request on the game page. If the pop-up never opened, allow pop-ups for games.divi.domains. |
Related: Sign and verify a message (the wallet side), Browser extension API, The JavaScript SDK, Run the gamechain and Build tic-tac-toe.
Still stuck? Ask the Divi chatbot, or see all tutorials.