Two-Phase Spend
How usertrust uses banking-style authorization holds for every LLM call.
Every governed LLM call in usertrust follows a two-phase spend lifecycle. This is the same pattern banks use for credit card authorization holds: reserve funds first, settle or release after the outcome is known.
No LLM call executes without a pending hold. No budget can be overspent.
The Banking Analogy
When you swipe a credit card at a restaurant, the bank does not immediately deduct the final amount. Instead:
- The terminal places an authorization hold for the estimated total
- The restaurant submits the final charge (which may differ slightly)
- The bank settles the hold into a real charge, or voids it if the transaction is cancelled
usertrust applies this identical pattern to every LLM API call. The "card" is your token budget. The "restaurant" is the LLM provider.
The Four Steps
Every call through a governed client executes this sequence inside govern.ts:
// 1. PENDING — reserve tokens atomically
await engine.spendPending({ transferId, amount: estimatedCost });
try {
// 2. Forward to LLM SDK
const response = await originalFn.apply(thisArg, args);
// 3. POST — settle the hold
await engine.postPendingSpend(transferId);
return { response, governance: receipt };
} catch (err) {
// 4. VOID — release the hold
await engine.voidPendingSpend(transferId);
throw err;
}| Step | TigerBeetle Operation | Budget Effect |
|---|---|---|
| PENDING | Create pending transfer | Tokens reserved (debited from available) |
| Forward | None (pure LLM call) | No change |
| POST | Post pending transfer | Hold becomes permanent spend |
| VOID | Void pending transfer | Reserved tokens returned to available |
Five Failure Modes
The two-phase lifecycle must handle every combination of success and failure across the LLM call, the ledger, and the audit chain. These are defined in Spec Section 15 and are exhaustively tested.
15.1 -- LLM Succeeds, POST Fails
The LLM returned a valid response, but the ledger failed to settle the hold. The response is still returned to the caller with settled: false on the receipt. A settlement_ambiguous event is written to the audit chain.
The hold remains pending in TigerBeetle and will eventually expire.
15.2 -- LLM Fails (Retryable)
The LLM provider returned an error (rate limit, server error, etc.). The pending hold is voided immediately, returning tokens to the available budget. The original error is propagated to the caller.
15.3 -- Audit Write Fails After POST
The LLM call and ledger settlement both succeeded, but the audit chain write failed. The response is returned with an auditDegraded flag. The failed audit event is written to the dead-letter queue.
usertrust never fails the user on audit degradation. If the LLM call succeeded and the ledger settled, the response is always returned. Audit failures are recoverable.
15.4 -- TigerBeetle Unreachable
The ledger is unreachable before the LLM call starts. usertrust throws a LedgerUnavailableError and does not forward the request to the provider. No tokens are at risk because no hold was placed.
Streaming Failure
The LLM stream starts successfully but errors mid-stream. The pending hold is voided, and the governance promise rejects. Partial stream content may have already been consumed by the caller.
The TigerBeetle Ledger
usertrust uses TigerBeetle for real double-entry accounting -- not a simple counter. Seven transfer codes model the full range of financial operations:
| Code | Purpose |
|---|---|
PURCHASE | Initial budget allocation (fund an account) |
SPEND | Standard governed LLM call cost |
TRANSFER | Move tokens between accounts |
REFUND | Return tokens after a voided or failed call |
ALLOCATION | Reserve tokens for a sub-budget or scope |
TOOL_CALL | Cost attributed to an LLM tool/function call |
A2A_DELEGATION | Tokens delegated to an agent-to-agent call |
Every transfer is a double-entry operation: one account is debited, another is credited. The ledger always balances.
Why This Matters
The two-phase lifecycle provides two guarantees that simple balance checks cannot:
-
Atomic budget enforcement -- The PENDING hold reserves tokens before the LLM call starts. Even if multiple concurrent calls race against the same budget, TigerBeetle's transfer atomicity prevents over-spend.
-
Failure-safe accounting -- Every failure path (provider error, ledger error, audit error, stream error) has a defined recovery. Tokens are never lost in limbo -- they are either settled or voided.
Without two-phase spend, a naive "check balance then call" approach is vulnerable to time-of-check-to-time-of-use (TOCTOU) races. Two concurrent calls could both pass the balance check and both succeed, spending more than the budget allows.