cd /news/ai-tools/how-i-cap-openai-spending-per-user-i… · home › topics › ai-tools › article
[ARTICLE · art-162275] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

How I cap OpenAI spending per user in ASP.NET Core

A developer published an ASP.NET Core sample (commit f43b206) that enforces per-user OpenAI spending caps by reserving estimated exposure before each request rather than recording token usage after the fact. The gateway authenticates users via configured bearer-token hashes, computes a maximum estimated request cost from prompt size, output limits, retry attempts and configured prices, and rejects requests with a 429 when the in-memory ledger cannot reserve the balance. The author notes the lock-based ledger does not coordinate across multiple application instances and that reservations are lost on restart, so multi-instance deployments need shared, durable, atomic reservations.

by read4 min views2 publishedNov 2, 2026

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

── more in #ai-tools 4 stories · sorted by recency
── more on @openai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-cap-openai-spe…] indexed:0 read:4min 2026-11-02 · —