Usage-based billing only works if usage is recorded server-side at the moment of the metered action. Client-side usage tracking (localStorage, session state) can be cleared, manipulated, or simply dropped on session expiration — meaning you never bill for real consumption. CWE-20 (Improper Input Validation) applies when client-supplied usage counts are trusted. ISO 25010 functional-correctness requires that what you charge matches what was consumed. Under-billing erodes revenue directly; over-billing from timing gaps creates customer disputes.
High because client-side or asynchronously-reported usage tracking is exploitable and creates systematic under-billing that compounds with scale.
Record usage server-side at the point of the metered action, synchronously, before returning a response. Track locally for display and report to Stripe in the same request.
// app/api/ai/generate/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({ where: { id: session.userId } })
const result = await runAIGeneration(req)
// Record usage AFTER successful action — server-side, not client-side
await stripe.subscriptionItems.createUsageRecord(
user.stripe_metered_item_id,
{ quantity: 1, timestamp: 'now', action: 'increment' }
)
// Also persist locally for the usage dashboard
await db.usageRecord.create({ data: { userId: user.id, action: 'ai_generation' } })
return Response.json({ result })
}
Never accept usage quantities from the client request body. Always derive quantity from the server-side action.
ID: saas-billing.pricing-enforcement.usage-billing-accurate
Severity: high
What to look for: Determine whether the application uses usage-based billing (metered billing — charging per API call, per seat, per GB, per message, etc.). Look for Stripe metered billing setup (billing_scheme: 'tiered' or aggregate_usage), usage reporting calls (stripe.subscriptionItems.createUsageRecord()), or manual usage tracking in the database. Verify that usage is recorded at the time of the action (not retroactively), that usage records are sent to Stripe before invoice generation, and that usage counts cannot be manipulated by the client.
Pass criteria: Count every metered action endpoint — at least 1 must report usage to Stripe. Usage metrics are recorded server-side at the time of the metered action. Usage records are reported to Stripe (or equivalent) in a way that ensures they are captured before the billing cycle closes. The usage tracking cannot be bypassed by the client.
Fail criteria: Usage is tracked only in client-side state and not recorded server-side. Usage records are sent to Stripe on a schedule that might miss the billing cycle cutoff. Usage tracking can be bypassed by calling the API in a way that doesn't trigger the meter.
Skip (N/A) when: No usage-based billing detected — the application uses flat-rate subscription pricing only. Signal: no metered billing configuration, no usage record API calls, no usage_type: 'metered' in Stripe price configuration.
Detail on fail: "Usage tracking for AI credits is stored in localStorage and reported to Stripe on logout — can be cleared by user" or "API endpoint /api/ai/generate does not record usage — metered billing will never report"
Remediation: Record usage server-side at the point of action:
// app/api/ai/generate/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({ where: { id: session.userId } })
// ... run AI generation
// Record usage AFTER successful action
await stripe.subscriptionItems.createUsageRecord(
user.stripe_metered_item_id,
{ quantity: 1, timestamp: 'now', action: 'increment' }
)
// Also track locally for display purposes
await db.usageRecord.create({
data: { userId: user.id, action: 'ai_generation', quantity: 1 }
})
return Response.json({ result })
}