{"slug": "how-i-cap-openai-spending-per-user-in-asp-net-core", "title": "How I cap OpenAI spending per user in ASP.NET Core", "summary": "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.", "body_md": "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.\n\nMy [ASP.NET Core sample](https://github.com/martinPisacreta/openai-spend-caps-dotnet/tree/f43b206) 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.\n\nThe 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.\n\nThe request contract is deliberately small:\n\n```\npublic sealed record ChatRequest(string Prompt);\n```\n\nAfter authentication, the endpoint passes the resolved identity separately:\n\n``` js\nvar reply = await gateway.SendAsync(\n    userId, request.Prompt, context.RequestAborted);\n```\n\nThe 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.\n\nThe policy calculates exposure from the maximum prompt size, an input overhead allowance, maximum output tokens, and explicitly configured prices:\n\n```\nreturn checked(\n    ((MaxPromptUtf8Bytes + InputOverheadTokenAllowance) *\n        InputUsdPerMillionTokens +\n     MaxOutputTokens * OutputUsdPerMillionTokens) / 1000000m);\n```\n\nThe 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.\n\nRetries 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:\n\n``` js\npublic decimal MaximumEstimatedRequestUsd() =>\n    checked(MaximumEstimatedAttemptUsd() * MaxAttempts);\n```\n\nThis 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.\n\nThe gateway makes its admission decision before entering the sending loop:\n\n```\nif (!ledger.TryReserve(authenticatedUserId, requestId,\n        policy.MaximumEstimatedRequestUsd()))\n    throw new BudgetExceededException();\n```\n\nThe 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.\n\nThe lock protects this process's ledger. It does not coordinate separate application instances, and restarting loses the in-memory records. The [ledger implementation](https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/SpendingLedger.cs) explicitly documents those limits. A deployment using multiple instances needs shared, durable, atomic reservations.\n\nA failed response does not supply reliable token accounting. An interrupted connection can also leave the application without a usable answer about what happened remotely.\n\nImmediately before sending, the gateway adds one attempt's estimated exposure to its uncertain total:\n\n``` js\nattempts++;\nuncertain += attemptExposure;\nusing var response = await client.SendAsync(\n    request, HttpCompletionOption.ResponseHeadersRead, token);\n```\n\nThe 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.\n\nThis 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.\n\nWhen a response contains valid nonnegative token counts, the gateway calculates their cost using configured prices and removes that attempt's estimated uncertain exposure:\n\n```\nconfirmed += policy.EstimateUsageUsd(inputTokens, outputTokens);\nuncertain -= attemptExposure;\nknownUsage = true;\n```\n\nCompletion runs in a `finally` block:\n\n```\ncompleted = ledger.Complete(requestId, confirmed, uncertain, outcome);\n```\n\nThe 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.\n\nFor 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.\n\nHere, 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.\n\nThe 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.\n\nFor 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.\n\nThe sample was **not tested against real OpenAI**. Its [self-tests](https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/SelfTests.cs) use a scripted local provider. To run that path without an API key:\n\n```\ndotnet run --project LlmIntegration.csproj -- --self-test\n```\n\nThe [README](https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/README.md) explains real-provider environment configuration and the memory ledger's limits. Live compatibility, current pricing, and actual billing remain outside the reported validation.\n\nIf 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](https://www.upwork.com/services/product/2114839106032222506)", "url": "https://wpnews.pro/news/how-i-cap-openai-spending-per-user-in-asp-net-core", "canonical_source": "https://dev.to/martinpisacreta/how-i-cap-openai-spending-per-user-in-aspnet-core-27bb", "published_at": "2026-11-02 04:39:22+00:00", "updated_at": "2026-11-02 04:52:23.016381+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-infrastructure", "mlops"], "entities": ["OpenAI", "ASP.NET Core", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-i-cap-openai-spending-per-user-in-asp-net-core", "markdown": "https://wpnews.pro/news/how-i-cap-openai-spending-per-user-in-asp-net-core.md", "text": "https://wpnews.pro/news/how-i-cap-openai-spending-per-user-in-asp-net-core.txt", "jsonld": "https://wpnews.pro/news/how-i-cap-openai-spending-per-user-in-asp-net-core.jsonld"}}