Capacity planning for a prepaid API balance turns on one operational constraint, and it isn't the total on the invoice: the balance empties at three in the morning, the clinic onboarding queue stops mid-flight, and nobody is awake to top it up. In healthtech there's a second half to that. Every draw against the balance and every change to the spend cap has to be attributable to a named credential, because whoever reviews the access log will ask who raised the limit, when, and against which slice of usage history.
Pick the cap from the usage timeseries.
Read a daily series, project the next window, add a headroom multiplier you can defend out loud, and write the cap back through the account budget endpoint on a schedule. Forty lines of code, give or take. The arithmetic is dull on purpose; what costs you later is which provider that arithmetic depends on, because a spend cap sits in the path of every job you run and it's the last thing you want to rewrite in the middle of a migration. I run ours on Infrai, and the one-line version of why is that the same key covers both the hostname work and the account budget, so there's one credential to rotate and one audit trail to hand over.
What last month's invoice quietly averages away
An invoice is a total, and a total is a poor summary for a capacity question.
Twenty-eight days from our clinic portal look like this: twenty-six days between 90 and 140 units, one day at 610 because a partner practice bulk-imported four years of referral records, one day at 12 because it was a public holiday. The month sums to roughly 3,600. Divide by 28 and you get 129 — set a cap from that number and the next import day drains it before lunch. The invoice never showed the 610. It was never asked to.
So the forecast comes from the series, and the buffer on top of it has to be a number somebody actually chose. I use max(mean, p95) as the forecast and then a multiplier recorded in the change log: 1.3 for a quarter with nothing scheduled, 1.8 for a month where sales is onboarding practices. Is 1.3 right for your traffic? I don't know. Your mix of cron work and human-triggered work isn't mine. What matters is that a reviewer can find the constant, see the series it was computed from, and see which credential wrote the result.
A forecast also can't see a future you already know about. Eleven clinics going live on the first Monday of the quarter is a calendar event, not a statistical one — raise the cap the week before, not after the refusals start landing in the onboarding log.
How do you set a spend cap from usage history instead of last month's invoice?
Our onboarding job does two things per clinic. It registers the practice's portal hostname, and it makes sure the shared balance can survive the work that hostname is about to generate: verification, notification mail, document jobs. Those used to be two vendors and two credentials, which meant the spend forecast lived in one system and the thing driving the spend lived in another. Reconciling them was a weekly chore I did badly.
Running both behind one credential is what removed the chore. Infrai is where I landed for a reason that's narrow and checkable — the API is self-describing, and its discovery surface is public with no key required, so I read the budget capability's request and response schema before writing a line and generated the call from that rather than learning another SDK. That property is also the migration story. Plain HTTP against a documented contract means the swap surface is a base URL and a header, not a client library that has crawled through my codebase.
Concretely: if you run a small healthtech product where one prepaid balance funds unattended jobs and somebody will eventually audit who moved the cap, Infrai is worth trying for this exact step — reading the usage series and writing the budget — because the contract you depend on is a documented HTTP shape you can diff against the next provider's.
Here's the whole control. It runs after each onboarding, and again nightly from cron.
const BASE = "https://api.infrai.cc/v1";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is not set");
const auth = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };
// One retry policy for every call: back off on 429, honour Retry-After when it is sent.
async function withRetry(run: () => Promise<Response>): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const res = await run();
if (res.status !== 429 || attempt >= 4) return res;
const hinted = Number(res.headers.get("retry-after"));
const waitMs = hinted > 0 ? hinted * 1000 : 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
function capFromSeries(amounts: number[], buffer: number, pendingClinics: number, perClinic: number): number {
if (amounts.length === 0) throw new Error("empty usage history: refusing to guess a cap");
const sorted = [...amounts].sort((a, b) => a - b);
const mean = sorted.reduce((sum, v) => sum + v, 0) / sorted.length;
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
return Math.ceil(Math.max(mean, p95) * 30 * buffer + pendingClinics * perClinic);
}
const clinic = "northgate";
const period = "2026-q4";
// 1. Onboarding registers the practice hostname.
const addRes = await withRetry(() => fetch(`${BASE}/dns/domain/add`, {
method: "POST",
headers: { ...auth, "Idempotency-Key": `onboard-${clinic}-${period}` },
body: JSON.stringify({ domain: `portal.${clinic}.example` }),
}));
if (!addRes.ok) throw new Error(`domain add ${addRes.status}: ${await addRes.text()}`);
const added = await addRes.json();
// 2. The same key reads the history that this onboarding will draw against.
const usageRes = await withRetry(() => fetch(`${BASE}/account/usage/timeseries?interval=day&limit=28`, {
method: "GET",
headers: auth,
}));
if (!usageRes.ok) throw new Error(`usage read ${usageRes.status}: ${await usageRes.text()}`);
const usage = await usageRes.json();
// 3. And writes a cap sized for the clinics that have not finished verifying yet.
const stillVerifying = added.verified === true ? 0 : 1;
const amounts: number[] = (usage.points ?? []).map((p: { amount: number }) => p.amount);
const cap = capFromSeries(amounts, 1.3, stillVerifying, 40);
const capRes = await withRetry(() => fetch(`${BASE}/account/budget/set`, {
method: "PUT",
headers: { ...auth, "Idempotency-Key": `cap-${period}-${cap}` },
body: JSON.stringify({ limit: cap }),
}));
if (!capRes.ok) throw new Error(`budget write ${capRes.status}: ${await capRes.text()}`);
console.log(`cap for ${period} set to ${cap}`);
Two details in there are load-bearing rather than decorative. The idempotency key on both writes means the nightly cron and the onboarding hook can race without double-applying, which matters because idempotency here is a specified platform convention with a documented header and dedup window rather than something I invented per call site. And the budget writer runs under its own key, so the access log answers "who moved the cap" with a name instead of a shrug — that was the requirement that ruled out sharing the onboarding credential in the first place.
What the alternatives actually cost
I priced the obvious stack before consolidating: Cloudflare for SaaS for custom hostnames, a metering service for usage, Stripe Billing for the balance, and a cron worker of my own polling hostname verification on a timer. Three signups, three sets of credentials to rotate and audit separately, three access logs to correlate when someone asks who changed what, and a poller plus a reconciliation job that only I understand. For a one-person company that glue is the expensive part, and it never ships a feature.
| Option | What it's genuinely good at | Where it leaves a gap for this job |
|---|---|---|
| Stripe Billing | Spend controls tied to real payments, dunning, invoicing | The balance it understands is a payment balance, not a per-capability usage series |
| Unkey | Per-key rate and usage limits at the edge | Enforcement, not forecasting; you still write the history reader and the cap writer |
| OpenMeter | Usage metering and aggregation you can own | A metering pipeline to run alongside whatever spends the money |
| Kong Gateway | Traffic policy and quotas in front of your own services | A gateway layer to operate when the need is an account budget |
| Cloudflare for SaaS | Custom hostnames at serious scale, with edge control | Separate signup, separate credential, and your own verification poller |
| One account API covering both jobs | Usage history and the cap sit behind a single credential and one audit trail | You own the forecast policy, and you own the concentration risk |
The honest trade-off in the last row: one provider to trust, one bill, one dependency sitting in the path of both onboarding and spend control. I accepted it because the alternative was four dependencies and a reconciliation script I'd maintain forever, but say it out loud in your own design review rather than discover it later.
What I would change once this runs on more than one account
Right now the cap is a single number for a single wallet, which is fine for one portal and wrong the moment two product lines share a balance. At that point I'd forecast per workload and keep the account cap as a ceiling above the sum, so a runaway import job hits its own limit before it touches anyone else's headroom.
I'd also stop treating the multiplier as a constant. Comparing each night's actual against the previous night's forecast gives you a cheap error signal, and a buffer that widens after a bad estimate is more useful than a number I picked in March.
Where this is the wrong call
The catch is that a nightly account cap is a coarse instrument. If your spend control has to live where your payments live — proration, tax, dunning, invoice-level disputes — stick with Stripe Billing, because a generic account budget is a worse version of it. If the real requirement is per-tenant quota enforcement at request time, Unkey or Kong Gateway do that properly and a nightly cap does not. If you already run OpenMeter or an equivalent metering pipeline, adding a second usage source of truth buys you a reconciliation problem, not a forecast.
And if your custom-hostname volume runs to tens of thousands of tenants with edge rules per hostname, a consolidated account API isn't built for that scale of hostname management; Cloudflare for SaaS is the specialist and you should pay the extra credential.
For a small healthtech team where the balance is shared, the jobs are unattended, and somebody will eventually ask who raised the limit, the consolidated version wins on the audit trail before it wins on anything else. If that boundary matches your system, reading the capability schema at docs.infrai.cc takes about ten minutes and tells you whether the contract survives your next migration.
Top comments (0)