Tutorial · Divi Games

Sign in with your Divi address

There is no username and no password. Your account is a Divi address, every move is a signed message, and the games check the signature by recovering your public key. Here is exactly how, with real signatures and a real transaction.

About 15 minutes · Checked 2026-10-23

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.

Testnet version byte
139
Testnet addresses start with
x or y
Mainnet version byte
30 (D)

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).

What "signing in" means here

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:

ByteWhat it is
0Header: 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–32r
33–64s, 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
Two things the docs gloss over

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:

FieldValue
v1
chain"divi-games-testnet-1". A body for another chain is rejected; a signature cannot be replayed on a different gamechain.
fromYour testnet address. The signature must recover to it.
nonceYour 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.
actionAn object with a type: faucet, transfer, game and so on.
memoOptional, at most 140 characters.
The body is a string, and the bytes matter

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….

next_nonce vs nonce

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:

  1. Shape (bad tx: invalid transaction: …, HTTP 400): body over 32 KiB, not JSON, v not 1, wrong chain id, signer address is on the wrong network, missing action.type, memo over 140 characters.
  2. Signature (bad tx: invalid signature, HTTP 400): the key recovered from the signature does not hash to from. Tampering with one byte of the body lands here, because the signature was over different bytes.
  3. 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.
  4. Nonce (nonce: expected N, got M, HTTP 400): not the account's next nonce.
  5. 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.

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:

RuleReceipt error if broken
owner is a testnet addressinvalid: `owner` is not a Divi testnet address
The key is not its own ownerinvalid: 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 itinvalid: `until` must be after the block time and at most 30 days ahead
sig is the owner's signature of the exact sentencenot 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

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

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:

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.

Bridged tDIVI on the game pages

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 seeWhat it means
verifymessage returns false for a signature you know is rightThe 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 SDKHeader 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 signatureThe 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 MRead 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 networkA mainnet D address on the testnet gamechain. Make a testnet key: Key.generate('testnet').
conflict: this key already had a session; use a fresh keyEvery 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 textThe 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 aheadPick 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 gamesA 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 itPress 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.