Blog / Engineering
EngineeringBuilding Keel's AI credit system with the Vercel AI SDK
Every AI feature eventually needs an answer to the same question: who pays for this call, and when. Get it wrong and you either bill someone for a request that errored out, or you let a runaway client retry loop drain a balance no one approved. Keel's answer is small on purpose, a credits balance per team, a usage_events row per call, and a single place where the two ever change together.
Charge after, not before
The naive version deducts credits before the model call, then refunds on failure. That works until a refund itself fails, or a process dies mid-request, and now the balance is wrong in a way nobody can reconstruct. Keel charges inside onFinish, the callback the Vercel AI SDK fires once a stream completes successfully:
// lib/ai/credits.ts, charge and log in one transaction, only on success
export async function chargeForGeneration(teamId: string, cost: number) {
return db.transaction(async (tx) => {
await tx
.update(credits)
.set({ balance: sql`${credits.balance} - ${cost}` })
.where(eq(credits.teamId, teamId));
await tx.insert(usageEvents).values({ teamId, cost, kind: "ai_message" });
});
}
A request that errors, gets aborted, or never finishes streaming simply never reaches this function. There's nothing to refund because nothing was ever charged.
One ledger, two readers
usage_events isn't just a billing record, it's the audit trail a team can look at when their balance drops faster than expected. The credit pill in the top bar reads the running balance; the billing page's usage history reads the event log. Same source of truth, two views, no separate "usage tracking" system to keep in sync.
Provider-agnostic by accident, not by design
Keel resolves Anthropic or OpenAI from whichever API key is set in the environment, which means the credit system never had to know which provider answered the call. It only cares about tokens in, tokens out, and the cost mapping you configure. Swapping providers, or adding a third, doesn't touch billing at all.
The Keel Team
We build Keel, the Next.js foundation you'd have built yourself. We write here about the decisions behind it.