← Blog

Security

AI key audits: the checklist before agents ship to prod

A staging key in a public GitHub Action burned €11,800 in two days of someone else's prompt traffic. Here is the audit we now run before any agent ships.

Jacob Molkenboer· Founder · A Brand New Company· 11 Jun 2026· 8 min
Brass key on cream linen ledger, wax-sealed envelope with green ribbon on ivory paper, side window light.

A founder messaged us on a Tuesday afternoon: the Anthropic dashboard had just emailed him about "unusual usage." The number on the invoice preview was €11,800. He had spent €340 the month before. The agent he was building was still in staging, hadn't gone live, hadn't seen a single real customer prompt.

We traced it in under an hour. A staging key had been written into a GitHub Actions workflow eight weeks earlier, during a hurried CI fix. The workflow file lived in a public repo. The key had been quietly used by someone else for two days before the rate-limit alert tripped. Anthropic refunded most of it after a polite support ticket, but the founder lost a week of sleep and we lost a planned sprint to incident response.

Since then, we run a fixed audit on every sub-€20M SaaS founder's key inventory before we let any agent we build touch their production traffic. It takes about ninety minutes. It has caught something on every single client. Here is the whole thing.

The blast-radius question, asked first

Before we look at a single key, we ask one question: if this key leaked right now, what is the worst case in euros over forty-eight hours? Not "is it possible to leak." Assume it has leaked. What stops the bleeding?

For most founders the honest answer is: the provider's fraud-detection email, eventually. That is not a control. That is a hope. A leaked key with no per-key spend cap and full org access can burn five figures before a human notices. We have now seen it happen twice in our client portfolio. The second time was a Friday evening leak that ran through the weekend.

If you cannot name the dashboard, the budget, and the alerting channel that would stop the bleeding within an hour, you do not have key hygiene. You have a key spreadsheet.

Step one: inventory every key that exists

You cannot audit what you cannot list. We start by getting read access to the Anthropic and OpenAI consoles, plus any of the smaller providers in play (Mistral, Together, Groq, Fireworks). For each, we pull the full API key list and dump it into a single sheet with five columns: provider, key prefix, label, created date, last used.

The Anthropic console exposes this directly under Settings → API Keys. OpenAI's equivalent lives at platform.openai.com/api-keys. Both show the last-used timestamp, which is the most useful column on the sheet.

Then we sort by last used and ask a brutal question of every key older than thirty days with zero traffic: who created this, what was it for, and can we delete it now? About a third of keys in a typical founder's account fail that question. They are leftovers from a Replit experiment, a Zapier test, a Vercel preview that was never cleaned up. Every one of them is a live credential.

Warning

A key that has zero usage for sixty days is not "safe because it's unused." It is a credential with no owner who would notice if it started being used. Delete it.

Step two: grep the world for prefixes

This is the part nobody likes. For every key in the inventory, we search for its prefix (the first ten or twelve characters, e.g. sk-ant-api03- plus the next eight) across:

  • The client's entire GitHub organisation, including private repos, gists, and Actions logs.
  • Every .env, .env.local, .env.staging file on every dev laptop with access.
  • The Vercel, Netlify, Railway, Fly, and Render dashboards.
  • Slack, Notion, Linear, and any internal wiki. Especially Slack DMs.
  • The browser history and password manager of the founder. Yes, really.

GitHub's secret scanning will catch the obvious cases for the major providers, but it is a backstop, not a control. Push protection only triggers on push, not on already-committed history, and it does not see Actions logs or private gists. The grep is manual and tedious and it works.

#!/usr/bin/env bash
# Find any Anthropic or OpenAI key prefix across the org.
# Requires: gh CLI, jq, the user's org token with repo:read.

ORG="$1"
PATTERNS=("sk-ant-api03" "sk-ant-api04" "sk-proj-" "sk-svcacct-")

for repo in $(gh repo list "$ORG" --limit 500 --json nameWithOwner -q '.[].nameWithOwner'); do
  for pat in "${PATTERNS[@]}"; do
    hits=$(gh api -X GET search/code \
      -f q="$pat repo:$repo" \
      --jq '.items[].path' 2>/dev/null)
    if [ -n "$hits" ]; then
      echo "== $repo : $pat =="
      echo "$hits"
    fi
  done
done

Run that against every org the founder owns. Then run trufflehog across the same set for the keys you do not know about yet, which there will be.

Step three: scope every key to one thing

Once the inventory is clean, every surviving key gets a single job. We do not allow a key labelled "prod" to also be used by a developer's local script. We do not allow the same key in staging and CI. The rule is: one key, one environment, one workload, one owner.

On Anthropic, this means workspace-scoped keys with a clear naming convention: prod-api, staging-ci, dev-jacob-local. On OpenAI, project-scoped keys do the equivalent. Both let you set a per-key monthly budget; both will email you when you hit a percentage of it. We set the limit to 1.5x the highest legitimate month we have observed. Not 10x. Not "unlimited just in case." 1.5x.

The €11,800 incident would have capped at about €450 if the staging key had been scoped this way. We know because we did the math afterwards while the founder ate cold pizza at 1am.

