Credit-based billing
Credit-based billing meters usage as prepaid, integer credit units debited atomically before the paid work runs, rather than as raw dollars settled after the fact. Caisson's credits package inserts an append-only ledger event and decrements an account's wallet in one transaction: an insufficient balance throws before any work starts, and the balance never goes negative.
In code
export async function debit(
tx: TenantExecutor,
input: DebitInput,
): Promise<CreditResult> {
assertPositiveInt(input.amount);
// ... elided: idempotency + feature-tag resolution, the per-account SELECT ... FOR UPDATE
// wallet row lock, and the FIFO walk over unexpired grants (unexpiredGrantsFifo +
// insertConsumption) that throws InsufficientCreditsError when grants cannot cover the debit ...
const fresh = await insertEvent(tx, {
accountId: input.accountId,
eventType: input.eventType,
amount: -input.amount,
// ...
});
if (!fresh) return { balance: await balance(tx, input.accountId), idempotent: true };
const updated = await tx.query<{ balance: number }>(
`UPDATE credit_wallet SET balance = balance - $2
WHERE account_id = $1 AND balance >= $2
RETURNING balance`,
[input.accountId, input.amount],
);
if (updated.rows.length === 0) {
// Insufficient: rolls back the transaction, a failed debit leaves no trace (ADR-0007).
throw new InsufficientCreditsError(input.amount, await balance(tx, input.accountId));
}
return { balance: updated.rows[0]?.balance ?? 0, idempotent: false };
}How it holds
Integer-only, branded credits
Every credit amount is a branded Credits integer minted through asCredits (ADR-0007/ADR-0212); there is no float anywhere in the ledger, so a debit and its balance always resolve to the exact same whole number.
Debit-before-spend, atomic
grant() and debit() insert the ledger event and touch the wallet balance inside the same transaction; when the WHERE balance >= amount guard on the wallet UPDATE matches zero rows, InsufficientCreditsError throws and the ledger insert rolls back with it, so a failed debit leaves no trace.
Idempotent on a caller key or provider event id
insertEvent's INSERT ... ON CONFLICT DO NOTHING RETURNING absorbs a retried grant or debit without aborting the surrounding transaction; a caught unique-violation would poison it, so the conflict is swallowed instead and the call returns the current balance with idempotent: true.
A non-negative wallet, enforced twice
credit_wallet carries its own credit_wallet_balance_nonneg CHECK constraint, backstopping the application-level WHERE balance >= $2 guard even against a bug or a direct write that bypasses debit() entirely.