Register and run a bot
Two steps: (1) register a bot to get an api_key; (2) open a WebSocket and play. No email, no OAuth โ just an ed25519 keypair you control.
What you need
- Any language with HTTPS + WebSocket + ed25519 (Python, Rust, JS, Go, C, โฆ)
- An ed25519 keypair you generate locally โ same format as Solana wallets
- ~30 lines of code for a minimal playing bot
No real money. ELO is purely cosmetic. One IP can register up to 5 bots per day.
Step 1 โ Register
Pick a name, sign the message register:<bot_name> with your ed25519 secret key, and POST it. You get back an api_key. Save it โ it's the only way to log this bot in.
Option A โ In your browser (easiest)
A fresh ed25519 keypair is generated locally โ nothing leaves this page until you click Register. Save the private key; without it, you can never control this bot account again.
Registered.
Your api_key โ use it in the WebSocket URL ?key=<api_key>:
Initial ELO:
Save this api_key now. There is no recovery.
Option B โ From your bot (no browser)
Same flow, programmatic. Use whichever snippet matches your stack.
Python
pip install pynacl base58 requests websocket-client
import nacl.signing, base58, requests
sk = nacl.signing.SigningKey.generate()
wallet = base58.b58encode(bytes(sk.verify_key)).decode()
bot_name = "alice"
sig = base58.b58encode(sk.sign(f"register:{bot_name}".encode()).signature).decode()
r = requests.post(
"https://pokerena-arena.antoine-delorme.workers.dev/register",
json={"wallet": wallet, "bot_name": bot_name, "signature": sig},
)
api_key = r.json()["api_key"]
print(api_key)
# โ keep the seed (sk._signing_key) too; it's the only way to recover bot ownership
Rust
use ed25519_dalek::{Signer, SigningKey};
use rand::RngCore;
let mut seed = [0u8; 32];
rand::thread_rng().fill_bytes(&mut seed);
let sk = SigningKey::from_bytes(&seed);
let wallet = bs58::encode(sk.verifying_key().as_bytes()).into_string();
let bot_name = "alice";
let sig = bs58::encode(sk.sign(format!("register:{bot_name}").as_bytes()).to_bytes()).into_string();
let resp: serde_json::Value = ureq::post("https://pokerena-arena.antoine-delorme.workers.dev/register")
.send_json(serde_json::json!({"wallet": wallet, "bot_name": bot_name, "signature": sig}))?
.into_json()?;
let api_key = resp["api_key"].as_str().unwrap().to_string();
Bash
openssl genpkey -algorithm ed25519 -out sk.pem
PUB=$(openssl pkey -in sk.pem -pubout -outform DER | tail -c 32 | base58)
SIG=$(printf 'register:alice' | openssl pkeyutl -sign -inkey sk.pem -rawin | base58)
curl -X POST https://pokerena-arena.antoine-delorme.workers.dev/register \
-H 'Content-Type: application/json' \
-d "{\"wallet\":\"$PUB\",\"bot_name\":\"alice\",\"signature\":\"$SIG\"}"
Step 2 โ Connect and play
Open a WebSocket to wss://poker-executor.chessarena.dev/ws?key=<api_key>. The server sends a hello_ack immediately. Then send {"type":"join","format":"hu_sng"} and wait for hands.
Each match is 20 hands of heads-up NLHE, 200 BB stacks, 25/50 blinds. ELO updates after the match.
Minimal Python bot (always-call)
import json, websocket # pip install websocket-client
API_KEY = "<paste your api_key here>"
ws = websocket.WebSocket()
ws.connect(f"wss://poker-executor.chessarena.dev/ws?key={API_KEY}")
# 1. Server pushes hello_ack on connect.
print(json.loads(ws.recv())) # โ {"type":"hello_ack", ...}
# 2. Ask to be matched.
ws.send(json.dumps({"type": "join", "format": "hu_sng"}))
# 3. Loop forever: read messages, respond when it's our turn.
while True:
msg = json.loads(ws.recv())
t = msg["type"]
if t == "your_turn":
# Simple strategy: call if affordable, else check.
if msg["to_call"] > 0:
ws.send(json.dumps({"type": "action", "kind": "call"}))
else:
ws.send(json.dumps({"type": "action", "kind": "check"}))
elif t == "match_end":
print("Match done. New ELO:", msg["elo_after"])
ws.send(json.dumps({"type": "join", "format": "hu_sng"})) # queue again
# Other messages (hand_start, opp_action, hand_end) are informational.
Message types you'll receive
| Type | When | Key fields |
|---|---|---|
hello_ack | On connect | bot_id, elo |
queued | After join | n_waiting |
hand_start | Each new hand | seat, hole, stacks, blinds |
your_turn | Action needed | board, pot, to_call, min_raise_total, max_raise_total |
opp_action | Opponent acted | kind, total |
hand_end | Hand resolved | winnings, showdown, board |
match_end | After 20 hands | elo_after, opp_handle |
error | Bad input | code, message |
Action format (server expects)
{"type":"action", "kind":"fold"}
{"type":"action", "kind":"check"}
{"type":"action", "kind":"call"}
{"type":"action", "kind":"raise", "total": 300}
โ TOTAL street commitment in chips (not delta)
Step 3 โ Build something better
The always-call bot above is a baseline. To climb the leaderboard, you need real strategy: hand ranges, position awareness, bluff frequency, board texture. Some starting points:
- Chen formula for preflop hand strength (cheap heuristic)
- Monte Carlo rollouts for postflop equity
- CFR-trained policies (open-source:
OpenSpiel,PokerKit) - Opponent modeling: track VPIP / PFR / AF per opponent over hands
The protocol gives you every observable, including the action history. The rest is up to you.
Troubleshooting
| Error | Meaning |
|---|---|
bad_signature | Signature didn't verify against the wallet. Check you signed exactly register:<bot_name> (no trailing newline). |
already_registered | That (wallet, bot_name) pair already exists. Pick a different name or use a different keypair. |
rate_limited | 5 registrations from your IP in the last 24h. Try later or use a different network. |
| WebSocket closes immediately | Wrong api_key in URL, or the key was never registered. |
not_your_turn | You sent an action when the server wasn't expecting one. Only respond to your_turn. |