← Back to all work

2026 · Live at burnguard.run

BurnGuard

AI API Cost-Protection Proxy

GoSQLitePostgreSQLNext.jsWebAuthnGoReleaserDocker

The Problem

In May 2026, a developer ran up $30,141 in AWS Bedrock charges in a single month. They had monitoring. They had a $100 anomaly threshold. None of it mattered — Bedrock bills through AWS Marketplace, and the monitoring tool doesn't watch Marketplace. That distinction is buried in docs nobody reads until the invoice arrives.

The same month, a Google Cloud customer woke up to an $18,000 bill despite a $7 budget. An attacker found a public API key and sent 60,000 requests overnight. Google had nine safety features that could have stopped it. All nine were off by default.

These aren't edge cases — cost surprises top developer-frustration surveys in 2026. The tools that solve it start at $6,000/year and target enterprises with half-million-dollar cloud budgets. A solo dev spending $200/month on Claude is nobody's target customer. The only alternative is manually checking dashboards that refresh with 8–24 hour delays. A runaway loop at 2pm might not alert until morning — and by then the damage is done.

Sources: $30K Bedrock invoice, $18K on a $7 budget, surprise AI bills.

The Approach

BurnGuard is a reverse proxy. The developer changes one line — the base URL — and every request flows through BurnGuard on its way to the provider. BurnGuard reads the response, counts tokens, calculates cost, stores it, and passes the response through unchanged. If accumulated spend crosses the limit, it returns a 429 and the request never reaches the provider. No tokens consumed. No cost.

This is not a SaaS wrapper. The proxy runs locally — API keys never leave the developer's machine. The only thing that reaches the cloud is anonymized usage data (token counts and costs), synced every 60 seconds to power the dashboard.

Architecture

Three independent programs connected by HTTP:

The proxy (local)

A single Go binary, no dependencies — the SQLite driver is pure Go, so budget enforcement works offline and survives restarts. Download one file, run burnguard init, and you're protected.

The backend API (cloud)

Go + PostgreSQL in a Docker container on Render. Receives usage data, stores it, serves the dashboard. Auth via GitHub, Google, and WebAuthn — all producing the same session token.

The dashboard (Vercel)

A Next.js frontend at burnguard.run — GSAP on the landing page, Recharts for spend, TanStack Query for data. If the backend goes down, the proxy keeps enforcing budgets and queues records until sync resumes.

How the proxy works

The proxy has three critical paths:

Non-streaming requests

The response is a single JSON body. Read it with io.ReadAll, parse the usage field, calculate cost, then put the body back for the client via io.NopCloser(bytes.NewReader(...)). You can only read a Go body once — so you read, save, inspect, then replace.

Streaming requests (SSE)

The hardest path. When Content-Type is text/event-stream, resp.Body is swapped for a custom StreamReader. Every Read sends a chunk to the client AND copies it to an internal buffer; on Close the buffer is parsed for usage. FlushInterval: -1 means the client sees zero added latency.

Budget enforcement

Middleware checks the in-memory spend total before forwarding. If totalSpend ≥ budgetLimit, the request gets a 429 and never reaches the provider. SQLite is the source of truth for persistence; memory is the source of truth for speed, guarded by a sync.Mutex.

Multi-provider routing

The proxy routes on the URL path prefix, storing the provider name in the request context so ModifyResponse can route to the right parser:

localhost:8080/anthropic/v1/messages
  → api.anthropic.com/v1/messages

localhost:8080/openai/v1/chat/completions
  → api.openai.com/v1/chat/completions

Formats differ — Anthropic uses input_tokens/output_tokens and splits usage across message_start and message_delta; OpenAI uses prompt_tokens/completion_tokens and puts everything in the final chunk before [DONE]. The SSE parser is a function type injected into the StreamReader, keeping stream handling provider-agnostic.

Cost calculation

Pricing is more complex than "input × rate + output × rate" because of caching. For Anthropic prompt caching, cache writes cost 1.25× and cache reads 0.1× (a 90% discount):

input cost = (standard tokens      * rate)
           + (cache creation tokens * rate * 1.25)
           + (cache read tokens     * rate * 0.10)

OpenAI caching is automatic over 1,024 tokens and appears in prompt_tokens_details.cached_tokens (50–90% off, cache writes free). Output tokens always bill at the standard rate for both.

The sync layer

The bridge between local SQLite and cloud PostgreSQL is a background goroutine on a time.Ticker. Every 60 seconds it queries unsynced records, batches them into JSON, POSTs them with a Bearer token, and marks them synced. Network down? Records queue and flush next cycle. The sync token is hashed with SHA-256 — only the hash is stored, and the raw token is shown once. Same pattern as password hashing.

Authentication

Three methods, one session ID. GitHub and Google OAuth follow the standard authorization-code flow. WebAuthn passkeys use discoverable credentials — the backend issues a challenge, the browser verifies with Touch ID / Face ID / Windows Hello, and the user is looked up by credential ID. No email, no password.

The trickiest part was cross-domain cookies: the backend on one origin can't set a cookie the frontend on another can read. The fix — redirect to the frontend with the session ID in the URL, let the frontend set its own cookie, and send the session as a Bearer token on API calls.

The CLI experience

One command to install:

brew tap verifieddanny/tap && brew install burnguard
# or
curl -sSL https://burnguard.run/install.sh | sh
# or (Windows)
irm https://burnguard.run/install.ps1 | iex
# or
go install github.com/Verifieddanny/BunGuard/cmd/proxy@latest

Then burnguard init launches an interactive wizard (Charm's huh) covering provider, budget, sync token, webhooks, alert thresholds, and port — writing a burnguard.yaml that burnguard start reads. GoReleaser builds binaries for macOS (Intel + Apple Silicon), Linux (amd64 + arm64), and Windows, ships them to GitHub Releases, and updates the Homebrew formula automatically.

"I started this project not knowing what a reverse proxy was. Starting with the hardest, most novel piece meant everything after it — REST APIs, OAuth, React dashboards — was familiar territory."

What I learned

I built a reverse proxy from scratch — manually forwarding HTTP in Go — before touching httputil.ReverseProxy. I'd never used SSE; I built a streaming parser that counts tokens in real time without buffering. I'd never used SQLite in Go; I built an offline-first persistence layer with background sync. The SSE work was the hardest: understanding that resp.Body is a stream you can read only once, that io.TeeReader lets you observe it without consuming it, that Close is where you do the final parse — none of it was obvious. Each piece clicked only after I'd built the wrong version first.

What It Can't Do Yet

  • Meters Anthropic (Claude) and OpenAI (GPT) directly — the AWS Bedrock/Marketplace and Google Cloud billing paths from the horror stories aren't proxied yet
  • Budget is a single hard 429 cutoff, not per-model or per-project sub-budgets
  • Enforcement is per local proxy — no team-wide shared budget across machines yet

Built with

Proxy

Go · net/http/httputil · SQLite (modernc.org/sqlite, pure Go) · Charm huh (CLI wizard)

Backend

Go · Chi router · PostgreSQL · go-webauthn · Zap

Frontend

Next.js 15 · TypeScript · Tailwind · GSAP · Recharts · TanStack Query

Infra

Docker · GoReleaser · Homebrew · Render (backend) · Vercel (frontend)

Links