Adding an AI feature to an app is now a few lines of code. The hard part comes after launch: a cost that grows with every click, from users you’ve never met, on a feature you’d like people to use a lot.
These are the patterns we use in our own apps, such as Braineos, which drafts flashcards from your notes, fact-checks decks and suggests tags.
Keep the key on your server
The app never talks to the AI provider directly. Phones and browsers call our API, and our server calls the model. An API key shipped inside a mobile app can be extracted, and then anyone can spend your money.
Routing everything through your server has a second benefit: it’s the one place where you can count, limit and log usage.
Not all calls cost the same
Generating a whole deck of cards costs roughly five to ten times as much as suggesting three tags. So the quotas have two tiers: a small monthly allowance for heavy “generation” calls, and a larger one for lightweight helpers. A single number would either be too tight for the cheap features or too loose for the expensive ones.
Some details that matter more than they look:
- Check and consume the quota atomically. If two requests land at once, they must not both see “one left” and both go ahead.
- Reset on a simple calendar. We reset at the start of each month (UTC). It’s simpler than following every subscriber’s billing date, and no one has complained.
- Say what happened. A clear “you’ve reached this month’s limit; it resets on the 1st” saves a support email.
Use a cheap, fast model for small jobs
Not every task needs the most capable model. On the Braineos blog, every post gets its photo the same way. A small, fast model reads the title and summary and writes a short photo search query. The query goes to a stock photo service, and the top result is saved with the photographer’s credit. This blog finds its photos the same way.
The job costs a fraction of a cent and takes about a second. The expensive models are kept for the work users actually notice.
Let it fail quietly
AI services time out, hit rate limits, and sometimes return nonsense. Decide for each feature what “no answer” looks like, and make it ordinary:
- No photo found? The post publishes without one, and the next run tries again.
- Tag suggestions unavailable? The form still works; you type your own.
- Generation failed? Say so plainly, and let the user try again.
What we don’t swallow is a failure on our side, like our own storage being down. That’s an outage to fix, not a flaky third party to tolerate, and hiding it would only delay finding out.
Don’t trust the shape of the answer
Even when you ask for a specific format, models sometimes return something slightly different. Validate every response before using it, and treat “unparseable” as just another way of returning no answer.
Gate the expensive parts
In our apps, the AI features are part of the paid plan, and the quota still applies within it. That way usage is tied to revenue, and even the heaviest subscriber has a known ceiling.
None of this is exotic. It’s the same discipline as for any metered resource: know what each call costs, put a ceiling on it, and decide in advance what happens when it isn’t there.
