Case study · 2026 · In-house product · API + Web
10

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 behind nine endpoints, and a React front-end on Cloudflare Pages that holds no logic at all. Saves are parsed in memory and discarded when the response ends. Nothing is written, nothing is logged. Live at towerpredict.com.

ClientTower Predict
Year2026
LocationRemote · EU infrastructure
Tower Predict landing page
Live · towerpredict.comVisit site ↗
~49msTypical response
9API endpoints
ZeroSaves stored
Livetowerpredict.com
[ 10 · Overview ]

The brief,
in three beats.

Challenge · Approach · Outcome
— Challenge

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, storing any part of it was never on the table.

— Approach

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 posts a file and renders the answer, with no algorithm in the bundle at all.

— Outcome

What happened.

Live at towerpredict.com. A typical request costs about 49ms, the one slow endpoint is isolated so it cannot starve the fast ones, and no save file is ever written to disk.

[ 10 · How we got there ]

Inside the
engagement.

6 chapters
01 / 06Architecture

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. 26 test modules cover the library, 7 more cover the service.

02 / 06API

An envelope that refuses to guess

Every response carries the same wrapper: whether the input is supported, how confident the answer is, which build 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 an answer can be trusted, because the answer says.

  • supported · confidence · build · catalog · warnings on every response
  • Version gate refuses unsupported inputs outright
  • One shape for every endpoint, so clients need no special cases
03 / 06Performance

The slow path cannot starve the fast one

Most endpoints cost about 49ms, almost all of it the save parse. One does an exhaustive search and blocks for seconds. Left alone it would occupy every worker and stall everything behind it, 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 responsive while the slow one runs.

  • 5s budget on fast endpoints, roughly 40x measured cost
  • 60s budget on the search, deliberately under Cloudflare's proxy limit
  • Concurrency capped per worker, queueing bounded
04 / 06Security

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.

05 / 06Privacy

Nothing stored, end to end

Saves are parsed in memory and discarded when the request ends. The server runs with access logging off, so the request itself leaves no trail either. The front-end holds no prediction logic and ships without source maps, and the site's privacy policy states exactly this behaviour, which makes it a promise the code has to keep rather than a page nobody reads.

06 / 06Infrastructure

Deployed without a public port

The container binds to loopback only and Cloudflare fronts it through a tunnel, so the origin is never directly reachable and its 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.

  • Origin bound to 127.0.0.1, reached only through the tunnel
  • Test then deploy, same pipeline, no manual step
  • Deploy key as a build secret, never baked into a layer
[ 10 · Tech ]

The tools
we shipped on.

Battle-tested choices · not flavour-of-the-month
Python 3.12FastAPIuvicornReact 18ViteCloudflare PagesCloudflare TunnelPages FunctionsDockerGitLab CI/CDpytest
[ 10 · Related work ]

Other
engagements.

From the studio

— Let's make something —

Tell us what
you're building.

Studio
Port of Spain
Trinidad & Tobago
Hours
Mon — Fri
09:00 — 17:00 AST
Response
Within 24 hours
on business days