I've been building mobile apps independently since 2014. When I first tried to integrate Claude API into one of my apps, the basic setup — set your API key, send a request — worked within minutes. But the moment I tried to go beyond that, I ran into a series of architectural decisions I hadn't anticipated.
Which model should I use? Should I call the API the instant a user taps a button, or prepare responses in the background? Where does conversation history live? What happens when the user's connection is poor? How do I make sure a viral moment doesn't send my API bill through the roof?
These are decisions you need to make before you write the main feature code. Get them wrong early, and you'll be rewriting entire layers of your app later. I've done exactly that — twice — across two different apps, so I'm writing this to help you avoid the same pain.
What follows is the set of patterns I actually settled on while shipping these features solo — including the ones I picked first and later regretted.
Model Selection — Haiku, Sonnet, or Opus?
The first decision is which model to use. From an indie developer's perspective, the framework is straightforward: balance latency, cost, and accuracy against your specific task.
Claude Haiku 4.5 is the fastest and cheapest. It's ideal for tasks where the correct answer is clear-cut — classification, labeling, simple filtering. In one of my wallpaper apps, I use Haiku to auto-tag images uploaded by users. The per-request cost is tiny, and even processing thousands of images daily keeps my monthly bill in the single-digit dollars.
Claude Sonnet 5 strikes the best balance for interactive features. If you're building chat interfaces, content generation, or any reasoning that involves moderate complexity, Sonnet is where most indie app features will land. This is the model I reach for most often.
Claude Opus 5 delivers the highest accuracy but comes with higher latency and cost. Using it as a default for a mobile app is usually impractical. Reserve it for high-value features that users access infrequently — a "deep analysis" mode, a monthly review summary, or a background task where latency doesn't matter.
As of August 2026, list pricing is $1/$5 per Mtok for Haiku 4.5, $2/$10 for Sonnet 5 (introductory, through August 31), and $10/$50 for Opus 5. That's a tenfold spread from top to bottom. On mobile, where call volume accumulates quietly in the background, "just use the smartest model" is a decision you pay for on a statement three months later.
The pattern I've found most effective is routing requests to the appropriate model based on task type:
# Server-side Python example
import anthropic
client = anthropic.Anthropic(api_key="YOUR_ANTHROPIC_API_KEY")
def select_model_for_task(task_type: str) -> str:
"""
Route to the appropriate model based on task complexity.
Cost optimization is critical for indie developers — use the
cheapest model that gets the job done.
"""
model_map = {
"classification": "claude-haiku-4-5-20251001", # Tagging, labeling
"chat": "claude-sonnet-5", # User conversations
"generation": "claude-sonnet-5", # Content creation
"analysis": "claude-opus-5", # Deep reasoning (infrequent)
}
return model_map.get(task_type, "claude-haiku-4-5-20251001") # Default to Haiku
def process_request(user_input: str, task_type: str) -> str:
model = select_model_for_task(task_type)
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": user_input}]
)
# Log model usage for cost analysis
print(f"Model: {model} | In: {response.usage.input_tokens} | Out: {response.usage.output_tokens}")
return response.content[0].textThe key is keeping model selection logic separate from your business logic. When a new Haiku version releases or you want to experiment with a different default, you change one function — not a hundred call sites.
API Call Timing — Protecting UX with Async Design
The most common mistake I see in early AI integrations is calling the API synchronously the moment a user taps a button.
Claude API responses typically take 1–5 seconds. Five seconds of an unresponsive UI is unacceptable by modern mobile standards. On iOS, blocking the main thread long enough will cause watchdog terminations — the system kills your app for being unresponsive.
I rely on three patterns depending on the use case:
Pattern 1: Streaming responses
The simplest fix for perceived latency. Start rendering text to the UI the moment the first tokens arrive, rather than waiting for the full response. The user sees the model "thinking" in real time, and the experience feels dramatically faster.
// iOS Swift — Streaming response with URLSession
import Foundation
class ClaudeStreamingClient {
private let apiKey = "YOUR_ANTHROPIC_API_KEY"
private let baseURL = "https://api.anthropic.com/v1/messages"
func streamResponse(
userMessage: String,
onChunk: @escaping (String) -> Void,
onComplete: @escaping () -> Void
) async {
var request = URLRequest(url: URL(string: baseURL)!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
let body: [String: Any] = [
"model": "claude-sonnet-5",
"max_tokens": 1024,
"stream": true,
"messages": [["role": "user", "content": userMessage]]
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let (asyncBytes, _) = try! await URLSession.shared.bytes(for: request)
for try await line in asyncBytes.lines {
guard line.hasPrefix("data: ") else { continue }
let jsonString = String(line.dropFirst(6))
guard jsonString != "[DONE]" else { break }
if let data = jsonString.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let delta = json["delta"] as? [String: Any],
let text = delta["text"] as? String {
await MainActor.run { onChunk(text) }
}
}
await MainActor.run { onComplete() }
}
}Pattern 2: Prefetching
When you can predict what a user will need next, fetch it before they ask. In a reading app, for example, you might pre-generate a summary of the next article while the user is still reading the current one. When they tap "next," the response is already waiting.
Pattern 3: Background jobs
For heavy processing that doesn't need to be immediate, schedule it while the app is in the background. iOS and Android both offer background task APIs. The user opens the app and the result is already there — it feels instant.
Context Management — Where Does Conversation History Live?
Building a chat-style AI feature means managing state that the Claude API doesn't maintain for you.
Claude API is stateless. Each request must include the full conversation history for the model to "remember" previous turns. The UI might feel conversational, but under the hood you're passing an ever-growing array of messages with every request.
The problem: longer conversations mean more tokens per request, which means higher cost and higher latency.
My approach is a sliding window with aggressive pruning:
// iOS Swift — Sliding window conversation manager
struct ConversationManager {
private var history: [(role: String, content: String)] = []
private let maxHistoryItems = 10 // Keep last 10 turns
mutating func addMessage(role: String, content: String) {
// Truncate very long messages before storing
let truncated = content.count > 2000
? String(content.prefix(2000)) + "..."
: content
history.append((role: role, content: truncated))
// Drop the oldest message when we hit the limit
if history.count > maxHistoryItems {
history.removeFirst()
}
}
func buildAPIMessages() -> [[String: String]] {
return history.map { ["role": $0.role, "content": $0.content] }
}
var estimatedTokenCount: Int {
history.reduce(0) { $0 + ($1.content.count / 4) }
}
}Beyond the code, you need a clear policy on where history is persisted:
- Device-only storage (UserDefaults, Core Data): High privacy, zero infrastructure, but lost on device reset or app deletion.
- Cloud sync: Enables cross-device continuity but requires a privacy policy update and backend infrastructure.
- Session-only: History is cleared when the app is closed. Simple and safe.
For most of my personal apps, I default to device-only storage and offer cloud sync as an opt-in feature. Users who care about privacy appreciate the default, and power users can opt in for continuity. Trust built here compounds over time into long-term retention.
Defensive Patterns for Unstable Networks
Mobile apps live in a world of tunnels, elevators, and crowded stadiums. Your API integration needs to handle network failures gracefully — not crash or hang.
I implement four layers of defense:
Layer 1: Timeouts
var request = URLRequest(url: url)
request.timeoutInterval = 30 // 30 seconds — don't leave users hangingLayer 2: Retry with exponential backoff
func sendWithRetry(request: URLRequest, maxRetries: Int = 3) async throws -> Data {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse {
if http.statusCode == 429 {
// Rate limited — wait and retry
let wait = Double(2 << attempt) // 2s, 4s, 8s
try await Task.sleep(nanoseconds: UInt64(wait * 1_000_000_000))
continue
}
if http.statusCode >= 500 {
lastError = AppError.serverError(http.statusCode)
try await Task.sleep(nanoseconds: UInt64(Double(2 << attempt) * 1_000_000_000))
continue
}
}
return data
} catch {
lastError = error
if attempt < maxRetries - 1 {
try await Task.sleep(nanoseconds: UInt64(Double(2 << attempt) * 1_000_000_000))
}
}
}
throw lastError ?? AppError.maxRetriesExceeded
}
enum AppError: Error {
case serverError(Int)
case maxRetriesExceeded
}Layer 3: Graceful offline fallback
When the API is unreachable, show a clear, friendly message rather than a blank screen or crash. Non-AI features of the app should continue to work normally. An AI feature that doesn't work in airplane mode is okay — an app that crashes in airplane mode is not.
Layer 4: Response caching
For features where the response doesn't change between requests — FAQs, static content summaries, reference lookups — cache the last successful response locally. Serve it while offline, and refresh in the background when connectivity returns.
Cost Control — Building a Safety Net for Viral Moments
API cost management is existential for indie developers. If your app goes viral and 100,000 users hit your AI feature the same day, an uncontrolled API integration could generate a bill that exceeds your lifetime app revenue.
I use two layers of control:
Layer 1: Anthropic dashboard spending limits
Set a hard monthly cap in your Anthropic console. This is your backstop — the absolute ceiling you cannot exceed. Set it conservatively at first.
Layer 2: Per-user rate limiting in your application
Dashboard limits protect you globally but don't stop a single heavy user from exhausting your budget on their own.
# Python server-side — Per-user rate limiter
import time
from collections import defaultdict
class UserRateLimiter:
def __init__(self):
self.request_history = defaultdict(list)
self.hourly_limit = 20
self.daily_limit = 100
def is_allowed(self, user_id: str) -> tuple[bool, str]:
now = time.time()
history = self.request_history[user_id]
# Clean up old entries
history = [ts for ts in history if now - ts < 86400]
if len(history) >= self.daily_limit:
return False, "Daily limit reached. Try again tomorrow."
recent = [ts for ts in history if now - ts < 3600]
if len(recent) >= self.hourly_limit:
return False, "Hourly limit reached. Please wait a bit."
history.append(now)
self.request_history[user_id] = history
return True, ""
limiter = UserRateLimiter()My current tiering for personal apps: free users get 5 AI requests per day; premium subscribers get 50. This structure does two things — it keeps costs predictable, and it creates a natural upgrade path.
Prompt Caching — Cutting Costs by Half
If all your users share a common system prompt (a persona, instructions, or a large knowledge base), enable prompt caching. After the first request, Anthropic caches the prompt and charges roughly 10% of the normal input token price for subsequent requests that hit the cache.
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": """You are a knowledgeable travel guide specializing in Japanese destinations.
Provide specific, practical travel advice. Your goal is to help users plan
safe and memorable trips.""",
"cache_control": {"type": "ephemeral"} # Enable caching
}
],
messages=[{"role": "user", "content": user_message}]
)For apps with high-traffic AI features, prompt caching can reduce your API bill by 40–60%. The system prompt needs to be at least 1,024 tokens to qualify, but for most real-world prompts, that threshold is easy to hit once you include proper context and instructions.
Recalculating Unit Economics for the September 1 Price Change
One assumption behind this article shifted after I first wrote it. Sonnet 5's introductory pricing of $2/$10 per Mtok ends on August 31, 2026, and standard pricing of $3/$15 per Mtok takes effect on September 1. Input and output both go up by 1.5x.
Mobile API costs are small per call and large in aggregate. When the unit price rises 1.5x, your monthly statement follows almost exactly. I ran the numbers using the same assumption I've used throughout this article: 1,000 input tokens and 500 output tokens per request.
| Usage | Model and rate | Cost per user / month | Approx. JPY ($1 = ¥150) |
|---|---|---|---|
| 20/day (600/month) | Sonnet 5 introductory $2/$10 | $4.20 | ~¥630 |
| 20/day (600/month) | Sonnet 5 standard $3/$15 | $6.30 | ~¥945 |
| 20/day (600/month) | Haiku 4.5 $1/$5 | $2.10 | ~¥315 |
| 50/day (1,500/month) | Sonnet 5 introductory $2/$10 | $10.50 | ~¥1,575 |
| 50/day (1,500/month) | Sonnet 5 standard $3/$15 | $15.75 | ~¥2,362 |
| 50/day (1,500/month) | Haiku 4.5 $1/$5 | $5.25 | ~¥788 |
The bolded row is the one that stopped me. The "50 requests per day for premium users" cap described earlier in this article costs $15.75 — roughly ¥2,362 — per month for a single subscriber who actually reaches it. Against a ¥1,000 monthly plan, that subscriber loses money no matter how you frame it.
Introductory pricing hid this. At $10.50 you can still tell yourself that heavy users are rare enough to average out. At $15.75 that story stops working.
Three levers are available.
1. Lower the cap. Dropping from 50/day to 20/day brings standard pricing down to $6.30 (~¥945). As a code change it's a single constant. The resistance is psychological, not technical.
2. Drop the model. The same 50 calls per day on Haiku 4.5 ($1/$5) come to $5.25, about ¥788. For summarization, classification, and tagging — tasks with a well-defined correct answer — users often can't feel the accuracy difference. For features where conversational quality is the product, you can't make this trade. Decide per feature, not per app.
3. Lean on caching. With a 1,200-token system prompt, my calculation shows 1,500 monthly calls dropping from $21.15 to $16.29 when the cache hits — a 23.0% reduction. Caching scales with system prompt length, but it does nothing for user input or generated output. It won't absorb a 1.5x unit price increase on its own.
I went with a combination of the first two: cap at 20 per day, and route summarization tasks to Haiku 4.5. Caching stays on, but I no longer treat it as the answer.
The transferable part isn't the specific figures. It's the habit of recomputing worst-case cost per user every time a rate changes. Design around averages and you'll learn about a price change from your invoice.
Unexpected Problems from Production
Here's what actually surprised me after shipping:
Eager users exceed rate limits faster than expected. A small percentage of users will push every feature to its limits. Make sure your rate limit error messages are localized and user-friendly — don't surface raw API error strings.
Long responses hurt mobile UX. Claude defaults to thorough answers. On a small screen, verbosity is a liability. Tune max_tokens and add explicit instructions in your system prompt: "Answer concisely. Limit responses to 3–5 sentences unless the user explicitly asks for more detail."
Response inconsistency surprises users. The same question can return meaningfully different answers on different days. If your app positions Claude as a reference source, users may notice. Lowering temperature improves consistency but reduces creativity — calibrate based on your use case.
Model deprecations happen — and so do silent default changes. I was caught off-guard when a model I depended on was deprecated. Keep model identifiers as constants defined in one place, preferably as environment variables. A model swap should be a one-line config change.
The same discipline covers a subtler case. On August 7, 2026, the default Opus model in Claude Code shifted to Opus 5. Any setup that leaves the model unspecified inherits new response characteristics and new pricing without a single line changing on your side. In an app, never rely on a default: pin the model string explicitly, and keep the swap to one config edit. Pinning and moving fast are not in conflict.
Sustainable Monetization — Combining Claude API with AdMob
For apps with an existing AdMob monetization base, adding Claude API features requires thinking carefully about how costs and revenue interact.
My hybrid model:
- Free tier: Ads enabled, 5 AI requests per day
- Premium tier (monthly subscription): Ads removed, 50 AI requests per day
The goal is ensuring that premium subscription revenue exceeds the API cost each premium user generates. Here's the back-of-envelope math I use before launching any AI feature:
Price against the cap, not the average. The rule I use is: monthly subscription price > monthly API cost for a user who hits the daily limit every single day.
Per the table above, a 20/day cap lands at roughly ¥945 per user even at standard pricing, so a ¥1,000 plan barely holds. A 50/day cap lands at roughly ¥2,362, and the same plan doesn't hold at all. Averages let heavy users tip you into loss quietly. Designing against the cap at least guarantees that each additional subscriber isn't a growing liability.
If you skip this math, you can end up in a situation where user growth makes you less profitable. I've seen indie developers celebrate virality and then panic when they realized their cost structure meant every new active user was a net loss.
AI features are powerful, but only sustainable if the economics are deliberately designed from the start.
Where to Go From Here
None of these patterns is universally correct. The right choices depend on your specific app, your users, and how you make money. What I've described is what's worked across my portfolio — not a prescription.
The most important thing is to start small. Pick one feature, integrate it with Claude API, ship it to real users, and learn from how they actually use it. Adjust from there. Building indie apps since 2014 has taught me that developers who iterate quickly on real feedback consistently outperform those who try to design the perfect system upfront.
One task does come with a deadline attached. Before September 1, put your own cap settings through the arithmetic above and see what a single limit-hitting subscriber actually costs you. The formula is trivial. Even so, I didn't grasp what leaving the cap at 50 per day really meant until I laid the numbers out side by side.