Use Case 034: Spend Reservation & Reconciliation
Overview
| Property |
Value |
| Use Case ID |
UC-034 |
| Use Case Name |
Spend Reservation & Reconciliation |
| Module |
Agent Identity — Cost Governance |
| Priority |
Critical |
| Status |
✅ Implemented |
| Version |
1.0 |
| Last Updated |
June 15, 2026 |
Implementation status (agent-identity release, June 2026). Implemented: pre-flight LlmSpendReservation (5-min TTL) gated on plan headroom + the cap lattice, post-call reconciliation into the append-only LlmCallLedger, and forensic LedgerSource (Reconciled / ReconciledAfterExpiry). API at /api/spend (reserve / reconcile / cancel). The hourly SpendRollup aggregation is covered by UC-036.
Description
This use case describes the reserve / reconcile call path and the append-only spend ledger at the heart of cost governance in the Application Manager agent-identity release. Before any LLM call is dispatched, the gateway places a short-lived LlmSpendReservation (a pre-flight hold) against the active plan budget (UC-033) and every applicable cap in the lattice (UC-032). After the provider responds, the gateway reconciles with actual token counts, writing an immutable LlmCallLedger row that records exactly what was spent and against which provider account, deployment, model, plan, application, template, and owner.
A Hangfire SpendRollupJob pre-aggregates ledger rows into the SpendRollup table hourly so the insights dashboard (UC-036) queries cheap rollups instead of raw ledger rows. This use case also covers the lifecycle edge cases that make the accounting honest: reservation expiry with ReconciledAfterExpiry tagging, cap period rollover for long-running plans, and Strict mid-stream aborts. The SDK master design lives in riptide-sdk/docs/plans/AGENT-IDENTITY-ARCHITECTURE.md; the Application Manager plan in docs/internal/agent-identity-plan.md (Section 3, §3.1 entities and §3.9 lifecycle edge cases).
Actors
| Actor |
Description |
Role |
LLM Gateway (LlmDispatchService) |
Calls reserve/reconcile around every dispatch |
Primary |
| PlanBudgetManager |
Gates reservation against plan budget (UC-033) |
Supporting |
| CapLatticeResolver |
Confirms cap-lattice headroom (UC-032) |
Supporting |
| SpendRollupJob |
Hourly Hangfire job aggregating ledger into rollups |
Supporting |
| Cost Administrator |
Views ledger / introspects reservations |
Supporting |
| System |
Application Manager platform |
Supporting |
| LLM Provider |
External model endpoint returning actual token usage |
External |
Preconditions
- Application Manager is running with the cost-governance subsystem enabled (Phase 3).
- The model catalogue resolves a computed cost (input/output/cache prices +
PricingVersion) for the target deployment; unknown model → fail-closed.
- An active
AgentPlan is resolvable for the call (explicit or implicit per-turn).
- The cap lattice has a Top-tier cap for the tenant (UC-032).
- Reserve / reconcile endpoints are gateway-internal — not normally called by external SDK consumers directly.
Postconditions
Success Postconditions
- A reservation is
Held against plan budget and all applicable caps before dispatch.
- After the call, reconciliation writes an immutable
LlmCallLedger row with actual token counts and ComputedCost.
- The reservation transitions to
Reconciled; plan consumedBudget and cap period spend reflect the actual cost.
SpendRollupJob folds the new ledger row into the relevant SpendRollup row within the next hourly run.
Failure Postconditions
- If headroom is insufficient at reserve time, no reservation is created and the call is denied (
BudgetExhausted).
- If a reservation expires before reconciliation, the hold is released; a late reconciliation still writes the ledger row tagged
Source = ReconciledAfterExpiry.
- A cancelled reservation releases its hold without writing a ledger row (the call never dispatched).
- A Strict mid-stream exhaustion aborts the stream and returns
BudgetExhausted with partial content; the consumed portion is still reconciled to the ledger.
Primary Flow Sequence
sequenceDiagram
participant GW as LLM Gateway
participant PBM as PlanBudgetManager
participant CLR as CapLatticeResolver
participant DB as IdentityDbContext
participant Prov as LLM Provider
participant Ledger as LlmCallLedger
GW->>PBM: ReserveSpend(planId, estimatedMax)
PBM->>PBM: estimatedMax <= plan.remainingBudget ?
PBM->>CLR: confirm cap-lattice headroom
CLR-->>PBM: OK
PBM->>DB: insert LlmSpendReservation (Held, ExpiresAt = +5m)
PBM-->>GW: reservationId
GW->>Prov: dispatch completion
Prov-->>GW: response + actual token counts
GW->>PBM: ReconcileSpend(reservationId, tokenCounts)
PBM->>DB: reservation Status = Reconciled
PBM->>Ledger: append row (ComputedCost, PricingVersion, Source=Reconciled)
PBM->>DB: plan.consumedBudget += ComputedCost
PBM-->>GW: reconciled
UC-034a: Reserve Spend (Pre-Flight)
Triggers
LlmDispatchService calls POST /api/spend/reserve before dispatching a completion.
Basic Flow
- The gateway computes an
EstimatedMax cost for the call from the model catalogue (max output tokens × output price + input cost).
ReserveSpend checks EstimatedMax ≤ plan.remainingBudget via PlanBudgetManager.
PlanBudgetManager invokes CapLatticeResolver to confirm headroom across every applicable cap.
- If all gating caps have headroom, an
LlmSpendReservation is created with Status = Held, ReservedAmount, EstimatedMax, PlanId, ReservedAt = now, ExpiresAt = now + 5 min.
- The reservation id is returned; the gateway dispatches the call.
Alternative Flows
- A1: Plan budget insufficient — Denied; no reservation;
BudgetExhausted.
- A2: Cap exhausted (HardStop) — Denied; no reservation;
BudgetExhausted.
- A3: Cap exhausted (ApprovalRequired) — Reservation set to
PendingApproval and parked (see UC-032d).
- A4: Unknown model — Catalogue cannot price it; call rejected (fail-closed) before a reservation is attempted.
UC-034b: Reconcile with Actual Token Counts
Triggers
- The provider response returns;
LlmDispatchService calls POST /api/spend/reconcile.
Basic Flow
- The gateway reports actual token counts (input, output, cache-read, cache-write) for the reservation.
ReconcileSpend computes the actual ComputedCost from the catalogue prices and PricingVersion at call time.
- If an active subscription overrode the catalogue rate, the effective rate is snapshotted as
SubscriptionRateSnapshot (see UC-035).
- An
LlmCallLedger row is appended (immutable) with agent, tenant, provider, providerAccount, modelDeployment, model, plan, application, template, owner, token counts, ComputedCost, PricingVersion, ReservationId, Source = Reconciled.
- The reservation transitions to
Reconciled; plan consumedBudget and cap period spend are updated with the actual cost.
Alternative Flows
- A1: Actual cost below reservation — Difference is released back to plan and lattice headroom.
- A2: Actual cost above EstimatedMax — The reservation overrun is recorded; the excess counts against caps (a subsequent reservation may be denied), but the completed call still reconciles to the ledger.
- A3: Reconcile after expiry — Handled in UC-034d (
ReconciledAfterExpiry).
- A4: Strict mid-stream abort — The partial token counts produced before the abort are reconciled; the response returns
BudgetExhausted with partial content; no auto-continuation.
UC-034c: Cancel / Release a Held Reservation
Triggers
- The call never dispatches (validation failure, schema rejection, provider error before tokens); the gateway calls
DELETE /api/spend/reservations/{rid}.
Basic Flow
CancelReservation finds the Held reservation.
- The hold is released back to plan budget and lattice headroom.
- The reservation transitions to
Released.
- No
LlmCallLedger row is written — no spend occurred.
Alternative Flows
- A1: Reservation already reconciled — Cancel is a no-op; the ledger row stands.
- A2: Reservation already expired — The hold was already released; cancel is idempotent.
UC-034d: Reservation Expiry & ReconciledAfterExpiry
Triggers
- A reservation's
ExpiresAt (default 5 min) passes before reconciliation; a late reconciliation later arrives.
Basic Flow
- The reservation hold is released at
ExpiresAt; Status = Expired (headroom is freed for other calls).
- A late provider response eventually arrives and the gateway calls reconcile.
ReconcileSpend still writes an LlmCallLedger row, tagged Source = ReconciledAfterExpiry for forensic visibility.
- The spend is recorded against the relevant caps (audit trail preserved) even though the hold had been released.
- The reservation row remains
Expired; the ledger row is the durable spend record.
Alternative Flows
- A1: Late reconcile pushes a cap negative — The cap goes (transiently) over because headroom was reused; subsequent reservations against that cap
HardStop until the period rolls over or the cap is raised.
- A2: Late reconcile after plan completed — The ledger row is still written; the plan's
consumedBudget is updated for accounting even on a terminal plan.
sequenceDiagram
participant GW as LLM Gateway
participant DB as IdentityDbContext
participant Sweep as Expiry Sweep
participant Ledger as LlmCallLedger
GW->>DB: reserve (Held, ExpiresAt = +5m)
Note over Sweep: 5 min elapse, no reconcile
Sweep->>DB: reservation Status = Expired; release hold
GW->>DB: late ReconcileSpend(reservationId, tokenCounts)
DB->>Ledger: append row (Source = ReconciledAfterExpiry)
DB->>DB: record spend against caps (hold already released)
DB-->>GW: reconciled (after expiry)
UC-034e: SpendRollup Hourly Aggregation
Triggers
- The Hangfire
SpendRollupJob fires (hourly).
Basic Flow
SpendRollupJob reads ledger rows appended since the last run.
- For each
(tenantId, providerId, modelDeploymentId, ownerUserId?, applicationId?, templateId?, day) key, it upserts the SpendRollup row.
- Aggregates updated:
totalCost, totalInputTokens, totalOutputTokens, totalCachedTokens, callCount.
- The insights dashboard (UC-036) reads
SpendRollup, never raw ledger rows.
Alternative Flows
- A1: ReconciledAfterExpiry rows — Folded into rollups identically; the
Source tag is for forensics, not aggregation.
- A2: Day-boundary spanning calls — Each ledger row is attributed to its own call's day; the rollup key's
day comes from the ledger row, computed in the tenant time zone (or UTC).
- A3: Late ledger rows arriving after a rollup ran — Picked up on the next hourly run; rollups are upserts, not append-only.
API Endpoints
| Method |
Path |
Auth |
Purpose |
| POST |
/api/spend/reserve |
X-Api-Key (gateway-internal) |
Pre-flight reservation against plan + cap lattice |
| POST |
/api/spend/reconcile |
X-Api-Key (gateway-internal) |
Post-call reconciliation; appends ledger row |
| DELETE |
/api/spend/reservations/{rid} |
X-Api-Key (gateway-internal) |
Cancel a held reservation |
| GET |
/api/spend/ledger |
X-Api-Key + cost.insights.view.tenant |
Query the spend ledger (GetSpendLedger) |
| GET |
/api/spend/by-period |
X-Api-Key + cost.insights.view.tenant |
Spend grouped by period (GetSpendByPeriod) |
| GET |
/api/spend/reservations/{rid} |
X-Api-Key + cost.cap.view |
Introspect a reservation (IntrospectReservation) |
// POST /api/spend/reconcile
{
"reservationId": "c41a7e90-2b33-7d54-a8e1-66bb2299aa01",
"modelDeploymentId": "anthropic-prod-sonnet-cloud",
"tokenCounts": {
"input": 1842,
"output": 533,
"cacheRead": 1200,
"cacheWrite": 0
}
}
// 200 OK
{
"ledgerRowId": "e90b3c71-44ad-7f22-b711-0c5a18de77bb",
"reservationId": "c41a7e90-2b33-7d54-a8e1-66bb2299aa01",
"computedCost": 0.0214,
"currency": "USD",
"pricingVersion": "anthropic-2026-05",
"subscriptionRateSnapshot": null,
"source": "Reconciled",
"planRemainingBudget": 11.9786,
"reconciledAt": "2026-06-09T14:11:07Z"
}
Business Rules
| Rule |
Description |
| BR-1 |
Every dispatch is preceded by a reservation against plan budget AND all applicable caps. |
| BR-2 |
Reservations default ExpiresAt = ReservedAt + 5 minutes. |
| BR-3 |
LlmCallLedger is append-only and immutable; rows are never updated or deleted. |
| BR-4 |
Reconciliation computes ComputedCost from catalogue prices + PricingVersion at call time. |
| BR-5 |
When a subscription overrode the catalogue rate, SubscriptionRateSnapshot is recorded on the ledger row (UC-035). |
| BR-6 |
A reservation reconciled after ExpiresAt writes a ledger row tagged Source = ReconciledAfterExpiry; spend still counts against caps. |
| BR-7 |
A cancelled reservation writes no ledger row (no spend occurred). |
| BR-8 |
Long-running plans crossing a cap period boundary keep allocatedBudget; post-boundary reservations draw from new-period headroom. |
| BR-9 |
Strict mid-stream exhaustion aborts the stream with BudgetExhausted + partial content; the partial spend is reconciled. |
| BR-10 |
SpendRollupJob runs hourly, upserting SpendRollup rows; the dashboard reads rollups, not raw ledger. |
| BR-11 |
Reserve / reconcile / cancel endpoints are gateway-internal; external callers do not invoke them directly. |
| BR-12 |
Cap period boundaries are computed in the tenant's configured time zone (or UTC). |
Data Requirements
LlmSpendReservation
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key |
| PlanId |
Guid |
FK → AgentPlan |
| ReservedAmount |
decimal |
≥ 0; current hold amount |
| EstimatedMax |
decimal |
≥ ReservedAmount; worst-case cost estimate |
| ReservedAt |
DateTimeOffset |
Set on reserve |
| ExpiresAt |
DateTimeOffset |
Default ReservedAt + 5 min |
| Status |
enum |
Held, PendingApproval, Reconciled, Released, Expired |
LlmCallLedger
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key; append-only |
| AgentId |
Guid |
Agent that incurred the spend |
| TenantId |
Guid |
Owning tenant |
| ProviderId |
string |
LLM provider |
| ProviderAccountId |
Guid |
FK → ProviderAccount |
| ModelDeploymentId |
Guid |
FK → ModelDeployment |
| Model |
string |
Model id |
| PlanId |
Guid |
FK → AgentPlan |
| ApplicationId |
Guid? |
Bound application (reporting) |
| TemplateId |
string? |
Agent template (reporting aggregation) |
| OwnerUserId |
Guid |
Agent's human owner |
| InputTokens |
int |
≥ 0 |
| OutputTokens |
int |
≥ 0 |
| CacheReadTokens |
int |
≥ 0 |
| CacheWriteTokens |
int |
≥ 0 |
| ComputedCost |
decimal |
≥ 0 |
| PricingVersion |
string |
Catalogue pricing version at call time |
| SubscriptionRateSnapshot |
json? |
Null unless a subscription rate applied (UC-035) |
| ReservationId |
Guid |
FK → LlmSpendReservation |
| Source |
enum |
Reconciled, ReconciledAfterExpiry |
| CreatedAt |
DateTimeOffset |
Append timestamp |
SpendRollup
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key |
| TenantId |
Guid |
Part of rollup key |
| ProviderId |
string |
Part of rollup key |
| ModelDeploymentId |
Guid |
Part of rollup key |
| OwnerUserId |
Guid? |
Nullable rollup key dimension |
| ApplicationId |
Guid? |
Nullable rollup key dimension |
| TemplateId |
string? |
Nullable rollup key dimension |
| Day |
DateOnly |
Day bucket (tenant time zone or UTC) |
| TotalCost |
decimal |
Sum of ComputedCost |
| TotalInputTokens |
long |
Sum |
| TotalOutputTokens |
long |
Sum |
| TotalCachedTokens |
long |
Sum of cache-read + cache-write |
| CallCount |
long |
Number of ledger rows folded in |
Security Considerations
- Authentication: Reserve / reconcile / cancel are
X-Api-Key-authenticated and gateway-internal. Ledger and reservation read routes require X-Api-Key plus the relevant cost capability.
- Authorization / capabilities:
cost.insights.view.tenant / cost.insights.view.agency for ledger/period reads; cost.cap.view for reservation introspection.
- Data protection: The ledger contains no prompt/response content or secrets — only token counts, costs, and identifiers; it is tenant-scoped.
- Audit (actor chain): Reserve / reconcile events flow into
AuditLog on the actor-chain pipeline; the ledger itself is the immutable spend record. ReconciledAfterExpiry tagging preserves forensic visibility of late settlements.
- Immutability:
LlmCallLedger rows are never mutated; corrections are additive, preserving an auditable history.
Testing Scenarios
| ID |
Scenario |
Expected Result |
| T-1 |
Reserve within plan + lattice headroom |
Reservation Held; ExpiresAt = +5 min |
| T-2 |
Reserve with insufficient plan budget |
Denied; no reservation; BudgetExhausted |
| T-3 |
Reserve against an exhausted HardStop cap |
Denied; no reservation; BudgetExhausted |
| T-4 |
Reserve against an ApprovalRequired cap |
Reservation PendingApproval (UC-032d) |
| T-5 |
Reserve with unknown model |
Rejected (fail-closed) before reservation |
| T-6 |
Reconcile with actual token counts |
Ledger row appended; reservation Reconciled; plan/cap spend updated |
| T-7 |
Actual cost below reservation |
Difference released to plan + lattice |
| T-8 |
Actual cost above EstimatedMax |
Overrun recorded; ledger row written; next reservation may HardStop |
| T-9 |
Cancel a held reservation |
Hold released; Released; no ledger row |
| T-10 |
Cancel an already-reconciled reservation |
No-op; ledger row stands |
| T-11 |
Reservation expires before reconcile |
Status = Expired; hold released |
| T-12 |
Late reconcile after expiry |
Ledger row tagged ReconciledAfterExpiry; spend counts against caps |
| T-13 |
Late reconcile pushes a cap over |
Cap goes over; subsequent reservations HardStop until rollover/raise |
| T-14 |
Late reconcile after plan completed |
Ledger row written; plan consumedBudget updated |
| T-15 |
Strict mid-stream exhaustion |
Stream aborts; BudgetExhausted + partial content; partial spend reconciled |
| T-16 |
Lenient mid-stream exhaustion |
In-flight stream completes; reconciled in full (UC-032 A2) |
| T-17 |
Plan crosses cap period boundary |
allocatedBudget retained; post-boundary reservation draws new-period headroom |
| T-18 |
Cache-read/write tokens in reconcile |
Cache token counts recorded and priced per catalogue |
| T-19 |
Subscription rate active at call time |
SubscriptionRateSnapshot populated on ledger row (UC-035) |
| T-20 |
SpendRollupJob hourly run |
New ledger rows folded into SpendRollup; aggregates correct |
| T-21 |
ReconciledAfterExpiry row in rollup |
Folded identically; Source tag ignored for aggregation |
| T-22 |
Late ledger row after a rollup ran |
Picked up next hourly run; rollup upserted |
| T-23 |
Day-boundary spanning calls |
Each row attributed to its own day; rollup day from ledger |
| T-24 |
GET /api/spend/ledger with tenant scope |
Returns tenant's rows only |
| T-25 |
Reservation introspection |
Returns reservation status, amount, expiry, plan |
| T-26 |
Immutability — attempt to mutate a ledger row |
Rejected; ledger is append-only |
- UC-030: LLM Gateway Dispatch & Structured Output — the request lifecycle that calls reserve/reconcile around dispatch.
- UC-032: Budget Cap Lattice & Enforcement — the caps reservations are checked against.
- UC-033: Agent Plan Lifecycle & Budget Delegation — the plan budget reservations draw from.
- UC-035: Provider Subscriptions & Utilization — subscription rates snapshotted on the ledger.
- UC-036: Cost Insights Dashboard — reads
SpendRollup produced by the rollup job.
Revision History
| Version |
Date |
Author |
Notes |
| 1.0 |
June 9, 2026 |
Platform Architecture Team |
Initial draft |