GenerateSaaS

Metering your features

Debit credits from any server-side path - gate refusable work, settle incurred cost, stay idempotent, and bound concurrent exposure.

From any server-side path - a Hono route, background job, webhook - derive the price, gate work that can still be refused, perform it, then settle its full cost:

import { Billing, costToCredits } from "@repo/payments";

const billing = await Billing(session); // or BillingForWorkspace(orgId, userId) off-request
const credits = costToCredits(0.4, "seo"); // this lookup costs you $0.40, margined as "seo"

if (!billing.hasCreditsFor(credits)) {
	return c.json({ error: "Insufficient credits" }, 402);
}

const data = await fetchSerpResults(query); // your paid upstream call

await billing.settleCredits(credits, {
	source: "system",
	type: "credit_usage",
	feature: "seo.serp-lookup",
	description: "SERP lookup",
	metadata: { cost: { usd: 0.4, category: "seo" } } // what the vendor charged you
}, {
	cause: "usage",
	idempotencyKey: `serp_${runId}`
});
OperationBehaviorUse when
removeCreditsFull debit or INSUFFICIENT_CREDITS; never overdraws.Work can still be refused.
settleCreditsFull signed debit; may create debt.Usage, refund, or chargeback already happened.
  • Idempotency: pass a globally unique run id created once for the logical operation and reused on retries, so a retry resolves without settling twice.
  • Debt: a negative balance repays through normal signed addition (adding 12 to -10 leaves 2), and hasCreditsFor keeps new metered work gated while debt exists.
  • Feature tags: tag every usage debit with a feature id ("ai.chat", "seo.serp-lookup"). It lands on the billing log row, filters queryBillingLogs({ feature }), powers sumCreditUsageByFeature(...), and labels the row in the transactions UI.
  • Vendor cost: stamp metadata.cost with the vendor's USD charge before markup and its markup category. The admin finance dashboard reads it to set spend against revenue; a row without it is excluded, so leave the stamp off rather than sending { usd: 0 } for a cost you do not know.

Token-billed streaming only knows its cost once the run ends, so the shipped AI surfaces add a pre-flight estimate gate and a mid-run budget stop. An estimate miss or a concurrent run can still expose debt rather than hide incurred usage.

Concurrency caps

Each live run can spend up to a balance-worth before settlement catches up, so exposure scales with how many runs draw on the same balance. A chat turn counts while it streams and a cloud automation while it executes; BYOK and runner runs never count.

LimitWhereDefaultWhat it bounds
maxConcurrentRunsthe plan (pricing.ts)free 2, starter 5, pro 10Runs open against one balance across everyone drawing on it. The tier's throughput promise and its exposure bound.
perUserconfig.ai.runLimits4Runs one user may hold open. Keeps multi-tab work smooth and stops one member starving the team.
perPayerconfig.ai.runLimits16The per-balance fallback for a plan that names no maxConcurrentRuns.

On a personal (no-organization) account the payer is the user, so the tighter of perUser and the plan's ceiling binds.

Refusals differ by surface. A chat turn answers 429 with a message naming how many runs are in flight. A due cloud automation waits: it defers to the next 5-minute sweep, burning zero credits, writing zero usage, and recording no failure on the row.

Raise perPayer for larger teams, lower it to tighten worst-case debt. BYOK and runner runs are never counted.

On this page