#AI#大模型#独立开发

Prepaid AI Credits Ledger: Atomic Reservations, Actual Settlement, and Traceable Refunds

A single Credits balance can overspend under concurrent requests, timeouts, and retries. This article builds a billing state machine with double-entry records, expiring lots, atomic holds, actual settlement, releases, and compensating refunds.

Key Points

  • Topic Focus: AI、大模型、独立开发
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Credits may look like nothing more than balance = balance - cost, but concurrent streaming requests immediately break this model. Two requests can both see a sufficient balance and start running, leaving the final cost higher than the balance. A cleanup task may release a hold after a worker times out even though the model is still generating. A retried payment callback may also issue the same credits twice. Even with locking, a balance column cannot explain which purchase supplied the credits, when they expire, or why they were refunded.

AI Product Pricing Models explains when Credits are appropriate, while Tenant Quotas for AI Platforms manages resource fairness. This article treats Credits as a product billing unit and models their accounting state machine. It does not present Credits as cash or as a general accounting conclusion; taxation, revenue recognition, and obligations for unused balances remain subject to finance policies and local rules.

Immutable Entries Are the Source of Truth for Balances

Define a smallest integer unit and a fixed product version, and prohibit floating-point values. Purchased grants, promotions, consumption, expiration, and refunds use different accounts or reason codes. Every transaction has at least two entries in opposite directions, with debits and credits netting to zero. You may cache available, but it must be reconstructible from the entries, and corrections may only append compensating transactions.

text
grant:    issuer_liability  -1000 | customer_available +1000
reserve:  customer_available -120 | customer_held      +120
settle:   customer_held      -83  | service_consumed    +83
release:  customer_held      -37  | customer_available  +37
service_refund: service_consumed -83 | original_lots +83

This is only an example of a conservation relationship; it does not mean Credits are exchangeable for legal tender. Each grant lot records its source, applicable products, effectiveAt, expiresAt, and restrictions. A consumption order such as first-expiring, first-used must be a versioned policy. Promotional and purchased credits cannot be merged into one balance if they have different refund rights.

Atomically Reserve the Worst-Case Cost Before Execution

At request admission, the server estimates the maximum permitted consumption instead of trusting the client's max_tokens. In the same database transaction, lock the account and the grant lots to be reserved, verify that they are available, unexpired, and compatible with the product, create a unique operation and hold, and then move the ledger entries. You can use SERIALIZABLE, row locks, or versioned conditional updates, but the affected row count must match exactly. Reusing the same idempotency key with different parameters is an immediate conflict.

Call the model only after the reservation succeeds. For streaming or long-running Agent tasks, capture usage in small segments. Each segment is unique by (operationId, usageEventId). Its transaction verifies that the hold remains open, atomically increments settled usage, and decrements remainingHold; a duplicate segment returns the original result. When usage approaches the limit, stop starting new tool rounds or generating more output. Raising the limit requires a new top-up reservation and explicit authorization. The operation ID from MCP Timeouts, Retries, and Idempotency spans model execution, ledger entries, and external billing.

text
AVAILABLE -> OPEN -> PARTIALLY_SETTLED -> SETTLED
              \-------------------------> RELEASED
SETTLED -> PARTIALLY_REFUNDED -> REFUNDED

Each capture records the actor, reason, usage event, policy version, and timestamp while preserving captured + remainingHold = originalHold + topUps. Only after a final marker is present and all known usage has been posted should the same transaction release the remainder and close the hold. Replaying the final operation returns the original result. This prevents the first small segment from prematurely releasing credits that later work still needs.

When the hold is created, the reserved lots and their expiration determination are fixed. After a valid reservation, a lot may still be settled even if it crosses expiresAt. If the original lot has expired when credits are released, the remainder moves to expired rather than becoming available again, preventing a long-running task from extending the credits' lifetime.

TTL Is Not Proof That an Automatic Refund Is Valid

A hold may have a lease, but every claim must acquire a monotonically increasing fence/attempt through CAS. Dispatch, heartbeat, usage capture, and final settlement must all carry and validate the current fence. If an old worker resumes, its actions can only be rejected. External calls also reuse a stable provider request ID. Lease expiration alone is not enough to release a hold based on a stale heartbeat. If the result is unknown, move the operation to RECONCILING, neither releasing the hold nor repeating the call. Release it only when authoritative evidence confirms that the request was not dispatched or executed; if it was executed, settle the actual usage.

Cancellation is also settled according to actual consumption. Inputs already sent, outputs already generated, and irreversible tool costs may still be charged, while the unused portion is released. The product should explain the calculation rules before the user acts. It should neither automatically pass every internal provider failure on to the user nor promise that every cancellation will have zero cost.

A Refund Is a Restricted Compensation, Not a Deleted Charge

A service Credits refund references the original settle allocation, uses a unique refund ID, and atomically verifies that cumulative refunds do not exceed the net refundable amount. Entries restore each original grant lot proportionally: valid lots return to available, while expired lots move to expired. If compensation is granted, issue a separate new grant with its own source, permissions, and expiration instead of using a refund to revive old credits. Cash refunds, chargebacks, and service Credits refunds use different transaction types. A payment reversal after consumption enters a debt or suspension process rather than masquerading as an ordinary balance change. External payment callbacks and Stripe Credit Grant callbacks are also deduplicated, with one source of truth explicitly designated and continuously reconciled.

PostgreSQL Transaction Isolation can be used to validate concurrent deductions. Tests must also cover one hundred requests competing for the final balance, replayed segmented captures, replayed final operations, resumed workers with stale fences, unknown provider results, refunds spanning lots that expire, and refunds exceeding the original amount. Assert that available + held + consumed + expired remains conserved against all grants and adjustments at every point, and that available never becomes negative.

For rollout, first generate opening entries from legacy balances and compare dual reads while allowing only the legacy system to deduct credits. Once the difference reaches zero, switch to the new reservation flow. During rollback, stop creating new holds, allow existing operations to settle or reconcile, and restore legacy admission without deleting the new entries. Correct errors through approved compensating transactions. A reliable Credits system is not one whose balance page merely looks correct. Every reservation, consumption, release, and refund must correspond to a unique business fact and remain replayable after a crash.

Further Reading

Related

Related Content