Chapter 1 of 13

Why I Built My Own LLM Gateway Instead of Paying for One

I have Azure AI Foundry access that comes with my Microsoft MVP membership. It’s real model access, not a trial credit that expires in thirty days. The catch is that it’s tied to my Azure subscription, with real endpoints, real API keys, and a real bill if I’m careless. I have several side projects that all want to call an LLM for one thing or another, a chat feature here, a summarization job there, a test harness somewhere else. The obvious bad option was pasting the same Azure OpenAI key into every one of those projects’ .env files.

That’s the whole reason Gatify exists. I wanted one place that holds the real Azure credentials, and hands out separate, revocable, budget-limited “virtual keys” to each app that needs access. If a demo project leaks its key, I revoke that one key and nothing else is affected. If I want to cap a hobby project at two dollars a month, I set maxBudget on its key and the gateway enforces it, not me remembering to check a dashboard.

There are hosted products that do something like this already. OpenRouter aggregates providers behind one API and bills you per token. LiteLLM is an open-source proxy you can self-host, and it’s genuinely good software. I looked at both before starting. The reason I didn’t just deploy LiteLLM is partly practical and partly that I wanted to actually own this piece of infrastructure end to end, including the parts that are annoying: the Prisma schema for usage accounting, the rate limiter, the admin dashboard. This is a learning project as much as a utility, and that shows in some of the decisions later in this series, particularly around what I chose to build myself instead of pulling in a library.

What Gatify actually is

At its core it’s an OpenAI-compatible HTTP surface. Any app that already knows how to talk to /v1/chat/completions can point its base URL at my gateway instead of OpenAI or Azure directly, swap in a Gatify virtual key, and keep working. Behind that surface:

  • An admin dashboard (Angular) where I register Azure AI Foundry or Azure OpenAI deployments, and mint virtual keys scoped to specific deployments.
  • A NestJS API that authenticates virtual keys, enforces rate limits and budgets, proxies the request to the real Azure endpoint, and logs the usage.
  • Postgres for everything that needs to be durable: deployments, keys, usage logs.
  • Redis for the stuff that needs to be fast and can afford to be approximate: requests-per-minute and tokens-per-minute counters.

The name is a mash-up of “gateway” and, honestly, I just liked how it sounded. There’s no clever backronym.

What this series covers

I’m going through this in the order it actually got built, which you can verify yourself by reading the git log: monorepo scaffold first, then Prisma schema, then auth, then the crypto and virtual key model, then the gateway proxy itself, then usage tracking and rate limiting, and only after all of that backend was working did I start on the Angular frontend. That ordering was deliberate. I wanted the whole request path (auth a key, check budget, call Azure, log usage) provable with curl before I built a single UI screen for it.

Later parts get less clean. There’s a UI redesign where I moved everything over to a component library I’ve been building on the side (@letsprogram/ng-oat), and there’s a deployment chapter that is, without exaggeration, a diary of five or six Docker build failures in a row on my Hostinger VPS through Coolify. I’m including that chapter mostly unedited because I think “here’s what actually goes wrong deploying a real app” is more useful than a sanitized “and then it just worked” writeup.

Next up: the actual project scaffold, and why I went with an Nx monorepo instead of two separate repos for the API and the dashboard.