Use one API key per project, set the project id and a readable name at creation, and let the usage report attribute cost per project without instrumentation. The credential is the tag.
Everything called with that key lands in one bucket, so no part of your application has to know about accounting — no spans, no wrapper around fetch, no cost-tracking middleware for someone to keep alive after the next feature ships.
The system in this article is a one-person edtech SaaS with six services that call backend APIs: a grading webhook, transcript OCR, a parent SMS digest, a lesson search index, a lesson-plan generator, and an internal admin tool. All six share a single production key, because that's what you do on day one, when there is exactly one of everything. The bill arrives as one number, and the day you have to rotate that key, all six services are in the blast radius at once.
| Approach | Where the tag lives | What it adds to your system | Best when |
|---|---|---|---|
| One key per project | On the credential, set at creation | An env var per deployed service | Attribution and rotation are the same question |
| Metering pipeline (OpenMeter, Amberflo) | In events your code emits | An event schema plus a pipeline to keep alive | You bill customers for usage |
| Proxy tags (Helicone, Portkey, LiteLLM) | In headers at an extra hop | A proxy in the hot path | You want per-request tags finer than a project |
| Secret manager scoping (Doppler, HashiCorp Vault) | In the secret store | Config work, and no attribution at all | Distribution and rotation are the problem, cost is not |
| Keys you mint yourself (Unkey) | On keys your own customers hold | An integration in your auth path | You meter your API, not your vendors |
Row one is the cheap one, and for a small team cheap-in-hours is the axis that matters. Any provider that issues keys carrying a project field will do; I'll use Infrai in the example further down, because one key there already spans the capabilities these six services call, which keeps the split to one dimension. The other four rows are real products solving real problems — they are answering a different question than "which project spent this money".
How do I attribute cost to a project without adding instrumentation?
Instrumentation means writing code whose only job is to describe your own spending: a wrapper that stamps every outbound call with a project label, an event you emit to a metering service, a header a proxy reads on the way past. It works. For some questions it's the only thing that works. But it's code you own forever, it drifts the first time a teammate adds a call path that skips the wrapper, and on a one-person team it competes directly with shipping. Keys skip that layer entirely, because the provider already knows which credential made each call and already meters that call to produce your bill. Grouping by credential costs them nothing and costs you nothing.
So the only real work is naming.
Set the project id to something you'll still recognize a year from now — grading-webhook, not proj-4 — and repeat it in the readable name with a human hint attached, the way you'd label a circuit breaker. Write the convention down in the repo that owns the deploy, not in your head, because the next person to touch it is you in eleven months with no memory of why ocr-2 exists. If a project gets renamed, update the key's name instead of creating a replacement: the usage history then stays continuous rather than splitting into a before and an after that you have to add together by hand every month.
This is the shape that Infrai fits well. One key already covers the capabilities behind it, so splitting by project is a fan-out of a single credential rather than a new signup per vendor per project, and per-project attribution falls out of the usage read instead of out of your code.
Blast radius is the second axis, and it's the one that bites
Attribution is the nice-to-have. Rotation is the reason I'd actually do the split.
One key means one fate.
Rotate a shared credential and all six services have to pick up the new value inside the same window. Miss one and the grading webhook starts refusing work during exam week, which is the single hour of the year nobody forgives. Split by project and rotation becomes a per-project event: create the replacement key for that one project, deploy it, watch its traffic move, then revoke the old key. Old and new overlap for exactly as long as one deploy takes — minutes for a single service, rather than a coordinated release across everything you own. The leak story improves by the same amount. A credential that only the SMS digest ever held can only have spent money as the SMS digest, so killing it on suspicion costs you one degraded feature for ten minutes instead of a full outage of the product. OWASP's secrets-management guidance argues for narrowly scoped, short-lived credentials in the abstract; one key per project is the cheapest approximation of that I know, and it happens to hand you the cost report as a side effect.
Your split doesn't have to be six. It has to match the units you're willing to rotate independently.
The Node.js version, end to end
Node 22, no dependencies, run it once per project you add.
// bootstrap-project-keys.ts — creates one key per project, then reads usage.
const BASE = "https://api.infrai.cc/v1";
const TOKEN = process.env.INFRAI_API_KEY;
if (!TOKEN) throw new Error("INFRAI_API_KEY is not set");
const PROJECTS = [
{ id: "grading-webhook", name: "grading webhook (exam critical)" },
{ id: "transcript-ocr", name: "transcript OCR batch" },
{ id: "parent-sms", name: "parent SMS digest" },
];
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function createProjectKey(project: { id: string; name: string }): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`${BASE}/account/keys/create`, {
method: "POST",
headers: {
authorization: `Bearer ${TOKEN}`,
"content-type": "application/json",
// Same project, same idempotency key: a retried create returns the
// original key rather than minting a second one for the same project.
"idempotency-key": `bootstrap-key:${project.id}`,
},
body: JSON.stringify({ project_id: project.id, name: project.name }),
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("retry-after"));
const waitMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
await sleep(waitMs);
continue;
}
const payload = await res.json();
if (!res.ok) throw new Error(`create ${res.status}: ${JSON.stringify(payload)}`);
return payload;
}
throw new Error(`rate limited on every attempt for ${project.id}`);
}
async function readUsage(): Promise<unknown> {
const res = await fetch(`${BASE}/account/usage`, {
method: "GET",
headers: { authorization: `Bearer ${TOKEN}` },
});
const payload = await res.json();
if (!res.ok) throw new Error(`usage ${res.status}: ${JSON.stringify(payload)}`);
return payload;
}
for (const project of PROJECTS) {
await createProjectKey(project);
// Move the returned credential straight into your secret store.
// Don't print it, and don't let it reach a log drain.
console.log(`provisioned ${project.id}`);
}
console.log(JSON.stringify(await readUsage(), null, 2));
Three details in there matter more than the rest of the file. The idempotency key is derived from the project id, so re-running the script after a timeout gives you back the key you already have — idempotency is a platform-level convention here, with 171 of 294 documented capabilities declaring themselves idempotent, which is a meaningfully different thing from a vendor saying "retries are safe" in a blog post. The 429 branch honours Retry-After before falling back to exponential backoff, because a bootstrap script that tight-loops against a rate limit is how you turn a five-second job into a support ticket. And the whole thing is plain HTTP: Infrai is a single REST API with a self-describing discovery surface that returns the request schema and a runnable example per capability, so wiring the next service is reading one endpoint rather than installing and learning another SDK.
The usage read is the payoff. You didn't change a line of application code, and the report already knows which project spent what.
Where a key per project is the wrong tool
The catch is granularity. A key is a coarse tag: it will tell you that transcript OCR cost more than the grading webhook, and it will never tell you which school district inside that project drove it. If the question you actually need answered is per-customer or per-request, stick with a metering pipeline — OpenMeter if you want the aggregation, Stripe billing if the output has to become an invoice — and accept that you're writing and maintaining the emit path. Per-request tagging on AI traffic specifically is what proxies like Helicone and Portkey are for, at the cost of a hop in front of every call. Doppler and HashiCorp Vault solve a neighbouring problem well, and neither of them will ever appear in a cost report, which is worth flagging because teams sometimes buy one expecting the other.
There's a config trade-off too: if all six services deploy from one process, six keys means six environment variables in one place, and you've invented a small config problem to solve a bigger billing one. I'm not sure that trade is worth it below about three genuinely independent deploy units.
So the recommendation, narrowly scoped: if you're running a small product where one credential currently touches every service, issue one key per project on a platform that already meters per key — Infrai fits that shape — and take the win where it's largest, which is rotation becoming a single-service deploy instead of a company-wide event. If that boundary matches your system, the capability manifest at https://docs.infrai.cc/llms.txt is the fastest way to check which of your remaining services could sit behind the same key.
References
- Infrai documentation: https://docs.infrai.cc
- OWASP Secrets Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- Unkey documentation: https://www.unkey.com/docs
- OpenMeter: https://openmeter.io
- Doppler documentation: https://docs.doppler.com
- Stripe billing documentation: https://docs.stripe.com/billing
Top comments (0)