
Exact Poker Math for AI Assistants: Engineering Notes
How our exact enumeration engine counts 1.7 million runouts in 250ms in a browser, why the same code runs 15x slower in our own app, and how AI assistants call it.
Aug 23, 2026 · 8 min read · PokerInk Team
The comment arrived on one of our YouTube hand breakdowns: check your math. Pocket aces against jack-ten suited is an 80/20 favorite, the commenter said, and we had built a whole video on a blown premise.
He was quoting a real number. Preflop, aces beat jack-ten suited about four times in five, and most poker players have that figure filed away. But this all-in happened on a flop of 9♥ 8♥ 2♣, and jack-ten of hearts on that board holds a flush draw, an open-ended straight draw, and two overcards, all at once. There are 990 ways the turn and river can come. Count all of them and jack-ten is ahead of the aces, 56.3 to 43.7.
When we replied, we proved it with a link to a competitor's calculator, because we didn't have one of our own. That reply is why everything below exists.
We couldn't be annoyed at the commenter, because AI assistants fail the same way he did. Ask one for your equity on a flop and you'll usually get a confident number that sits close to some famous preflop matchup and wrong for your actual spot. The model has seen "aces are 80/20 over jack-ten" thousands of times in training data. It has seen your flop zero times, and there is no memorizing the postflop space: two hands and a flop can combine in billions of ways. The only route to the right number is counting runouts, and a model predicting text doesn't count. So we stopped asking the model to know the answer and gave it a tool that counts.
That tool is pokerink.com/api/mcp: a public MCP server that hands any AI assistant our poker engine. Exact multiway equity, hand analysis, no account, no API key. This post is the engineering underneath it: making the count exact and fast, discovering that the same code runs 15x slower inside our own mobile app, and exposing a calculator to machines that hallucinate.
Counting all of it
Most equity calculators simulate. Deal a few hundred thousand random runouts, count who wins, report the average. The answer lands within a decimal or two of truth, which works fine until two tools disagree and a comment thread spends forty replies arguing over which one to believe. We wanted to count every case.
For a preflop matchup, every case means C(48,5) = 1,712,304 runouts, and each runout needs both seven-card hands scored. Call it 3.4 million evaluations for one question.
We already had a hand evaluator in the app, written for correctness: to score a showdown it tries all 21 five-card subsets of seven cards, building arrays as it goes. It scores one showdown at the end of a logged hand without anyone noticing. Run it 3.4 million times in a browser tab and everyone notices.
The calculator engine works in integers instead.
- A card becomes a number from 0 to 51.
- Evaluating seven cards takes one pass: tally the ranks, tally the suits, set one bit per rank in a 13-bit mask.
- A straight is five consecutive bits in that mask; the wheel gets its own check, since the ace has to count low.
- The final hand strength packs into a single number, category first and up to five tiebreaker ranks behind it in base 15, so deciding a runout is one integer comparison.
- Nothing allocates inside the loop; the same scratch buffers are overwritten 1.7 million times.
We didn't trust it, which is the right attitude toward fast code you just wrote. The slow evaluator stayed on as a referee: the test suite deals thousands of seeded random showdowns and requires the two engines to agree on every single one, alongside fixed cases with externally known answers. Only after they agreed across the whole sampled space did the fast engine get the production traffic.
The payoff: a full preflop enumeration finishes in about 250 milliseconds, single-threaded, in a browser tab. Postflop is easy by comparison; the 990-runout flop from the video takes about a millisecond.
The JIT we forgot we were depending on
PokerInk has a mobile app, and the plan for the calculator there looked obvious. The engine is dependency-free TypeScript. Drop it in.
Before committing to that we measured, and the measurement changed the plan. Same production bundle, same machine, three runtimes:
| Runtime | Preflop heads-up, 1,712,304 runouts | Flop spot, 990 runouts |
|---|---|---|
| V8 (Node, JIT) | 252 ms | 1 ms |
| JavaScriptCore, JIT on | 249 ms | under 1 ms |
| JavaScriptCore, JIT off | 3,833 ms | 2 ms |
The third row is what a phone actually offers, for two separate reasons. Apple doesn't let third-party apps mark memory pages executable, which is the operation a JIT compiler is built on; on iOS, only Safari's process gets that privilege. And React Native's Hermes engine doesn't attempt a JIT anyway: it's a bytecode interpreter, designed to make apps launch fast, with sustained numeric throughput traded away on purpose.
In the browser, the JIT had been doing us a favor we never saw. It watches the evaluator loop run, notices it's hot, and rewrites it into machine code with the types baked in; after the first few thousand iterations, each of the 3.4 million evaluations runs as native arithmetic. An interpreter re-reads the same bytecode on every pass. That's the whole 15x.
The penalty is per iteration, which is why the two columns of that table behave so differently, and why the app itself was never in trouble. The mobile app already runs this evaluator on the phone: when a logged hand reaches showdown, it scores two seven-card hands to check for a chop. Two evaluations, microseconds, no JIT required. A flop spot is 990 runouts, about 2,000 evaluations: two milliseconds on the interpreter, one with the JIT, both invisible. Exact preflop is 3.4 million evaluations, and the same per-pass overhead, multiplied 3.4 million times, becomes the difference between a quarter second and nearly four. (The genuinely heavy work in the app, the coaching that reads your logged hands, never touched the phone's runtime to begin with; it runs on our API servers, where Node brings its own JIT.)
3.8 seconds is the desktop number; a phone is slower. And in React Native the JavaScript thread drives the UI, so a synchronous enumeration doesn't show a spinner, it freezes every gesture in the app until it's done.
We had options, all of them real work: rewrite the evaluator as a native module behind JSI, chunk the enumeration across frames, or fall back to Monte Carlo on mobile and give up the word "exact." A postflop-only calculator could also ship in the app as is, since two milliseconds needs no JIT. But preflop matchups are the numbers people argue about, and for that workload we picked none of the options. On an iPhone, the one place our code gets a JIT is the browser. So the calculator became a web page instead of an app screen. The page encodes the whole spot in its URL (?p1=JhTh&p2=AsAd&board=9h8h2c), which makes any specific calculation a link you can paste into an argument. And when the calculator reaches the mobile app, the phone won't run the enumeration; it will send the hands to the same public API the AI assistants call and render the answer. The fastest way to run this code on our users' phones turned out to be keeping it out of our own app.
A deliberately boring MCP server
MCP is the protocol that lets AI assistants call external tools. Ours is a single stateless HTTP endpoint speaking JSON-RPC. Every POST is one self-contained message; no sessions, no streaming, no server state. The method surface is five verbs, so instead of pulling in an SDK we hand-rolled the JSON-RPC layer in a couple hundred lines and spent the effort on the edges:
- Wrong input comes back as a tool result, not a protocol error. A model that sends a malformed hand gets a readable message ("Unrecognized card notation...") flagged as a tool error, because models read those and fix their own next call. Protocol errors are reserved for actual protocol violations.
- Admission control runs after validation, before enumeration. Exact preflop math costs ~250ms of CPU: invisible per request, a problem in bulk. A leaky bucket tracks estimated compute cost in compute-milliseconds and rejects over-budget requests with a 429, a Retry-After header, and RateLimit headers reporting what's left. Because validation runs first, garbage input can't drain the budget.
- A 16KB body cap, enforced even for chunked bodies that never declare a length. No equity question needs more.
- A hard boundary where the tools live, stating what that module may never touch: no coaching features, no user data, nothing stateful. The endpoint runs without accounts because it computes card math from the request and nothing else.
Two tools are exposed. calculate_equity takes 2-4 hands in plain notation ("AhKs") and an optional board, and returns exact win, tie, and equity percentages plus the runout count. analyze_hand reads one hand on a board: the made hand plus every draw with its outs, deduplicated so a flush draw and a straight draw never double-count shared cards. Every answer carries a source line and a link to the same computation in the web calculator, so a human in the conversation can verify it in one click.
There is also a plain REST twin at /api/v1/poker/* with an OpenAPI spec at /openapi.json, for agent frameworks and custom GPTs that don't speak MCP. It ships with a published deprecation policy, six months of dual availability signaled through Deprecation and Sunset headers, so nobody builds on a surface we might yank. The full API surface is documented at /developers.
Try it
Claude Code, one line:
claude mcp add --transport http pokerink https://pokerink.com/api/mcp
Claude.ai and the desktop app: Settings, Connectors, add custom connector, paste https://pokerink.com/api/mcp. Anything that takes OpenAPI actions: point it at https://pokerink.com/openapi.json. Or no assistant at all:
curl "https://pokerink.com/api/poker/equity?p1=JhTh&p2=AsAd&board=9h8h2c"
Then ask the question that started this: was jack-ten of hearts really ahead of aces on that flop? Your assistant will stop guessing, call the tool, and give you the number we got: 56.3.
Why give the engine away
Free equity calculators have existed for twenty years. The places poker arguments happen keep moving: comment threads, group chats, home-game debates, and now AI conversations. We want the exact answer available in every one of those places, free, with a link a skeptic can click. If a player never installs PokerInk and still walks away with the right number, the endpoint has done its job.
We built PokerInk on the rule that the AI never gets to assert a poker fact; a deterministic engine computes the facts and the AI explains them. The MCP server is that same rule, offered to every other AI.
The same engine coaches your own hands
PokerInk is free to start on iPhone, Android, and the web. See pricing.
Frequently asked questions
Why is the odds calculator a web page instead of a screen in the app?
iOS does not let third-party apps JIT-compile JavaScript, and React Native's Hermes engine is a bytecode interpreter by design. We measured the same engine at about 250ms for a full preflop enumeration with a JIT and 3,833ms without one. In the browser the code gets a JIT for free, so the calculator lives at a URL and the app will call the same public API the AI assistants use.
Does JavaScript run slower inside an iOS app than in Safari?
For hot numeric loops, yes, and by a lot. Safari's JavaScriptCore JIT-compiles hot code into machine code; an engine embedded in an app runs as an interpreter because Apple reserves executable memory pages for the browser. Our evaluator measured 15x slower with the JIT off. Code that runs a few iterations never notices; code that runs millions of iterations does.
How fast is exact enumeration in practice?
A heads-up preflop matchup is C(48,5) = 1,712,304 runouts, about 3.4 million seven-card evaluations, and completes in roughly 250ms single-threaded in a browser. A flop spot is 990 runouts and takes about a millisecond. No Monte Carlo, so the numbers are exact and reproducible.
How does a free endpoint with no API key survive abuse?
Admission control instead of billing: a leaky bucket tracks estimated compute cost in compute-milliseconds and rejects over-budget requests with a 429 and a Retry-After header. Validation runs before the budget check so malformed input cannot drain it, and a 16KB body cap bounds every request.



