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}`
});| Operation | Behavior | Use when |
|---|---|---|
removeCredits | Full debit or INSUFFICIENT_CREDITS; never overdraws. | Work can still be refused. |
settleCredits | Full 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
12to-10leaves2), andhasCreditsForkeeps new metered work gated while debt exists. - Feature tags: tag every usage debit with a
featureid ("ai.chat","seo.serp-lookup"). It lands on the billing log row, filtersqueryBillingLogs({ feature }), powerssumCreditUsageByFeature(...), and labels the row in the transactions UI. - Vendor cost: stamp
metadata.costwith the vendor's USD charge before markup and its markupcategory. 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.
| Limit | Where | Default | What it bounds |
|---|---|---|---|
maxConcurrentRuns | the plan (pricing.ts) | free 2, starter 5, pro 10 | Runs open against one balance across everyone drawing on it. The tier's throughput promise and its exposure bound. |
perUser | config.ai.runLimits | 4 | Runs one user may hold open. Keeps multi-tab work smooth and stops one member starving the team. |
perPayer | config.ai.runLimits | 16 | The 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.
Credits
Meter any paid feature with usage-based credits - per-plan grants, recurring re-grants, cost-priced debits, custom top-ups, and refund handling.
Pricing and margins
costToCredits economics - creditsPerUsd, markups per category, the quantization step, the minimum charge, and the plan-solvency check.