← back to blog

Why We Built MonoRouter Instead of Just Using the API

The backstory behind MonoRouter — how a side project to avoid API bills turned into a product.

It started with a Slack message: "Did anyone else's Anthropic bill just triple?"

We were a small team, five engineers at the time, all heavy Claude users. Everyone had Pro subscriptions for the web UI. But as we started integrating Claude into our workflows — code review bots, test generators, documentation pipelines — we needed API access too. And API access means per-token billing.

The Math That Didn't Add Up

Our Pro subscriptions gave us unlimited access to Claude through the browser. But the moment we wanted to call the same model from Python, we needed API credits. Same model, same weights, same everything — different billing.

One engineer was spending $400/month on API credits while their $20 Pro subscription sat mostly idle during coding hours. Multiply that by five people and the numbers got uncomfortable fast.

We ran the actual numbers across the team for one month:

EngineerAPI SpendSubscriptionTotal
Dev 1 (agent pipelines)$420$20$440
Dev 2 (Claude Code)$310$20$330
Dev 3 (Cursor + scripts)$180$20$200
Dev 4 (code review bot)$260$20$280
Dev 5 (test generation)$340$20$360
Total$1,510$100$1,610

$1,610/month for five engineers. The subscriptions were a rounding error. The API was the real cost — and it was growing every sprint as we found more uses for Claude.

The Hack That Worked

The first version of MonoRouter was a weekend project. A simple Express server that intercepted Anthropic SDK requests and forwarded them through an authenticated browser session:

// v0 — the ugly prototype that started it all
const express = require("express");
const app = express();

app.post("/v1/messages", async (req, res) => {
  // Forward the request through an authenticated session
  const session = getAvailableSession();
  const response = await session.sendMessage(req.body);

  // Pipe the response back in Anthropic API format
  res.json(formatAsAnthropicResponse(response));
});

app.listen(4000);
// Then: export ANTHROPIC_BASE_URL=http://localhost:4000

It was ugly. It broke constantly. Session cookies expired mid-request. Streaming didn't work. Error messages were cryptic garbage. But it worked — and our API bill dropped to zero overnight.

That was enough motivation to keep going.

From Hack to Product

Over the next few months we rebuilt everything from scratch. The core challenges were:

Session management. Browser sessions expire, get rate-limited, and occasionally just die. We needed a pool manager that could track session health, rotate between accounts, and automatically recover from failures without dropping requests.

Streaming. The Anthropic Messages API uses Server-Sent Events for streaming responses. Our proxy needed to maintain a persistent SSE connection between the client SDK and Claude, forwarding delta events in real time. Getting this right took more time than anything else:

// Streaming proxy — forward SSE events as they arrive
app.post("/v1/messages", async (req, res) => {
  if (req.body.stream) {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    });

    const session = pool.acquire();
    const stream = await session.createStream(req.body);

    stream.on("event", (event) => {
      res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
    });

    stream.on("end", () => {
      pool.release(session);
      res.end();
    });
  }
});

Tool use. Claude's tool use protocol requires multi-turn request/response loops. Each turn is a separate API call, and MonoRouter needed to handle each independently without maintaining conversation state on the proxy side.

API compatibility. The goal was zero code changes on the client side. Every field, every header, every edge case in the Messages API had to work identically through MonoRouter. We built a comprehensive test suite that runs the same requests through both the direct API and MonoRouter and diffs the responses.

The Architecture Today

MonoRouter is now a proper service with two components:

  1. The proxy — a stateless API server that accepts Anthropic-compatible requests, authenticates them with MonoRouter API keys, and routes them through the session pool.
  2. The dashboard — a Next.js app where you manage sessions, view usage analytics, and configure your account.

The proxy is designed to be transparent. Your SDK sends a request, MonoRouter routes it, and the response comes back in the exact same format as the direct API. No wrapper types, no custom fields, no surprises.

Why Not Just Use the API?

For some teams, the API makes sense. If you need fine-grained billing, SLAs, or you're processing millions of requests for a customer-facing product, go with the API.

But if you're a team of developers who already pay for Claude subscriptions and just want to use Claude from your code without a second bill, MonoRouter exists for you. Our API bill went from $1,510/month to $0. The subscriptions we were already paying for now cover everything.