godozo × OpenRouter

Put a spend gate on your OpenRouter agent

Jun 29, 2026 · Building godozo

Your agent runs on OpenRouter. Every call spends real money. Most of the time that's fine — pennies. Then one loop decides to fan a 200k-token context across a frontier model 500 times, and you find out at the invoice.

godozo · now
🚦 Approve OpenRouter spend?
run: nightly-eval
model: openai/gpt-4o
est: ~$6.20 · 512 prompts · credits left $18.40
✅ Approve
🚫 Deny

The batch is frozen until you tap a button.

OpenRouter is where the money leaves. godozo is the bit that makes your agent fess up to what it spent and ask before it spends a lot. No new godozo feature required — its two verbs, notify and request_approval, are the whole toolkit. We'll just wrap them around a plain OpenRouter call. All runnable.

Three moves, cheapest first: a receipt on every run, a gate before an expensive one, and a budget backstop.


Move 1 — A cost receipt on every run

OpenRouter returns the exact cost of each call in the response. As of 2025 it's always included — usage.cost is the credits that call burned (OpenRouter credits are bought 1:1 with dollars, so read it as USD). Pipe it straight into a notify:

import { createGodozo } from 'godozo';   // npm link the repo, or import from ./src/core.js
const gd = createGodozo();

const KEY = process.env.OPENROUTER_API_KEY;
const MODEL = process.env.MODEL || 'openai/gpt-4o';  // any slug from openrouter.ai/models

async function chat(messages) {
  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: MODEL, messages }),
  });
  const json = await res.json();
  const { cost = 0, total_tokens } = json.usage ?? {};
  await gd.notify({ title: 'openrouter', message: `${MODEL} · $${cost.toFixed(4)} · ${total_tokens} tok` });
  return json.choices?.[0]?.message?.content;
}

Now every call leaves a line on your phone and in godozo's local audit log — a running tab of what the agent's spending, per model. That one thing changes how it feels to walk away from a running agent: you're watching the meter, not squinting at the invoice later.


Move 2 — Gate before an expensive call

A receipt is after the fact, though. For the calls that could actually sting — a big batch, a frontier model, a giant context — you want the run to stop and ask first. request_approval blocks until you answer, and its return value is the decision:

// Ask before spending. `estimate` is YOUR pre-flight number (see below).
async function chatGated(messages, { estimate = 0, approveOver = 1.00 } = {}) {
  if (estimate >= approveOver) {
    const r = await gd.requestApproval({
      title: `Spend ~$${estimate.toFixed(2)} on ${MODEL}?`,
      detail: messages.at(-1)?.content?.slice(0, 600),
    });
    if (!r.approved) throw new Error(`spend denied (${r.decision ?? 'timed out'})`);
  }
  return chat(messages);   // approved → the call (and its cost receipt) proceeds
}

Under the threshold, it just runs. Over it, your phone buzzes with the model and the estimate, and the batch sits frozen until you tap Approve. Deny, and the function throws instead of spending — your loop handles it like any other error.

Where the estimate comes from

OpenRouter doesn't pre-charge, so you supply the pre-flight number — the estimate the gate compares against. Two ways, depending on how exact you want to be.

Heuristic (good enough): gate on the shape of the job — "it's a batch of 500", "it's the frontier model", "the context is over 100k tokens." You don't compute dollars at all; you pass a number big enough to trip the gate for the runaway cases and 0 otherwise.

Priced (exact): every model's per-token price lives in GET /api/v1/models as pricing.prompt and pricing.completion — USD per token, as strings. Multiply by your token counts: input tokens approximated from character length, output bounded by your max_tokens.

// Rough pre-flight dollars = model price × token counts.
// Input ≈ chars/4; output = the max_tokens ceiling you'll request.
async function estimateCost(messages, { maxTokens = 1024 } = {}) {
  const res = await fetch('https://openrouter.ai/api/v1/models', {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  const { data } = await res.json();
  const price = data.find((m) => m.id === MODEL)?.pricing;
  if (!price) return 0;                          // unknown model → don't gate on price
  const inChars = messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
  const inTok = Math.ceil(inChars / 4);          // ~4 chars/token — plenty for a gate
  return inTok * Number(price.prompt) + maxTokens * Number(price.completion);
}

Then the gate reads a real dollar figure instead of a magic number:

const estimate = await estimateCost(messages, { maxTokens: 1024 });
const answer   = await chatGated(messages, { estimate, approveOver: 1.00 });

It won't be exact to the token — chars/4 is an approximation, and the model often stops short of max_tokens — but a gate only needs to answer "cents or dollars?", and that it nails. The precise figure always lands a moment later in the cost receipt from Move 1.

No code? Gate a batch from the shell

If the spend is a script rather than inline calls, the CLI does the same job — gate's exit code composes with &&:

godozo gate --title "Run nightly-eval on openai/gpt-4o?" \
  --detail "512 prompts · est ~\$6.20" --timeout 300 \
  && node run-eval.js

Move 3 — A budget backstop

The gate covers calls you expect to be big. The backstop covers the ones you don't — a loop that quietly grinds through your credits. OpenRouter exposes the balance on the key itself:

async function creditsRemaining() {
  const res = await fetch('https://openrouter.ai/api/v1/key', {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  const { data } = await res.json();
  return data.limit_remaining;   // dollars left on the key (null = no limit set)
}

// Check it between iterations; ping once when you cross a line, gate when it's dire.
const left = await creditsRemaining();
if (left != null && left < 5) {
  const r = await gd.requestApproval({
    title: `OpenRouter credits low: $${left.toFixed(2)} left`,
    detail: 'Approve to keep the agent running, or Deny to park it.',
  });
  if (!r.approved) process.exit(1);
}

The same endpoint carries usage_daily and usage_weekly, so you can just as easily notify "we've spent $40 today" and gate anything past a daily cap.


Make it permanent

If the OpenRouter agent is itself driven by a coding agent (Claude Code, Cursor), give it a standing rule so the gate isn't something it can forget. Paste into CLAUDE.md / AGENTS.md:

## Spend gate (godozo × OpenRouter)

- Before any OpenRouter run you estimate will cost over $1 — a big batch, a
  frontier model, a >100k-token context — call godozo request_approval (or
  `godozo gate`) and WAIT for the answer. Deny = do not spend.
- After a run, godozo notify the model + credits it burned, so there's a receipt.
- If credits remaining drops below $5, stop and ask before continuing.

The prompt is the policy; for the one spend path that must never slip — say, an autopay/top-up step — pin it with a deterministic hook, exactly like the destructive-git seatbelt in the last post.


Why this is the interesting part

A force-push you can undo. A spend you can't. As agents start running loose on metered APIs, "notify me" quietly becomes "get my sign-off" — and one approval in front of one call is the seed of something bigger: approval chains for agents that spend money. Per-run gates today; budgets, multi-party sign-off, and real spend policy down the road.

That's the arc. For today, the win is small and very real: your agent can't surprise you with a bill. Wrap the call, and let it ask.

← More from the godozo blog