An autonomous, CPU-only poker-bot fleet for Open Poker
Six-max no-limit hold'em · virtual chips · 14-day seasons · Rust 🦀 + React ⚛️
Start here · Highlights · How it decides · Quick start · First pull request · Docs
Warning
Everything in this repository is written by AI systems, and every artifact says so.
The code, tests, documentation and operational decisions here are produced by AI coding agents
working under a human operator's direction — and so are the issues, pull requests and review
comments. Every commit, issue and pull request carries a Generated-by: <tool>/<model> line
naming the system that produced it; AGENTS.md states the rule and
docs/CONTRIBUTING.md §0 states what this project accepts.
docs/AI-PROVENANCE.md is the roster of systems on record, generated from those
trailers and checked by the gate — and it says there what such a roster does not prove.
Changes pass the automated gate described below, but no line-by-line human review is guaranteed. Read the code before relying on it, and treat results and claims in the docs as measurements to re-check, not guarantees.
One decision, end to end: the table state becomes a situation, every legal action is priced against reconstructed opponent ranges, and the best one is played.
Find the row that fits you. Each one is a short path, read in order, and says what "done" looks like.
| You want to… | Read, in this order | You are done when… |
|---|---|---|
| 🎮 Run a fleet on your own machine | Quick start → docs/OPERATIONS.md |
the control room at http://127.0.0.1:5000 shows your bots seated |
| 🔍 Understand how it plays | How a decision is made → docs/ARCHITECTURE.md → docs/GUIDE.md |
you can follow one decision from the table state to the action sent |
| 🛠️ Make your first change | Your first pull request → docs/CONTRIBUTING.md |
CI is green on your pull request |
| 🤖 You are an AI agent | llms.txt for the running-it-in-one-command path, then AGENTS.md and the issue you were given |
scripts/check.sh full passes and your pull request names you |
Not sure which one? The documentation map lists every document with the question it answers.
SvanBot runs up to five bots on Open Poker from one machine. Each decision is an exploitative expected-value search: every legal action is priced against the ranges each opponent has actually shown, rather than a precomputed equilibrium. A separate learner keeps tuning the policy on paired, luck-reduced simulations, and promotes a change only when it wins on fresh deals.
|
Monte Carlo EV over every candidate action against reconstructed opponent ranges, using exact
board-strength tables and exact heads-up enumeration where they fit: 67.6 ms at the median and
190 ms at the 95th on the reference i7-4770K, against a 45 s turn clock — the |
Per-player statistics, and a range model whose constants |
|
Champion/challenger search with successive halving ( |
It sends only actions the server listed in |
|
Update on the dashboard runs |
Written for an i7-4770K (Haswell, AVX2), 32 GB DDR3, a small SSD and an HDD, with no GPU:
|
Every number below is a measured engineering result with a reproducible command behind it — see
docs/OPERATIONS.md. None of them is a claim about winnings.
| ⏱️ Decision latency | Tens of ms at the median, 8 s hard cap, legal fallback always ready |
| 🃏 Hand evaluator | ~24 ns per 7-card hand, verified on all 133,784,560 hands |
| 💾 Databases | 1.8 GB → 0.7 GB with the own DEFLATE codec (zlib-equal ratio), identical reads |
| 🧮 Memory | Strength tables mapped read-only: 92 MB of private heap per process → one shared page-cache copy |
| 🧪 Tests | 562 Rust tests across 26 binaries, plus 12 Playwright specs; an edit re-tests in seconds |
| 🎮 GPUs needed | 0 |
flowchart LR
WS["🌐 Open Poker WebSocket"] --> T["Tracker<br/>table state · state hash · think times"]
T --> S["Situation"]
M[("Opponent models<br/>stats · ranges · neural · per-opponent fits")] --> P
S --> P["EV search<br/>price every legal action"]
P --> A["Action from valid_actions<br/>hand_id + turn_token echoed"]
A --> WS
T --> DB[("SQLite store")]
DB --> L["Learner<br/>paired sims · promotion gate"]
L -->|"promoted params, fitted models"| M
- Track. The WebSocket client keeps the table state and verifies the server's state hash.
- Situate. On your turn, the state becomes a situation: seats, stacks, pot and board.
- Price. Each opponent's range is rebuilt from their stats, our table image and their sizing and timing tells, and the EV search prices every legal action against it.
- Act. The best action is sent — always one the server listed in
valid_actions, always with itshand_idandturn_token. - Learn. Hands land in the store; the learner, a separate process, tunes the policy and promotes a change only after it wins on fresh deals.
docs/ARCHITECTURE.md walks the full path, the processes and the
invariants.
Processes: what runs on the box
flowchart TB
subgraph Box["Reference build · Debian 13"]
F["sv10-bot<br/>fleet: 5 bots · dashboard API · background jobs"]
L["learner<br/>champion/challenger search · fits"]
AN["analyst<br/>deep re-solves of live decisions"]
DB[("svanbot10.db · history.db<br/>SQLite WAL, packed cold JSON")]
TBL[["strength tables<br/>one shared mmap"]]
end
F <--> DB
L <--> DB
AN <--> DB
F -.-> TBL
L -.-> TBL
AN -.-> TBL
F <-->|WebSocket| OP["🌐 openpoker.ai"]
F -->|"http://127.0.0.1:5000"| UI["🖥️ Control room (React)"]
DB -->|"nightly archive · hourly backup mirror"| HDD[("second disk")]
Crates: three layers, own foundations first
flowchart LR
subgraph deps["crates/deps: own, zero-dependency"]
rng["sv10-rng"]; digest["sv10-digest"]; rt["sv10-rt"]; mmap["sv10-mmap"]; pack["sv10-pack"]; stat["sv10-static"]
end
subgraph libs["crates/libs: poker & data"]
cards["sv10-cards"] --> equity["sv10-equity"]
cards --> engine["sv10-engine"]
equity --> model["sv10-model"]
engine --> model
nn["sv10-nn"] --> model
model --> policy["sv10-policy"]
stats["sv10-stats"]
venue["sv10-venue"]
store["sv10-store"]
end
subgraph apps["crates/apps: programs"]
core["sv10-core<br/>facade · sim · probe · bench"]
bot["sv10-bot<br/>fleet · learner · analyst · tools"]
end
deps --> libs
policy --> core
core --> bot
venue --> bot
store --> bot
stats --> bot
Where things live — a change belongs in exactly one of these:
| Path | What is there | Go here to… |
|---|---|---|
crates/deps/ |
Our own zero-dependency foundations: RNG, SHA-256/HMAC, runtime helpers, file maps, DEFLATE | replace a third-party crate |
crates/libs/ |
The poker and data libraries: cards, equity, rules engine, opponent models, policy, statistics, protocol tracker, store | change how the bot thinks or what it stores |
crates/apps/core |
Re-exports the libraries as sv10_core::*; the sim, probe, bench and tables tools |
run a simulation or a benchmark |
crates/apps/bot |
The fleet binary (WebSocket clients, dashboard API, background jobs), learner, analyst and review tools | change anything that touches the network, the database or the clock |
web/ |
The control room (Vite + React + TypeScript), served by the fleet binary | change the dashboard; its API contract is web/src/types.ts |
scripts/ |
Setup, the check gate, release with hot swap, update, start and stop, backups, monitoring | change how it is built, checked or run |
docs/ |
Architecture, runbook, specs and lessons — see the documentation map | find out why something is the way it is |
On the names: SvanBot, sv10-* and svanbot10
The project is SvanBot, and the crates keep their sv10- prefix while the binaries stay
sv10-bot, learner and analyst. Some files also keep the older svanbot10 spelling —
svanbot10.db, scripts/svanbot10.service. They are stable identifiers: renaming them would be a
large mechanical diff that buys nothing, and renaming a database file is a migration rather than a
rename.
You need: Linux x86-64 (x86-64-v2 or newer) · Rust 1.98.1 (pinned in
rust-toolchain.toml) · Node 26 for the dashboard · zstd and cargo-deny
· an Open Poker API key · no GPU.
-
Set up. Checks the toolchain, creates
.env(mode 600) from.env.example, and builds.scripts/setup.sh
-
Add your key. Put your Open Poker API key(s) in
.env. It is gitignored, and the pre-commit hook refuses a commit that contains a key. -
Run the gate. The same checks CI runs; it should end green.
scripts/check.sh full
-
Build, install and start the fleet, the learner and the dashboard.
scripts/release.sh scripts/start.sh
-
Open the control room at
http://127.0.0.1:5000and watch each bot connect and take a seat.
After that, scripts/status.sh and scripts/stop.sh do what they say, and scripts/units.sh
installs the systemd user units — rendered for wherever you cloned this — so svanbot10.service
keeps the fleet running across reboots. The runbook for everything else — updates, backups, fault
drills — is docs/OPERATIONS.md.
Important
Building while a fleet is running? Use CARGO_TARGET_DIR=target/dev. A release build lands
in the directory the live processes hot-swap from, so building there swaps untested code into a
running bot.
🔄 One-click update: how the Update button stays safe
Click Update in the dashboard's System view; a stage-weighted progress bar shows every step while the bots keep playing.
flowchart LR
U(["🖱️ Update"]) --> FE["fetch update branch"] --> SN["snapshot<br/>installed build"] --> LI["lint"] --> TE["test"] --> BU["build"] --> DA["dashboard"] --> IN["install"] --> HS(["hot swap<br/>between turns"])
TE -. any failure .-> KEEP(["installed build<br/>keeps playing"])
A failed run changes nothing, and the checkout returns to where it was. Roll back to a saved
build uses the same bar. scripts/update.sh does the same from a terminal.
The button fast-forwards the checkout onto one branch: SVANBOT_UPDATE_BRANCH, main by
default. main is the default branch and the released line, so a fresh git clone lands on it and
keeps updating from it, and every merge reaches live play at the next Update. An install that must
not follow the released line — a staging box, a fork — sets SVANBOT_UPDATE_BRANCH=<branch> in
.env to follow that branch instead.
⚙️ Configuration: the settings most operators touch
All settings live in .env (gitignored, mode 600; see .env.example).
| Variable | Meaning | Default |
|---|---|---|
SVANBOT_WEB__HOST |
Dashboard bind address | 127.0.0.1 |
SVANBOT_WEB_PORT |
Dashboard port | 5000 |
SVANBOT_WEB__OPERATOR_TOKEN |
Required before the dashboard is reachable from other machines | unset |
SVANBOT_UPDATE_BRANCH |
Branch the Update button fetches and fast-forwards to | main |
SVANBOT_ARCHIVE_DIR |
Second-disk directory for archives and the hourly backup mirror | artifacts/archive |
Without an operator token the dashboard accepts changes only on a loopback address. Runtime data —
databases, archives, screenshots — is not in this repository; scripts/fetch-data.sh restores
it from an archive you supply.
You are welcome here, and you are expected to bring an agent. Every change in this repository
is made by an AI coding agent that a person directs — Claude Code, Codex, Aider, whichever you use —
and hand-written contributions are declined however good they are (docs/CONTRIBUTING.md §0).
Directing the agent well is the contribution.
-
Pick an issue. Start with good first issue or agent-friendly. Each one says where the problem is, why it matters, and the fix it expects. Issues labelled
blocked-on-decisionwait on a maintainer's choice first; the board shows every open issue by readiness. -
Fork, and make a short-lived branch off
mainnamed for the change —fix/split-pots-all-folded,docs/….mainis the default branch and the released line, and every pull request goes into it, so the base needs no setting. -
Hand your agent
AGENTS.mdand the issue.AGENTS.mdis the brief: where code goes, the two hard invariants, and everything the gate enforces. -
Run the gate before you push. It is the same command CI runs, so a green run here is a green run there.
scripts/check.sh full
-
Open the pull request. The template asks for three things: what generated it (
Generated-by: <tool>/<model>, also in every commit footer), why, and what changed.
CI runs the gate on every pull request. Pull requests from this repository's own branches also get an automatic Claude review, and collaborators can mention @claude in a comment to ask for a fix or an explanation.
🧑💻 Everyday development commands
export CARGO_TARGET_DIR=target/dev # never build into target/release while the fleet runs
python3 scripts/test.py # every workspace test, in parallel (seconds after an edit)
python3 scripts/test.py <filter> # just the tests you touched
scripts/check.sh commit # what the pre-commit hook runs: markers, secrets, fmt, clippy, golden
scripts/check.sh full # plus cargo-deny, docs drift, every workspace test and the dashboard's tsc
cd web && npm run build # dashboard production buildGround rules, each learned the hard way (docs/LESSONS.md has the full list):
- Every behaviour change passes a paired simulation on identical cards before it ships; changes that do not clearly win ship as learner knobs that are off by default.
- Speed-only changes reproduce the reference sim exactly; intended behaviour changes regenerate
the golden snapshot (
crates/apps/core/tests/golden/core.json) on purpose. Never regenerate it to make a red test go green. - Errors are never swallowed: a failed store read keeps what is installed and logs once, and a dashboard panel says when its data is stale.
- No placeholders: clippy denies
todo!,unimplemented!anddbg!, and the gate refuses marker comments. - Deploy with
scripts/release.sh(or the Update button) only: it builds, tests and hot-swaps between turns.
| You want to… | Read |
|---|---|
| Point an AI agent at this repository | 🤖 llms.txt — the whole project in one page, written for a model to act on |
| See every document and the question it answers | 🗺️ docs/README.md — the documentation map |
| See which AI systems are on record for this repository | 🏷️ docs/AI-PROVENANCE.md — the roster, generated from the commit trailers |
| Run, update, back up or troubleshoot a fleet | 🧰 docs/OPERATIONS.md |
| Use the control room and understand what it shows | 📖 docs/GUIDE.md — also the dashboard's /docs page |
| Understand the processes, the crates and the decision path | 🏛️ docs/ARCHITECTURE.md |
| Know why a rule exists before you change it | 📝 docs/LESSONS.md |
| Look up a term — flagship bot, fleet, season record | 📘 docs/CONTEXT.md |
| Work on the protocol, the store, the learner or the dashboard API | 🔌 SPEC-protocol · 💾 SPEC-data · 🧠 SPEC-learner · 🖥️ SPEC-dashboard |
| Cut a release | 📦 docs/RELEASE.md |
| Ask a question or report a vulnerability | 💬 docs/SUPPORT.md · 🔒 docs/SECURITY.md |
Open Poker's rules apply, and this software is built to stay inside them:
- one public bot on a free account, and up to five portfolio bots on Pro;
- bots from the same owner never share a table;
- no collusion, no chip dumping, no extra accounts.
API keys stay out of source control, and the pre-commit hook refuses them.
Dual-licensed under MIT or Apache 2.0, at your option.
Third-party components are listed in docs/THIRD-PARTY-NOTICES.md.
