{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"capping-what-one-user-can-cost","title":"Capping what one user can cost you","url":"https://addaly.com/learn/building-apps-with-ai/capping-what-one-user-can-cost","position":63,"of":87,"minutes":8,"answer":"Cost is tokens in plus tokens out, per call, per user, per day, and it needs three caps in three places: a hard limit at the provider, a per-user count in your own table.","keyIdea":"Cost is tokens in plus tokens out, per call, per user, per day, and it needs three caps in three places: a hard limit at the provider, a per-user count in your own table, and a maximum length on every request."},"body":{"format":"markdown","text":"## The arithmetic\n\nProviders charge per token, separately for tokens in and tokens out, and output is several times the price of input. The exact figures are on each provider's pricing page and change often enough that this lesson will not quote them; the shape is what to learn.\n\nA summarise call sends a 2,000-token document plus a 200-token prompt and gets 300 tokens back. Look up your model's two prices per million tokens, and the call costs `2,200 × input price + 300 × output price`, divided by a million. For a small hosted model that is a fraction of a cent; for a frontier model it is a few cents. Neither matters at ten calls a day. At ten thousand, the difference between the two models is a phone bill and a rent payment.\n\n`data.usage` in every response reports the actual counts. Log them, sum them per user per day, and the arithmetic becomes a report rather than a guess.\n\n## Three caps, in three places\n\n**The provider's hard limit.** Every dashboard has a monthly spending cap or a prepaid balance. Set it before the key exists. This is the one cap that holds when every line of your code is wrong, because it is enforced by the people sending the bill. Set it to a number you would be annoyed to lose and not devastated.\n\n**A per-user allowance in your own table.** A row per user per day with a count, checked before the call:\n\n> Figure: Three caps, in three different places. Layers, top to bottom: A hard spending cap at the provider (set before the key exists; an alert is not a cap); A per-user allowance in your own table (claimed atomically before the call, refunded on failure); A maximum on every request (max_tokens for the output, a character limit for the input). Each fails on its own. The first holds when every line of your code is wrong, because it is enforced by the people sending the bill.\n\n```typescript\nconst used = await claimUsage(session.userId, today);   // atomic increment, returns new count\nif (used > DAILY_LIMIT) return new Response(\"Daily limit reached\", { status: 429 });\ntry {\n  return await callModel(...);\n} catch (e) {\n  await refundUsage(session.userId, today);              // the call never happened\n  throw e;\n}\n```\n\nThe order matters and is the point of the question above. Increment first, atomically, in one statement: `UPDATE ... SET count = count + 1 RETURNING count`, and refund on failure. A counter incremented after the response is a counter that a burst of parallel requests passes before it has moved. Twenty checks read zero at the same instant, twenty calls go out, and the script that sent them sends nine hundred more.\n\n**A maximum on every request.** `max_tokens` on the call caps the output. A cap on the input length (characters, before the call) caps the other half. A user pasting a 200-page document into a summariser is either a mistake or a test of your limits, and both should get a polite refusal rather than a $4 call.\n\n## Who is doing the calling\n\nYour route is reachable by anything that has a session cookie, including a script written in five minutes. The per-user cap handles the signed-in abuser. Two more layers handle the rest: a per-IP rate limit on the route (a few requests a minute is generous for a person and hopeless for a script) and the sign-up flood defence from the security block, because a thousand accounts at twenty calls each is twenty thousand calls.\n\nOn the free tiers, the provider's own per-minute limit acts as a crude cap: the route returns 429 when the tier is exhausted and the cost is zero. That is a feature of the free tier and it goes away the day you add a card.\n\n## The failure that costs the most\n\nA retry loop with no upper bound. A route that, on a 429 or a timeout, tries again immediately, and again, and again, generates thousands of calls from one user's single click and stops only when the function is killed. The resilience lesson next sets the shape: a small fixed number of retries with growing delays, and then an honest failure. Search the generated code for `while` near `fetch` and read what ends the loop.\n\n## Say the number\n\nPut the limit in the interface. \"You have used 14 of 20 today\" is honest, sets expectations, and turns the 429 from a mystery into a feature. It also means the day you raise the limit, users notice. A cap nobody can see reads as a bug.\n\n## What this looks like at the end of the month\n\nTotal tokens by feature, by day. Calls per user, sorted. The top user should be a person, and you should be able to name them. If the top user is an account with no other activity that made three thousand calls at 4am, the caps above were not all in place, and the arithmetic at the top of this lesson tells you what it cost to learn that."},"check":{"question":"A free summarise feature has a per-user limit of 20 calls a day, implemented by incrementing a counter after the model responds. One user's row shows 20 but the provider bill shows 900 calls from that user overnight. How?","options":[{"text":"The provider is double-counting calls in its dashboard, which is a billing error to raise with their support team along with your own counter as evidence of the true number.","correct":false,"tempting":"Attributes the gap to the wrong side; the app's own counter is what failed to count, and the provider saw every request it was sent."},{"text":"The user found a second account and used both, so the counts are spread across rows.","correct":false,"tempting":"Two accounts give forty calls, not nine hundred; the gap is far larger than any number of manual sign-ups."},{"text":"The counter is incremented after the response, so a script firing hundreds of requests at once passes the check before any of them has been counted.","correct":true},{"text":"The counter resets at midnight in the server's time zone, which let the user get a second allowance.","correct":false,"tempting":"A reset gives one extra allowance of twenty; it cannot produce nine hundred."}]},"prev":{"slug":"structured-answers-you-can-render","title":"Structured answers you can render","url":"https://addaly.com/learn/building-apps-with-ai/structured-answers-you-can-render"},"next":{"slug":"when-the-model-is-down-or-slow","title":"When the model is down, slow or empty","url":"https://addaly.com/learn/building-apps-with-ai/when-the-model-is-down-or-slow"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Capping what one user can cost you', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/capping-what-one-user-can-cost (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Capping what one user can cost you","courseTitle":"Building Apps with AI","module":{"index":7,"slug":"a-model-inside-your-app","title":"Putting a model inside your own app"},"answer":null,"keyIdea":"Cost is tokens in plus tokens out, per call, per user, per day, and it needs three caps in three places: a hard limit at the provider, a per-user count in your own table, and a maximum length on every request.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"stack","title":"Three caps, in three different places","layers":[{"label":"A hard spending cap at the provider","detail":"set before the key exists; an alert is not a cap"},{"label":"A per-user allowance in your own table","detail":"claimed atomically before the call, refunded on failure"},{"label":"A maximum on every request","detail":"max_tokens for the output, a character limit for the input"}],"caption":"Each fails on its own. The first holds when every line of your code is wrong, because it is enforced by the people sending the bill."},"alt":"Figure: Three caps, in three different places. Layers, top to bottom: A hard spending cap at the provider (set before the key exists; an alert is not a cap); A per-user allowance in your own table (claimed atomically before the call, refunded on failure); A maximum on every request (max_tokens for the output, a character limit for the input). Each fails on its own. The first holds when every line of your code is wrong, because it is enforced by the people sending the bill."}},"blocks":[{"t":"h2","id":"the-arithmetic","runs":[{"text":"The arithmetic"}]},{"t":"p","runs":[{"text":"Providers charge per token, separately for tokens in and tokens out, and output is several times the price of input. The exact figures are on each provider’s pricing page and change often enough that this lesson will not quote them; the shape is what to learn."}]},{"t":"p","runs":[{"text":"A summarise call sends a 2,000-token document plus a 200-token prompt and gets 300 tokens back. Look up your model’s two prices per million tokens, and the call costs "},{"text":"2,200 × input price + 300 × output price","code":true},{"text":", divided by a million. For a small hosted model that is a fraction of a cent; for a frontier model it is a few cents. Neither matters at ten calls a day. At ten thousand, the difference between the two models is a phone bill and a rent payment."}]},{"t":"p","runs":[{"text":"data.usage","code":true},{"text":" in every response reports the actual counts. Log them, sum them per user per day, and the arithmetic becomes a report rather than a guess."}]},{"t":"h2","id":"three-caps-in-three-places","runs":[{"text":"Three caps, in three places"}]},{"t":"p","runs":[{"text":"The provider’s hard limit.","b":true},{"text":" Every dashboard has a monthly spending cap or a prepaid balance. Set it before the key exists. This is the one cap that holds when every line of your code is wrong, because it is enforced by the people sending the bill. Set it to a number you would be annoyed to lose and not devastated."}]},{"t":"p","runs":[{"text":"A per-user allowance in your own table.","b":true},{"text":" A row per user per day with a count, checked before the call:"}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"const used = await claimUsage(session.userId, today);   // atomic increment, returns new count\nif (used > DAILY_LIMIT) return new Response(\"Daily limit reached\", { status: 429 });\ntry {\n  return await callModel(...);\n} catch (e) {\n  await refundUsage(session.userId, today);              // the call never happened\n  throw e;\n}"},{"t":"p","runs":[{"text":"The order matters and is the point of the question above. Increment first, atomically, in one statement: "},{"text":"UPDATE ... SET count = count + 1 RETURNING count","code":true},{"text":", and refund on failure. A counter incremented after the response is a counter that a burst of parallel requests passes before it has moved. Twenty checks read zero at the same instant, twenty calls go out, and the script that sent them sends nine hundred more."}]},{"t":"p","runs":[{"text":"A maximum on every request.","b":true},{"text":" "},{"text":"max_tokens","code":true},{"text":" on the call caps the output. A cap on the input length (characters, before the call) caps the other half. A user pasting a 200-page document into a summariser is either a mistake or a test of your limits, and both should get a polite refusal rather than a $4 call."}]},{"t":"h2","id":"who-is-doing-the-calling","runs":[{"text":"Who is doing the calling"}]},{"t":"p","runs":[{"text":"Your route is reachable by anything that has a session cookie, including a script written in five minutes. The per-user cap handles the signed-in abuser. Two more layers handle the rest: a per-IP rate limit on the route (a few requests a minute is generous for a person and hopeless for a script) and the sign-up flood defence from the security block, because a thousand accounts at twenty calls each is twenty thousand calls."}]},{"t":"p","runs":[{"text":"On the free tiers, the provider’s own per-minute limit acts as a crude cap: the route returns 429 when the tier is exhausted and the cost is zero. That is a feature of the free tier and it goes away the day you add a card."}]},{"t":"h2","id":"the-failure-that-costs-the-most","runs":[{"text":"The failure that costs the most"}]},{"t":"p","runs":[{"text":"A retry loop with no upper bound. A route that, on a 429 or a timeout, tries again immediately, and again, and again, generates thousands of calls from one user’s single click and stops only when the function is killed. The resilience lesson next sets the shape: a small fixed number of retries with growing delays, and then an honest failure. Search the generated code for "},{"text":"while","code":true},{"text":" near "},{"text":"fetch","code":true},{"text":" and read what ends the loop."}]},{"t":"h2","id":"say-the-number","runs":[{"text":"Say the number"}]},{"t":"p","runs":[{"text":"Put the limit in the interface. “You have used 14 of 20 today” is honest, sets expectations, and turns the 429 from a mystery into a feature. It also means the day you raise the limit, users notice. A cap nobody can see reads as a bug."}]},{"t":"h2","id":"what-this-looks-like-at-the-end-of-the-month","runs":[{"text":"What this looks like at the end of the month"}]},{"t":"p","runs":[{"text":"Total tokens by feature, by day. Calls per user, sorted. The top user should be a person, and you should be able to name them. If the top user is an account with no other activity that made three thousand calls at 4am, the caps above were not all in place, and the arithmetic at the top of this lesson tells you what it cost to learn that."}]}],"check":{"question":[{"text":"A free summarise feature has a per-user limit of 20 calls a day, implemented by incrementing a counter after the model responds. One user’s row shows 20 but the provider bill shows 900 calls from that user overnight. How?"}],"options":[{"text":[{"text":"The provider is double-counting calls in its dashboard, which is a billing error to raise with their support team along with your own counter as evidence of the true number."}],"correct":false,"tempting":[{"text":"Attributes the gap to the wrong side; the app’s own counter is what failed to count, and the provider saw every request it was sent."}]},{"text":[{"text":"The user found a second account and used both, so the counts are spread across rows."}],"correct":false,"tempting":[{"text":"Two accounts give forty calls, not nine hundred; the gap is far larger than any number of manual sign-ups."}]},{"text":[{"text":"The counter is incremented after the response, so a script firing hundreds of requests at once passes the check before any of them has been counted."}],"correct":true,"tempting":null},{"text":[{"text":"The counter resets at midnight in the server’s time zone, which let the user get a second allowance."}],"correct":false,"tempting":[{"text":"A reset gives one extra allowance of twenty; it cannot produce nine hundred."}]}],"order":[0,2,3,1],"why":null},"prev":{"slug":"structured-answers-you-can-render","title":"Structured answers you can render","minutes":8},"next":{"slug":"when-the-model-is-down-or-slow","title":"When the model is down, slow or empty","minutes":7}}}