Step four: route everything through one server

The strongest control is the one most founders skip because it feels like over-engineering. Your application code should not hold provider API keys at all. It should call your own server, which calls the provider. The provider key lives on the server, ideally in a secrets manager, and the server enforces a per-user, per-feature spend cap that your auth system already understands.

This sounds like a lot of work. It is two files and an afternoon for most stacks. Here is the minimal shape we ship for clients on Supabase, which covers about half our portfolio:

// supabase/functions/anthropic-proxy/index.ts
// verifySupabaseJwt, getTodaysSpendEur, recordSpend are the project's own
// auth helper + spend ledger. Wire them to your existing auth and a usage
// table; ten lines each in our reference build.
import Anthropic from "npm:@anthropic-ai/sdk";

const anthropic = new Anthropic({ apiKey: Deno.env.get("ANTHROPIC_KEY")! });
const DAILY_CAP_EUR = 5;

Deno.serve(async (req) => {
  const auth = req.headers.get("authorization");
  const user = await verifySupabaseJwt(auth);
  if (!user) return new Response("unauthorized", { status: 401 });

  const spent = await getTodaysSpendEur(user.id);
  if (spent >= DAILY_CAP_EUR) {
    return new Response("daily cap reached", { status: 429 });
  }

  const { messages, model } = await req.json();
  const reply = await anthropic.messages.create({
    model: model ?? "claude-sonnet-4-5",
    max_tokens: 1024,
    messages,
  });

  await recordSpend(user.id, reply.usage);
  return Response.json(reply);
});

That is the entire defence. A leaked client token now caps at €5 per user per day, not €11,800 per weekend. The provider key never leaves the edge function. The audit log writes itself.

Step five: rotate on a calendar, not on a panic

Quarterly rotation, on a fixed day in the calendar, with a checklist. The point is not that ninety days is some magic number; the point is that you have practised the rotation in calm weather, so when you actually need to rotate at 2am because a contractor's laptop got stolen, you have a runbook and not a stress response.

The checklist is small: generate new key in console, write to secrets manager, redeploy, verify traffic on new key, revoke old key, update the inventory sheet. Six steps. We have a template our team works from; we will publish it as a gist if anyone asks.

Step six: alert on the second-derivative

Provider dashboards alert on absolute spend. That is too slow. By the time your €500 alert fires, an attacker has been running for hours. Alert instead on the rate of change: any hour where spend is more than 5x the trailing 24-hour median, page someone. This catches the €11,800 case in roughly forty minutes instead of forty hours.

If you do not have a metrics pipeline, the cheap version is a cron job that hits the provider's usage endpoint every fifteen minutes and posts to a Slack webhook when the delta crosses a threshold. The expensive version is the same thing in Grafana. The principle is identical.

Takeaway

Provider fraud detection is not your incident response. If your only alarm is the email from billing, you are paying for someone else's traffic until they notice.

What about the new retention rules

Anthropic's recent change to its consumer privacy policy on data retention is a separate concern from key hygiene, but it lands on the same desk. If you are bound by a customer DPA that promises zero retention, audit which model IDs you actually call from production today, and confirm each one against the provider's current policy. Add the model ID to your inventory sheet next to the key. We have seen founders accidentally upgrade their default model in a dependency bump and discover the retention change in a customer security review three months later.

The smallest thing you can do today

When we built the email-responder agent for a Dutch logistics client last quarter, the thing we ran into was exactly this: three forgotten staging keys, two of them in a public Actions log. We solved it by running the audit above and routing every call through a single Supabase edge function with a per-tenant daily cap. If you want the same on your stack, that is the kind of work we do under AI agents.

Today, before you close this tab: open your Anthropic console, sort your keys by last-used date, and delete every key with no traffic in the last sixty days. It is a five-minute job and it is the single highest-leverage thing on this list.

Key takeaway

If a leaked key's only stop-gap is the provider's fraud email, you do not have key hygiene — you have a hope, and it has a five-figure failure mode.

FAQ

How did the €11,800 leak actually happen?

A staging API key was hardcoded into a GitHub Actions workflow in a public repository during a CI fix. Someone scraping public Actions configs found it and used it for two days before the provider's rate-limit detection emailed the owner.

Do I really need a proxy server in front of the provider?

If your app is a backend service that already holds the key server-side, no, scoping and budgets are enough. If any client-side code, mobile app, or browser extension touches a provider, yes — otherwise the key is one devtools tab away from being public.

Can GitHub secret scanning replace this audit?

No. It catches obvious commits to the default branch on supported providers, but it does not cover Actions logs, private gists, Slack messages, or developer laptops. Treat it as a backstop, not a control.

What is the right spend cap to set on a production key?

1.5x the highest legitimate month you have observed in the last six months. Not 10x, not unlimited. If you genuinely grow past it, you will get a polite 429 and raise the cap deliberately, instead of finding out from an invoice.

How often should I rotate keys?

Quarterly on a fixed calendar date is the floor. The frequency matters less than the fact that your team has rehearsed the rotation when nothing is on fire, so the 2am emergency rotation has a runbook.

securityai agentsoperationstoolingarchitecture

Building something?

Start a project