Hold'Em Analytics

A poker analytics platform for session tracking, hand evaluation, and real-time odds calculation, with the Monte Carlo odds simulation running in a concurrent Go microservice.

Status
Live
Stack
  • FastAPI
  • Go
  • PostgreSQL
  • Next.js
  • AWS

What it is

Hold’Em Analytics is a tool for Texas Hold’em players. Drag your two cards and the board onto the table, choose 1 to 9 opponents, and it names your best hand and estimates your chance to win, tie, or lose by playing out the rest of the hand 10,000 times. The analyzer works without an account.

Sign in and you can log hands into sessions. A dashboard turns them into statistics about how you play: win rate by position and by action, win rate over time, and a read on how tight or loose and how passive or aggressive your play is.

Hold’Em Analytics’ analyzer in dark mode: the ace and king of spades as hole cards, the queen and jack of spades and the four of hearts on the board, and the results panel estimating a 75.9% chance to win against one opponent

How it’s built

Three deployed pieces over PostgreSQL, all on AWS. The Next.js frontend is hosted on AWS Amplify, which serves it over HTTPS and redeploys it on every merge. A FastAPI server on an EC2 instance handles accounts, sessions, logged hands, and statistics, with bcrypt-hashed passwords and JSON Web Tokens for sign-in. A Go service on the same instance does the card math, evaluating hands and running the odds simulation, and FastAPI calls it over localhost. The database is PostgreSQL on RDS, Amazon’s managed database service.

Hold'Em Analytics' architecture The browser loads the Next.js app from AWS Amplify over HTTPS and sends every API call to Amplify too, which forwards the allowed routes to the FastAPI server on EC2. FastAPI calls the Go odds service on the same instance and stores data in PostgreSQL on RDS. Browser Next.js app, calls /api AWS Amplify HTTPS, forwards /api calls EC2 instance FastAPI accounts, hands, stats Go odds service hand evaluation, simulation PostgreSQL on RDS
The browser only ever talks to Amplify. The API and the Go service share one instance.

Every statistic is computed when you ask for it, straight from your stored hands. Nothing is aggregated ahead of time, so a newly logged hand shows up in every statistic at once, with no cached numbers to invalidate.

The hard parts

Moving the odds math from Python to Go

The first version was all Python. One odds request deals out the unknown cards 10,000 times and evaluates your best hand and every opponent’s each time, and finding the best hand from seven cards means checking all 21 five-card combinations. At nine opponents that is 2.1 million five-card evaluations for one request.

Python threads cannot speed that up. The global interpreter lock lets only one thread run Python code at a time, and this work is pure computation, not waiting on a network. So I split the trials across a pool of four processes, which do run in parallel. That helped, but each request still paid to start the pool and copy data to it, and the inner loop was still interpreted Python.

So I built a separate Go service for the evaluation and the simulation, which splits the trials across goroutines, Go’s lightweight threads. Splitting 100,000 simulations across eight goroutines ran them about 3 times faster than one goroutine alone. Against the Python version on the same 8-core laptop, 10,000 simulations took 172 ms instead of 764 ms at one opponent, and 806 ms instead of 2,700 ms at nine.

FastAPI’s two card-math endpoints became thin proxies to the Go service, with separate timeouts for the fast evaluation and the slow simulation, and an HTTP 503 when the service is down. I kept the Python implementation in the repository as a working reference.

Calling an HTTP API from an HTTPS page

The frontend is served over HTTPS, and the API on EC2 serves plain HTTP. Browsers block an HTTPS page from calling an HTTP address, which is called mixed content, so the app worked locally and failed the moment it was deployed. The direct fixes were a certificate and a proxy on the server, or a load balancer in front of it.

I took the browser off that hop instead. The frontend calls a relative path, /api, on its own domain, and Next.js rewrites those requests to the API on the server side. From the browser’s side, every request is same-origin HTTPS.

The first version forwarded everything under /api, which made the public domain a pass-through for the whole API, including endpoints the app itself never calls. I later replaced it with an allowlist of the five route groups the frontend uses. It denies by default, so there is no list of bad paths to keep in sync, and adding an API call means adding its route on purpose.

Hand evaluation you can compare with one operator

Poker hands do not sort on one number. Two hands of the same kind are separated by kickers, and the rules differ by kind: a full house compares its three of a kind and then its pair, two pair compares both pairs and then one kicker, and a flush compares all five cards. The lowest straight, ace through five, has the ace playing low, which breaks the ordering every other straight follows.

The Python evaluator I built first returns each five-card hand as a (rank, kickers) tuple, built so that ordinary tuple comparison is the correct poker ordering. All the per-category rules live in building that tuple once, and every comparison is a single >. Seven cards are evaluated by trying all 21 five-card combinations and keeping the largest tuple. The low straight is one explicit case that sets its high card to the five, so it sorts below every other straight. Its 21 tests cover all ten hand categories at five cards and again at seven.

What I’d do differently

Put tests where the risk is. The 24 tests cover the Python evaluator and simulator, pure functions with no database, which were easy to test. When the card math moved to Go, the tests did not follow, so the code users actually reach has no automated tests.

Set up CI and automated deploys from the first commit. The frontend redeploys on every merge, but nothing runs the tests automatically, and the backend does not deploy on merge at all. When half of a system deploys easily and half does not, changes get pulled toward the half that is easy to ship.

Give the API its own certificate. The rewrite proxy solved mixed content without one, but it puts the frontend server on the path of every API call, including simulations that take seconds.

Aggregate in the database. The dashboard reads every stored hand once per statistic and folds them in Python. That is invisible at today’s scale. The sessions list already does its counting in one SQL query with GROUP BY, and the statistics should work the same way.