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

  1. Application Manager is running with the cost-governance subsystem enabled (Phase 3).
  2. The model catalogue resolves a computed cost (input/output/cache prices + PricingVersion) for the target deployment; unknown model → fail-closed.
  3. An active AgentPlan is resolvable for the call (explicit or implicit per-turn).
  4. The cap lattice has a Top-tier cap for the tenant (UC-032).
  5. Reserve / reconcile endpoints are gateway-internal — not normally called by external SDK consumers directly.

Postconditions

Success Postconditions

  1. A reservation is Held against plan budget and all applicable caps before dispatch.
  2. After the call, reconciliation writes an immutable LlmCallLedger row with actual token counts and ComputedCost.
  3. The reservation transitions to Reconciled; plan consumedBudget and cap period spend reflect the actual cost.
  4. SpendRollupJob folds the new ledger row into the relevant SpendRollup row within the next hourly run.

Failure Postconditions

  1. If headroom is insufficient at reserve time, no reservation is created and the call is denied (BudgetExhausted).
  2. If a reservation expires before reconciliation, the hold is released; a late reconciliation still writes the ledger row tagged Source = ReconciledAfterExpiry.
  3. A cancelled reservation releases its hold without writing a ledger row (the call never dispatched).
  4. 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

  1. The gateway computes an EstimatedMax cost for the call from the model catalogue (max output tokens × output price + input cost).
  2. ReserveSpend checks EstimatedMax ≤ plan.remainingBudget via PlanBudgetManager.
  3. PlanBudgetManager invokes CapLatticeResolver to confirm headroom across every applicable cap.
  4. If all gating caps have headroom, an LlmSpendReservation is created with Status = Held, ReservedAmount, EstimatedMax, PlanId, ReservedAt = now, ExpiresAt = now + 5 min.
  5. 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

  1. The gateway reports actual token counts (input, output, cache-read, cache-write) for the reservation.
  2. ReconcileSpend computes the actual ComputedCost from the catalogue prices and PricingVersion at call time.
  3. If an active subscription overrode the catalogue rate, the effective rate is snapshotted as SubscriptionRateSnapshot (see UC-035).
  4. An LlmCallLedger row is appended (immutable) with agent, tenant, provider, providerAccount, modelDeployment, model, plan, application, template, owner, token counts, ComputedCost, PricingVersion, ReservationId, Source = Reconciled.
  5. 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

  1. CancelReservation finds the Held reservation.
  2. The hold is released back to plan budget and lattice headroom.
  3. The reservation transitions to Released.
  4. 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

  1. The reservation hold is released at ExpiresAt; Status = Expired (headroom is freed for other calls).
  2. A late provider response eventually arrives and the gateway calls reconcile.
  3. ReconcileSpend still writes an LlmCallLedger row, tagged Source = ReconciledAfterExpiry for forensic visibility.
  4. The spend is recorded against the relevant caps (audit trail preserved) even though the hold had been released.
  5. 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

  1. SpendRollupJob reads ledger rows appended since the last run.
  2. For each (tenantId, providerId, modelDeploymentId, ownerUserId?, applicationId?, templateId?, day) key, it upserts the SpendRollup row.
  3. Aggregates updated: totalCost, totalInputTokens, totalOutputTokens, totalCachedTokens, callCount.
  4. 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