Keep a 50% Margin on an AI Subscription: Per-User Cost Caps That Actually Bind
If you sell a monthly plan that talks to large language models, the user who loves the product is the one who can wipe out your margin. A “fair use” sentence in the terms does not stop a model bill. You need a ceiling in
If you sell a monthly plan that talks to large language models, the user who loves the product is the one who can wipe out your margin. A “fair use” sentence in the terms does not stop a model bill. You need a ceiling in code, denominated in the same dollars the provider charges you, and you need that ceiling to be a hard stop — not a polite downgrade that keeps spending.
I built this for Plenifa, a bilingual AI life coach and language tutor. This is a write-up of the metering design in the running app, not a pitch for the product.
Two numbers per request, not one
Every metered action writes a row with two dollar amounts:
- Real cost — what the model provider billed, in USD.
- Charged amount — real cost multiplied by a tier multiplier, which is what draws down the user’s plan allowance.
Cheap “economy” models are charged at 1×. Mid-range “standard” models are 1.2×, “premium” 1.5×, and “frontier” 2×. The multiplier is a product choice: a user who picks a costly model burns the visible allowance faster, while the hard cap (described below) still counts real dollars. Fallback replies are logged so they can be counted, but they do not consume the allowance the way a normal plan message does. Optional top-up credit is a separate balance the user has already paid for, and it is not counted toward the monthly spend cap.
If you only track tokens, or only track “messages,” a vision request or a long spoken reply will not look like what it costs.
Design the cap from fees, not from a round number
The rule in our config is simple enough to test, and it is unit-tested.
Take the plan price. Subtract a worst-case payment fee — the worst of PayPal (3.49% + $0.49), a conservative dLocal Go card fee (3.99% + $0.30, plus tax on the fee), and a Stripe international case (2.9% + 0.7% Billing + 1.5% international card + 1% FX + $0.30). Then subtract 50% of the list price. Then divide by 1.055 to leave room for the model router’s 5.5% credit-purchase fee. Round down to the cent. That number is monthlySpendCapUsd: a hard ceiling on real provider USD for the billing period.
The user’s visible allowance (monthlyBudgetUsd) is 90% of that hard cap. Weekly and daily windows are scaled in proportion. When the allowance is gone but the hard cap is not, we are in a soft-cap band: plain text can continue on a cheap economy fallback; photos, files, image generation, and HD voice stop at 100% of the allowance and offer a top-up or upgrade.
Paid plans and the hard caps currently look like this (USD, monthly):
| Plan | Price | Allowance | Hard cap (real $) | Daily hard stop | Weekly window |
|---|---|---|---|---|---|
| Free | 0 | 0.27 | 0.30 | 0.10 | 0.12 |
| Starter | 6.99 | 2.34 | 2.61 | 0.39 | 0.69 |
| Plus | 19.99 | 7.22 | 8.03 | 1.25 | 2.15 |
| Max | 39.99 | 14.71 | 16.35 | 2.55 | 4.39 |
| Ultra | 69.99 | 25.94 | 28.83 | 4.53 | 7.77 |
| Power | 100 | 37.18 | 41.32 | 6.49 | 11.15 |
Every feature is on every plan, including Free. Plans differ by how much you can use, not by locking tools behind a paywall. Free also has a 30-message daily cap as an abuse guard.
Annual billing charges 10 months (two months free). Every dollar limit on an annual plan is scaled by 10/12, because the payment fee is taken once on the annual amount. The same 50% floor still holds.
Top-up packs follow the same idea. A $5 / $10 / $25 pack currently adds $1.73 / $3.87 / $10.11 of credit, after the same worst-case fee, the 50% hold-back, and the 5.5% credit fee, rounded down. Older in-flight checkouts keep their original credit; balances already bought are not rewritten.
Enforcement order matters more than the config file
A cap checked only at month-end is a post-mortem. The chat path checks, in order:
- Daily hard stop — real dollars today, from every source, including voice. If it is reached, the API returns 429 and the user waits until midnight (the daily window resets at local midnight on the server).
- Free-plan message caps — per account, and also per IP and device so a pile of free accounts on one phone does not share an unlimited pool.
-
The monthly real-dollar spend cap. On these plans the policy is
block. If the cap is hit and the user has no top-up credit, the API returns 402 with the existing top-up packs and an upgrade prompt. There is no “keep answering on a fallback model” path around that block. - The weekly and monthly allowances (charged dollars). If those are gone, top-up credit can still pay; otherwise media stops, and text may use a limited number of cheap fallback messages, up to a per-plan daily count.
We also flag anomalies (bursts, repeated prompts, a day far above the plan’s daily average). Those flags are for review; they do not, by themselves, cut the user off. The dollar checks do.
The leaks we had to close
This was not the first version of the design.
An earlier paid-plan policy was onSpendCap: "degrade": after the monthly spend cap, chat kept running on a fallback model up to a daily fallback-message count. That sounds gentle. It is not a cost ceiling. A vision fallback is cheap per token but not free, and over a 31-day period the only thing that actually stopped spending was the daily hard stop. On every plan we modeled, a user who maxed that path could spend more than the subscription.
HD voice had a second leak: it was capped in characters, not dollars. A “500,000 characters a month” cap is a very different bill if one voice is $15 per million characters and another is $100. Listen usage is now counted in cost-weighted characters against a reference voice, so a more expensive voice burns the allowance faster. Max’s monthly HD allowance was cut from 500k to 250k of those weighted characters (Free is 5,000). Cache hits for the same text, voice, and speed cost $0 and do not count.
We also dropped a rolling five-hour window. Daily, weekly, monthly, and the hard cap were already doing the work; another window made the meter harder to explain.
None of that is visible to a typical light user. It only shows up when someone tries to run the allowance all the way to the wall.
What this does not pretend to do
This design guarantees a floor on margin if someone uses every leftover dollar the caps allow, after a pessimistic fee. It does not tell you what a typical user will cost. We did not use production medians to pick these numbers; the 50% figure is a worst-case constraint, not an observed average. A pre-flight estimate can undershoot the last message in a window, so that request can go slightly past it. The hard cap still sits below half the list price after fees.
Hosting, support time, and everything that is not a per-request model bill sit outside this meter. The meter is for the part that scales linearly with a power user.
If you are wiring something similar, the habits that mattered were: store real cost and charged cost on every event; put voice and images on the same ledger as chat; make “over cap” a 402, not a quieter model; and put the formula in config so a price change cannot drift from the fee math without a failing test.
Closed testing
Plenifa’s Android app (com.plenifa.app) is in Google Play closed testing. It is not a public Play Store listing. If you have an Android phone and want to help: join the plenifa-testers Google Group, then opt in at https://play.google.com/apps/testing/com.plenifa.app. Please install the app, open it, and actually use it (chat, voice, English/Spanish) over 14 days. Feedback through the in-app button is welcome. You can also use the product in the browser at plenifa.com and install it as a PWA.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.