A prediction engine, server-side and fast.
Tower Predict turns a mobile game save file into exact, deterministic predictions, computed rather than estimated. We built the whole stack: a pure Python prediction library, a FastAPI service over it across 21 routes, and a React front-end on Cloudflare Pages that carries no prediction logic at all. Saves are parsed in memory and discarded when the response ends, never written to disk. Live at towerpredict.com.

The brief,
in three beats.
What they needed.
A prediction is only worth anything if it is exact and it arrives fast. That means parsing a binary save file, replaying a deterministic sequence forward, and answering in the time it takes to tab back to the app. It also needs a strict honesty rule: an input the engine does not support has to produce an error, never a confident wrong answer. And because the input is somebody's personal save file, writing any part of it to disk was never on the table.
How we tackled it.
Three pieces with clean seams. A pure Python library holding the prediction logic, depended on at an exact tag rather than a version floor, because its data tables are derived from one specific build. A FastAPI service over that library, running four uvicorn workers in a container that can do almost nothing. A React front-end on Cloudflare Pages that carries no prediction logic: it collects a save, posts it, and renders the answer.
What happened.
Live at towerpredict.com. Fast endpoints cost about 49ms, the one slow endpoint is bounded so a single search cannot monopolise a worker, and no save file is ever written to disk.
Inside the
engagement.
A library, not a pile of endpoints
The prediction logic lives in its own package as pure functions: a save plus parameters in, an answer out, with nothing touching a network or a device. The API depends on it at an exact tag, not a floor, because the catalogs inside are derived from one specific build and "latest" is never the right answer. Both the library and the service carry their own test suites.
An envelope that refuses to guess
Every prediction response carries the same wrapper: whether the input is supported, how confident the answer is, which game version and catalog produced it, and any warnings. A save from an unsupported build gets a refusal rather than a plausible wrong list. The client never has to work out whether a prediction can be trusted, because the answer says.
- supported · confidence · game version · catalog · warnings
- Version gate refuses unsupported inputs outright
- One shape across the prediction endpoints, so clients need no special cases
One slow search cannot monopolise a worker
Fast endpoints cost about 49ms, almost all of it the save parse. The substat planner is different: it runs an exhaustive search and blocks for seconds. Left alone it would occupy a worker indefinitely, so it gets its own semaphore: two searches per worker, a third waits, and it waits against a deadline rather than forever. Four workers on two CPUs, oversubscribed on purpose to keep the fast paths moving while a search runs.
- 5s budget on fast endpoints, roughly 100x measured cost
- 60s budget on the search, deliberately under Cloudflare's proxy limit
- Concurrency capped per worker, queueing bounded
A container that owns nothing
The service parses untrusted input, so it runs as a non-root user with a read-only root filesystem, every Linux capability dropped, no-new-privileges set, and a small tmpfs as the only writable space. CPU, memory and process count are all capped from measurement rather than guesswork. The build is multi-stage: git and SSH exist only in the build stage, so the runtime image carries no toolchain and no key.
The save never touches disk
A save is parsed in memory and discarded when the response is sent. It is never written to disk and never kept. What the server does keep is a one-line-per-request access log of method, path and status, deliberately, because it is the operator's only usage record: no bodies, no headers, no save contents, and because every request arrives through the tunnel the client address it prints is the tunnel's rather than a visitor's. The published privacy policy documents that log, and the one place anything is retained, rather than claiming nothing happens at all.
- Prediction path writes nothing and retains nothing
- Access log is method, path and status only
- Front-end ships no prediction logic and no source maps
Pull the save off the phone in the browser
Finding playerInfo.dat by hand means digging through Android's data directory. So the front-end also speaks ADB over WebUSB: on a desktop Chrome or Edge, the phone is plugged in, the player approves the prompt once, and the file goes straight into the same upload path a manual pick uses. It runs entirely in the browser, and nothing else leaves the device.
Deployed without a public port
The service listens inside its container, but the port is published only to the host's loopback, so nothing outside the box can reach it directly. Cloudflare fronts it through a tunnel and the origin's address stays out of DNS. GitLab CI runs the tests, then deploys on the box itself through the host's Docker daemon, with the dependency's deploy key mounted as a build secret so it never lands in an image layer.
- Port published to 127.0.0.1 only, reached through the tunnel
- Test then deploy, same pipeline, no manual step
- Deploy key as a build secret, never baked into a layer
The tools
we shipped on.
Capabilities
behind this build.
Other
engagements.
— Let's make something —
Tell us what
you're building.
Trinidad & Tobago
09:00 — 17:00 AST
on business days

