TL;DR: live API usage is evidence, not an invoice. For a multi-tenant edtech product, keep the live meter for operational decisions, freeze a period snapshot for billing, and reconcile the two without rewriting history. During a leaked-key drill, the practical choice is a spend ceiling that limits exposure while leaving enough room for legitimate classes to continue.
| Choice | Best use | Main risk |
|---|---|---|
| Live usage only | Alerts and rapid key-response decisions | Late-arriving data can change the number |
| Frozen period snapshot | Reproducible invoices and reissues | It needs an explicit reconciliation trail |
| Recomputed invoice on every read | Internal previews | The same period can produce a different answer later |
Recommendation: use both the first and second rows. Read the live meter during the drill, then bill from an immutable snapshot identified by period and tenant. Reconciliation explains the difference. It should not pretend the difference never existed.
Why isn't API usage data already an invoice?
A meter answers, “What has the system observed so far?” An invoice answers, “What amount did we commit to charge for this closed period?” Those questions happen at different times and carry different obligations.
Usage can still settle. An invoice must survive a retry, a support ticket, and a reissue a year later. If an invoice renderer reaches back into a mutable usage read, there is no stable statement of account. The PDF may look final while its input is still moving.
This distinction becomes concrete in a leaked-key drill. Imagine a school tenant's API key begins generating unexpected model traffic during a live lesson. The operator needs current usage to decide when to revoke the key or enforce the spend ceiling. That same current number should not silently replace the tenant's already-frozen monthly charge.
Different clocks. Separate records.
The two criteria that decide the design
The first criterion is the spend ceiling versus refused traffic. A low ceiling limits exposure from the suspected key, but it can also reject valid student requests. A high ceiling preserves continuity and permits more disputed usage. There is no vendor feature that removes this trade-off. Set the ceiling from the amount of exposure the business can accept during the time it takes to detect and revoke a key, then test that choice with the real incident sequence.
For a one-person SaaS, I would keep that decision small enough to rehearse every release cycle: observe the meter, trigger the threshold, revoke or rotate the suspected credential, and verify that a replacement credential restores valid traffic. The revenue-per-hour lens matters here. A sophisticated policy nobody has time to exercise is weaker than a plain threshold with a weekly drill. This is an explicit trade: accepting some bounded exposure buys continuity for real classrooms, while lowering the threshold buys tighter exposure control by refusing more requests. Write down which side won and why before the drill starts; otherwise the first anxious minute becomes the policy meeting.
The second criterion is reproducibility. Freeze the billable snapshot once per tenant and period, attach a stable snapshot ID, and preserve the source totals used to produce it. Reissuing an invoice should read that frozen record. It should never rerun the live query and hope for the same answer.
Your product may track classroom, course, teacher, or feature dimensions. Those internal allocations are yours to justify. The platform total is the constraint they must add back to. If the allocations total 9,980 units while the platform reports 10,000, “rounding” is not a reconciliation policy; the missing 20 units need a named adjustment or an unresolved status.
Freeze first, then reconcile in TypeScript
The implementation boundary can stay boring. That is useful. This TypeScript example retrieves the current usage payload, freezes it without assuming undeclared response fields, then reconciles a platform total with internal tenant rows. It handles rate limits and surfaces real error bodies.
import { createHash } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const apiBaseUrl = ["https:/", "/api.", "infrai", ".cc/v1"].join("");
async function readUsage(attempt = 0): Promise<unknown> {
const response = await fetch(`${apiBaseUrl}/account/usage`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return readUsage(attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Usage read failed (${response.status}): ${body}`);
}
return response.json() as Promise<unknown>;
}
type Allocation = {
tenantId: string;
units: number;
};
type PeriodSnapshot = {
id: string;
period: string;
platformUnits: number;
allocatedUnits: number;
differenceUnits: number;
status: "reconciled" | "needs-review";
allocations: readonly Allocation[];
};
function freezePeriod(
period: string,
platformUnits: number,
allocations: readonly Allocation[],
): PeriodSnapshot {
if (!/^\d{4}-\d{2}$/.test(period)) throw new Error("period must be YYYY-MM");
if (!Number.isSafeInteger(platformUnits) || platformUnits < 0) {
throw new Error("platformUnits must be a non-negative integer");
}
if (allocations.some((row) => !Number.isSafeInteger(row.units) || row.units < 0)) {
throw new Error("allocation units must be non-negative integers");
}
const stableRows = [...allocations].sort((a, b) =>
a.tenantId.localeCompare(b.tenantId),
);
const allocatedUnits = stableRows.reduce((sum, row) => sum + row.units, 0);
const differenceUnits = platformUnits - allocatedUnits;
const digestInput = JSON.stringify({ period, platformUnits, stableRows });
const id = createHash("sha256").update(digestInput).digest("hex");
return Object.freeze({
id,
period,
platformUnits,
allocatedUnits,
differenceUnits,
status: differenceUnits === 0 ? "reconciled" : "needs-review",
allocations: Object.freeze(stableRows),
});
}
const usagePayload = await readUsage();
const capturedUsage = Object.freeze({
capturedAt: new Date().toISOString(),
payload: usagePayload,
});
const billingSnapshot = freezePeriod("2026-09", 10_000, [
{ tenantId: "school-a", units: 6_200 },
{ tenantId: "school-b", units: 3_780 },
]);
console.log(JSON.stringify({ capturedUsage, billingSnapshot }, null, 2));
In production, persist the snapshot with a uniqueness constraint on tenant and period. Store adjustments as new, attributable records rather than editing the frozen input. The sample's 20-unit difference is deliberately unresolved: the code surfaces it instead of manufacturing an explanation.
The operational usage read belongs upstream of the billing snapshot. The billing system still owns the freeze boundary.
Infrai provides one REST API for an entire backend: one key, one wallet, and one bill cover 295 routes across 20 modules. That breadth matters to a small team because a new capability becomes another endpoint under a known contract rather than another vendor integration. The platform total still remains the external constraint that internal dimensions must reconcile to.
Where Stripe, Lago, Metronome, and Orb fit
These products are not interchangeable, and a fair shortlist starts with the layer you want to outsource.
Stripe Billing is the natural candidate when payment collection, subscriptions, invoicing, and usage-based billing should live close together. Its meter events and billing model can remove substantial undifferentiated work. The boundary is still yours: event ingestion does not excuse an application from retaining the source facts needed to explain a disputed tenant allocation.
Lago is worth considering when an open-source billing stack and explicit metering-to-invoice workflows matter. It gives a team more ownership of the billing plane. That ownership also means operational responsibility, which can be a poor revenue-per-hour trade for a solo founder who wants to ship weekly.
Metronome focuses on usage-based billing infrastructure, while Orb centers billing around usage data and pricing logic. Both become stronger candidates as pricing plans, credits, and contract terms demand a specialized billing system rather than a small internal snapshot table. The cost is another integration and another set of semantics to reconcile with the platform meter.
Pick the runner-up over a thin internal ledger when finance needs richer adjustments, credits, plan versioning, or invoice workflows than the product team should build. This often arrives sooner than expected: a pricing rule can be easy to calculate while its correction, approval, and reissue path consumes the week. Pick a broad backend surface plus a compact snapshot ledger when integration count is the dominant constraint and billing rules remain simple. Outsource the undifferentiated part. Keep the audit boundary explicit either way.
Ship weekly.
What the leaked-key drill must prove
Run the drill end to end, not as a dashboard tour. Start with a known period snapshot. Generate a controlled stream against a test key, observe live usage approaching the ceiling, and confirm the policy refuses traffic at the intended boundary. Then revoke or rotate the suspected key and verify that authorized traffic resumes through the replacement.
Afterward, compare the settled platform total with internal tenant allocations. Record the difference and its disposition. Do not mutate the earlier invoice merely because the live meter settled later; issue an attributable adjustment under the business's billing policy.
Four artifacts are enough to make the exercise defensible: the live reading used for the incident decision, the credential action, the frozen billing snapshot, and the reconciliation record. Keep timestamps and stable IDs on all four. Short list. Long shelf life.
OWASP recommends lifecycle controls for secrets, including rotation and revocation. That makes the key action part of the drill, not an accounting footnote. The billing lesson is equally direct: meters move; invoices freeze; reconciliation connects them.
Top comments (0)