A per-user spending limit needs to affect whether an application sends a request. Recording token usage afterward helps explain costs, but it cannot prevent several concurrent requests from starting against the same remaining balance.
My ASP.NET Core sample addresses that admission decision with reservations. It authenticates a user, reserves estimated exposure before contacting the provider, and records known or uncertain costs afterward. This walkthrough refers to commit f43b206; it does not assume current model prices or claim live OpenAI validation.
The caller submits a prompt. The server chooses the user account from a configured bearer token, rather than accepting a user ID in request JSON. That distinction keeps a caller from selecting someone else's budget.
The request contract is deliberately small:
public sealed record ChatRequest(string Prompt);
After authentication, the endpoint passes the resolved identity separately:
var reply = await gateway.SendAsync(
userId, request.Prompt, context.RequestAborted);
The server maps configured token hashes to identities. The endpoint's JSON options reject unmapped members, so a request that adds a user field gets a 400 instead of choosing a budget. Model selection, prices, credentials, and budgets also come from server configuration. The provider credential and the user's authentication token serve different purposes.
The policy calculates exposure from the maximum prompt size, an input overhead allowance, maximum output tokens, and explicitly configured prices:
return checked(
((MaxPromptUtf8Bytes + InputOverheadTokenAllowance) *
InputUsdPerMillionTokens +
MaxOutputTokens * OutputUsdPerMillionTokens) / 1000000m);
The input calculation allows one token per UTF-8 byte plus configured overhead. It is an admission estimate for this text-only request shape, rather than a tokenizer. The application must maintain suitable allowances and prices for its selected model.
Retries also create exposure. Reserving enough for one attempt would leave later attempts outside the initial budget decision. The request estimate therefore includes every permitted attempt:
public decimal MaximumEstimatedRequestUsd() =>
checked(MaximumEstimatedAttemptUsd() * MaxAttempts);
This approach can reject a request whose eventual successful response would have been cheaper. That is a consequence of reserving the allowed exposure before knowing how many attempts will happen.
The gateway makes its admission decision before entering the sending loop:
if (!ledger.TryReserve(authenticatedUserId, requestId,
policy.MaximumEstimatedRequestUsd()))
throw new BudgetExceededException();
The default ledger coordinates reservations with a lock. Available budget accounts for both recorded costs and held exposure, so concurrent callers cannot independently reserve the same balance. The HTTP endpoint translates insufficient budget into a 429 response.
The lock protects this process's ledger. It does not coordinate separate application instances, and restarting loses the in-memory records. The ledger implementation explicitly documents those limits. A deployment using multiple instances needs shared, durable, atomic reservations.
A failed response does not supply reliable token accounting. An interrupted connection can also leave the application without a usable answer about what happened remotely.
Immediately before sending, the gateway adds one attempt's estimated exposure to its uncertain total:
attempts++;
uncertain += attemptExposure;
using var response = await client.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead, token);
The retry loop bounds the attempt count, retries 429 and server errors, and applies policy delays within an overall timeout. Earlier attempts retain their uncertain exposure even when a later attempt succeeds. A successful retry cannot establish the earlier attempts' billing outcome.
This conservative treatment can hold more budget than the eventual provider charge. The sample preserves that uncertainty instead of assuming that every unsuccessful attempt was free.
When a response contains valid nonnegative token counts, the gateway calculates their cost using configured prices and removes that attempt's estimated uncertain exposure:
confirmed += policy.EstimateUsageUsd(inputTokens, outputTokens);
uncertain -= attemptExposure;
knownUsage = true;
Completion runs in a finally block:
completed = ledger.Complete(requestId, confirmed, uncertain, outcome);
The ledger releases the original reservation while retaining the calculated confirmed cost and any uncertain exposure. Consequently, unattempted retries release their reserved share. Known usage replaces its attempt estimate. A successful answer without usage keeps that attempt's estimated exposure held.
For example, suppose the configured estimate is $0.01 per attempt and three attempts are permitted. Admission reserves $0.03. If the first attempt succeeds with $0.002 of calculated usage, completion retains $0.002 and releases $0.028. These are illustrative figures, not OpenAI prices.
Here, confirmed means calculated from returned usage and configured prices. It does not mean reconciled with a provider invoice. The ledger records reported usage even when its calculated cost exceeds the admission estimate.
The mechanism controls admission against application accounting. Its effectiveness depends on configuration, supported request shapes, persistent accounting where required, and treatment of uncertain outcomes. It cannot promise an exact provider bill.
For commit f43b206, the sample builds on Windows with 0 warnings, the self-test passes 9 of 9, and a local HTTP check on 127.0.0.1 returned 401 without a token, 400 for malformed JSON or a user field, and 429 when the budget was exhausted, without calling the provider. Those checks were run with Claude, the AI assistant I use for this kind of work.
The sample was not tested against real OpenAI. Its self-tests use a scripted local provider. To run that path without an API key:
dotnet run --project LlmIntegration.csproj -- --self-test
The README explains real-provider environment configuration and the memory ledger's limits. Live compatibility, current pricing, and actual billing remain outside the reported validation.
If you want this in your ASP.NET Core app, I offer it as a fixed-price project on Upwork: https://www.upwork.com/services/product/2114839106032222506