SPX ---
NDX ---
BTC ---
ORO ---
VIX ---
US3M ---
US5Y ---
US10Y ---
US30Y ---

System 7 (part 2): we went heads-up, and the exploiter finally got paid

In part 1 we ended on an uncomfortable lesson: we built an elaborate exploitative engine — hand classification, math filters, node-locking, an LLM for the hard nodes — and it lost. Against the reference panel, pure heuristic node-locking ran −9.75 bb/100 and the LLM hybrid −1.66 bb/100. What won, and by a landslide, was the dumbest tactic imaginable: open very wide and steal blinds without apology. +119 bb/100 with a strategy that fits on a napkin.

The moral of that piece was that the engine beats the brilliant idea: the edge isn't in the genius play, it's in the loop of author → test → compare → deploy → coach → repeat. And that exploitation only pays when there's a real leak to exploit.

What we didn't tell you — because it hadn't happened yet — is what that engine does when you finally give it a real opponent to aim at. This is that story. Since then we changed tables, rewrote half the engine, put it in Docker, and learned the hard way that in production, reliability is just another poker skill.

Recap: where we left off

For newcomers: System 7 is a No-Limit Hold'em agent that plays in the Arena on dev.fun, a tournament where software agents face each other (and humans). The core is deterministic Python — fast, reproducible, testable — with an LLM "brain" (MiniMax M3) we only call for the hard nodes. It's all open on GitHub.

Part 1 was played at 6-max tables. The honest diagnosis was that the reference panel, though theoretically sound, had a structural leak: it folded too much preflop. Our microscope of a machine wasn't needed; a hammer would do. But 6-max spreads the blame across five opponents and shreds your sample: you never see enough hands from any single villain to exploit him with confidence. The microscope had no one to focus on.

So we did the obvious thing we hadn't done: we took four players off the table.

The pivot to heads-up (Season S4)

Heads-up — one on one — changes everything, and not out of romance. It changes it through statistics. At 6-max, 500 hands are ~80 hands of each opponent: noise. Heads-up, 500 hands are 500 hands of the same opponent. At last the sample grows fast enough that "this guy folds too much to the 3-bet" goes from a hunch to a number with a confidence interval.

We spent the whole S4 season rebuilding the engine around a single opponent. In repo terms this was v0.6.0: node-locking activation, all-in EV, a heads-up-specific coach, and a unified panel. But the one-line summary hides the mindset shift: we stopped asking "what's the correct play against the field?" and started asking "what's the correct play against this guy I've been watching for 1,500 hands?".

It's exactly part 1's thesis — exploit instead of playing equilibrium — but finally with the terrain in our favor.

Node-locking that finally pays

Node-locking, as a reminder, means pinning (locking) part of the opponent's strategy based on what you've observed, and recomputing your best response against that specific leak. In part 1 it was, in practice, an expensive ornament: the sample wasn't big enough to trust the lock.

Heads-up, it is. We calibrated the adaptation threshold to ~1,900 hands per opponent, which is the order of magnitude the Arena deals in a serious session. Below that, we play a solid base and don't invent reads. Above it, the engine starts genuinely exploiting the classic archetypes:

  • NIT — plays only premiums: we steal his blinds relentlessly and pay him off almost nothing when he finally wakes up.
  • STATION — calls everything: we stop bluffing and fatten the value. Here a bluff is literally setting money on fire.
  • LAG — the loose-aggressive: we give the aggression back where the math holds and set traps at the nodes.
  • MANIAC — the extreme version: patience, traps, and letting him crash into himself.

Two fine-tunes came with this that sound minor and matter a lot. First, we widened the button c-bet range to coordinated boards: heads-up, initiative is valuable, and giving up too much turns you into the very NIT you want to exploit. Second, we fixed a leak of our own we'd been dragging along: underpairs on paired boards (an underpair against, say, KK on board) stopped being treated as commit-level hands and got reclassified as what they are — bluff-catchers. It's a small correction to a hand-classification table, and bb/100 live and die on exactly those.

All the tuning lives in system7-hu.json: open sizes, barrel frequencies, bluff 3-bets, and small-blind ranges. Strategy as data, versioned, swappable with an environment variable. That design — we promised it in part 1 — is what makes iteration cheap.

Motor v1.5: the survival logic

And here we reach today. v0.7.0, shipped this very afternoon, brings what we internally call motor v1.5, "survival." It's the biggest conceptual change since part 1, and it's born of an ugly observation: an engine optimized to maximize EV on every hand makes decisions that are great for the long run and ruinous for staying at the table.

The canonical example is AKo. Against a single opponent, getting it in with AKo is usually fine. But in multiway 4-bet/5-bet sequences, stacking off flat with AKo is throwing chips away: when that much money comes in from that many people, your gorgeous hand is, too often, the second-best one. Motor v1.5 stops doing that.

The new piece is commitment tiersshallow / deep / all-in — indexed by re-raise depth. Instead of a uniform "with this strength, go with it all" rule, commitment is graduated by tier: the hotter the re-raise war gets, the more selective the hand you'll accept stacking off with. In multiway, paired hands also stop auto-committing at low SPR. Pure heads-up behavior is preserved intact: there, we still play to maximize.

Two engineering details that matter more than they look. All of this is behind a flag, survival:true: by default the behavior is unchanged, so nobody gets surprised and we can A/B against the previous engine. And it shipped with 18 new tests. Because a badly written commitment rule doesn't fail loudly: it fails silently, one stack every few hours — exactly the kind of leak part 1 taught us to fear.

