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)