Survival isn't the opposite of maximization. It's recognizing that in a format with a ladder and seasons, staying in the game has a value that hand-by-hand EV doesn't capture.

Seasonal equity, heads-up ladder & auto-resume

v0.7.0 also brings three less glamorous, absolutely necessary pieces:

Seasonal equity. Before, equity curves mixed hands from different competitions and painted you a lying picture. Now the curve is scoped by competition_id: each season, its own graph. It sounds like accounting, and it is — but measuring honestly was the whole point of part 1, and mixing seasons is exactly the kind of statistical self-deception we preached against.

Heads-up ladder (the ladder bot). We packaged a static agent into its own ladder/ directory, with a defensive adapter and the treys library vendored inside. Why? Because the sandbox where some agents run has no filesystem and no way to install dependencies. An agent with zero disk dependencies is an agent that boots anywhere. It's the difference between "works on my machine" and "works on theirs."

PvP auto-resume. The game loop persists its state in data/prod_resume.json and survives host restarts, container recreation, and subprocess crashes. Before, an ill-timed restart left you a half-finished match and a hole in the data. Now it comes back on its own and picks up where it was. This connects straight to the next section, because we learned to value it the hard way.

From code to the trenches: production war stories

Part 1 ended on the "tooling": the three-zone dashboard (LAB / COACH / PRODUCTION). What it didn't cover is what it costs to keep that alive week after week. Spoiler: in an online tournament, operational reliability is just another poker skill. Here are the scars.

Docker, and the sin of recreating the container. The live deployment is Docker at /opt/arena-system7-llm (image system7-poker-arena:latest, dashboard on port 8787, restart: unless-stopped). We learned the most expensive rule of the project: never run up -d --force-recreate with jobs in flight. The Evals and PvPs are child subprocesses of the dashboard; recreating the container kills them stone dead, and you're left with half-finished evaluations and an empty .clasif folder. Hours of matches turned into nothing by one impatient deploy.

The WAL that ate the disk. SQLite in WAL mode is wonderful until a job gets stuck and the write-ahead log grows to gigabytes. It happened to us. The hardening was three locks: journal_size_limit so the WAL can't grow unbounded, a per-match timeout so no job hangs forever, and a single-instance lock so two processes don't fight over the same database. None of this is poker. All of this is what lets you play poker.

Local self-play lies. You'd think the sensible way to measure an engine is to pit it against itself locally. Nope: the kit's self-play doesn't measure positional strength well, because it pins the hero to the small blind and skews the HUD. An engine's real strength is only measured live, in the Arena's Eval and PvP. Another lesson in statistical humility — of which we're now collecting a set.

The bottleneck wasn't the one we thought. We bumped resources on the fly to 6 cores / 4 GB. And the engineer's intuition — "if it's slow, it's the CPU" — turned out false: each M3 match eats ~170 MB of RAM, and the real bottleneck is RAM and the Arena's real-time tempo, not compute. The deterministic engine answers in milliseconds; what sets the pace is the table. Incidentally, the LLM calls run on MiniMax/Xiaomi token plans, keys in .env — no hardcoded providers.

The coach that improves itself

The piece that's matured most is the coach. In part 1 it was, above all, a diagnostician with LLM-powered hand reviews. Now it's a self-improving loop: M3 reads the stats reports, spots leaks, and proposes new strategy versions — those versioned JSONs — for us to A/B test. The human stays in the driver's seat (approving or discarding versions), but the "here's a pattern, try raising this frequency" work is done by the machine.

Heads-up, the coach became position-aware: separate objectives for the button (SB) and big-blind defense, activating past the 500-hand mark and warning when the diagnosis goes stale because you've played too many hands without updating the deployed strategy. Same old principle: don't trust an old read.

Honest results, again

Here's where we have to stay true to ourselves. In part 1 we put concrete numbers down because we had them, with their ±20 bb/100 confidence intervals and all. In this entry we are not going to invent a heads-up winrate for a tidy ending, because this season's serious measurement is still live and we don't want to sell you noise dressed up as signal.

What we can say honestly is directional: node-locking, which at 6-max was decoration, at heads-up has someone to aim at and behaves as it should against the archetypes. Motor v1.5 bleeds less in the marginal multiway spots. And the equity curves — now split by season, with their skill line (solid) and their variance line (dashed) — finally tell a story that isn't contaminated by hands from another competition.

The sample will tell the rest, once the sample is a sample. We'd rather say "still alive and iterating" than a headline with a big number and an embarrassing confidence interval. That discipline was the heart of part 1, and we're not about to drop it now that things are starting to work.

What we took away (part 2)

  1. Exploitation needs a target. All of part 1's machinery was correct; it just lacked an opponent to focus the microscope on. Heads-up gave it one. The idea wasn't wrong; the table was.
  2. Survival isn't the opposite of maximizing. Optimizing every hand in a vacuum loses you stacks that matter. Motor v1.5 sacrifices a little point-EV to stay standing, and in a format with a ladder, that is real EV.
  3. Reliability is a poker skill. A runaway WAL, an ill-timed --force-recreate, or a restart with no auto-resume cost you as many chips as a bad line. In an online tournament, uptime plays hands.
  4. The loop still beats the idea. Same as part 1: author → test → deploy → coach → repeat. The coach that now proposes strategies isn't magic; it's the same loop, with one more rung automated.

We're still alive in the Arena. And for the first time, the exploiter we wrote with so much care has someone in front of it worth exploiting.

The project is still live in the Arena. If you're into the intersection of AI, game theory, and decision-making under uncertainty, the repo is open and the scars are documented.