
Cheerful — AI Gift Recommendation for iOS
Role: Co-founder and Principal Application Developer, NeuNet
Timeline: Nov 2023 – Aug 2025
Stack: Swift, SwiftUI, Node.js, Express, TypeScript, MongoDB, Redis, AWS, Firebase, OpenAI API

Summary
Cheerful turned five inputs — recipient’s name, age and gender, interests, budget, and occasion date — into twelve specific, purchasable gift suggestions. No account, no signup friction. At peak it served more than 50,000 users in the US on total infrastructure of under $400/month.
The interesting engineering was not the recommendation itself. It was making a generative system produce reliably structured, budget-accurate, non-hallucinated output, fast enough to hold a user’s attention, cheap enough to serve anonymously at consumer scale, and stable through national TV coverage and holiday traffic spikes.
Context and constraints
Three product decisions set the engineering constraints:
No registration. Users opened the app and got results immediately. Good for conversion, hostile for engineering: every anonymous user could trigger an expensive generative call with no account to attribute it to, no user ID to rate-limit against, and nowhere server-side to persist their lists.

Twelve results, not one. A single suggestion is a coin flip. Twelve gives the user something to browse and hides individual misses — but it multiplies the cost of every request and the surface area for malformed or repetitive output.

A hard budget ceiling. The user picked a spending limit, so suggestions had to correspond to things that actually exist at roughly that price. This is the constraint that makes gift recommendation harder than it looks.
Problem 1: Reliable structured output
Each suggestion needed a product name, description, price, category, image, and a link to the retail listing. All of it exists in the Amazon Gateway API response; the work was getting it into a shape the client could consume every time. In a mobile client a malformed response isn’t a logged warning — it’s a blank screen after a user waited.
Early on this was parsing, exception handling, and retries, which ran an 8% error rate and cost real money in redundant API calls. As providers shipped schema enforcement and strict mode, I moved validation upstream. Value-level errors remained, but malformed-response retries stopped being a line item in the budget.
Duplication was harder. A user could specify a recipient profile narrow enough that the model genuinely couldn’t find twelve distinct suggestions. Caching already-returned items filtered repeats, but that’s a patch, not a solution, once you want infinite scroll. I settled on a compromise: the result set terminates gracefully when the model stops producing new suggestions, rather than padding with near-duplicates. Not ideal for edge cases, better for everyone else.
Problem 2: Grounding suggestions in real products
Language models are confidently wrong about prices and will invent products outright. An app that suggests a $40 item costing $200 breaks trust on the first try. This was our first concern and where most of the initial development effort went. However, the solution didn’t manifest until later down the road.

The answer was lateral: stop relying on the model’s internal knowledge and use its tool-calling instead. I implemented MCP tools that let the agent query the Amazon Gateway API directly, searching on keywords and categories it derived from the recipient profile. That narrowed the hallucination surface to near zero — everything returned from the tool was already valid and trusted. Price, name, link, images, all of it real.
Problem 3: Latency
Twelve structured, personalized suggestions take real time. We launched at a 60-second average with a p90 close to three minutes. Smooth animations and rotating status messages made the wait tolerable, but they were a bandage.
First attempt: cut the initial fetch from twelve items to eight. Marginal latency gain, and it broke the layout on larger screens. Second attempt worked better — start the API call earlier in the recipient-creation flow, since fields like the occasion date aren’t needed to retrieve listings. That cut waiting time roughly 25%.
As the UI added images and infinite scroll, API demand grew past what flow changes could absorb. The fix was architectural: scale ECS horizontally and vertically, move to MongoDB with higher-tier nodes, and — most importantly — add a centralized Redis cache of every listing snapshot, queried before touching any third party. Listings are uniquely identifiable and persist cheaply on our side, so the cache compounded: every new user diversified the pool for everyone after them.
End state: 75% latency reduction, p90 at 45 seconds.
Problem 4: Anonymous users on an expensive endpoint
No accounts meant no user ID to rate-limit against and nowhere to store gift lists. This was non-negotiable from the start — we wanted casual users in without an onboarding wall — but it raised three questions: how do we attribute data to a user, how do we stop abuse, and what happens when someone deletes and reinstalls?
Less complex than it looks. No account doesn’t mean no identity. I used Firebase anonymous authentication to assign a user ID scoped to the device and install, with no data collected from the user. Every data point and every action tied back to that ID, which made rate limits and access controls straightforward. It also let us persist non-sensitive data — profile basics, recipients, gift lists, events — and restore it automatically on reinstall. A delete-and-start-fresh option came later.
Problem 5: Cutting operating cost
We set a $500/month ceiling when we designed the platform. Costs came from OpenAI, the Amazon Search API, cloud infrastructure (Google Cloud, later AWS), and MongoDB Atlas — all consumption-based, all highly variable, none easy to forecast. Caching and rate limits smoothed the peaks from the start, mostly as insurance against going viral and waking up to a five-figure AWS bill. It wasn’t enough. We were running over $650 in quiet months and past $1,000 during holidays.
Three things fixed it:
Backend efficiency. I profiled the request-handling and response-parsing paths and found real waste. Moving to strict schema validation cut JSON parsing retries by 12%. Refactoring the endpoints and trimming the response data structure reduced both latency and throughput requirements per generation.
Caching. Product suggestions repeat far more than you’d expect — a given gift covers a wide range of recipients. Local on-device caching plus the shared backend snapshot pool cut Amazon API calls roughly 45%.
Model economics. As new models shipped, older ones got cheaper. I re-benchmarked quality against cost and found that models a generation or two back were good enough for this workload, taking token costs from ~$15/M to ~$4/M.
Together with prompt optimization, model specialization by task, and a move to cheaper storage, monthly infrastructure went from $650 to under $400 — a 38% reduction.
Problem 6: Traffic spikes and seasonality
Cheerful was covered by Yahoo News, NBC Los Angeles, FOX 26 Houston, Good Day Alabama, and Good Things Utah. A morning segment is a load event, and a gift app has an obvious annual shape.
The spikes never stressed the infrastructure past its limits — the caching and scaling work above absorbed them. We’d see 400–600 new downloads in a day off a segment, with corresponding backend and API load. Traffic almost always correlated with revenue, so peak load was never the thing that worried us.

The off-season was more interesting, and it’s where I learned the most. Traffic there didn’t convert, because our revenue model depended on a 24-hour purchase window to earn commission. Users would browse, leave, and buy later — outside the attribution window. We were paying for generation and getting nothing back.
The fix was a conversion problem, not an infrastructure one: event reminders, incentives for prompt purchase, transparency about how the app stays free (subtle copy near results), and a more aggressive push notification cycle with reminders tailored to the occasion type attached to each recipient list. Purchase-after-search went from 13% to 24%.
Architecture

A request runs: client → Route 53 → ALB → Node.js tasks on ECS, backed by an Auto Scaling Group of EC2 instances. ECS Service Auto Scaling adjusts task count on CloudWatch CPU metrics while the ASG keeps enough instances underneath. The service first checks the Redis snapshot cache for matching listings. On a miss, it calls the OpenAI API, which uses MCP tools to query the Amazon Gateway API for real products and returns ranked recommendations. Post-processing Lambdas normalize results and archive new listings back into Redis, so the cache grows with use. MongoDB holds user data; Firebase issues the anonymous device identity. Deployment runs GitHub → ECR → ECS.
What I’d do differently
Instrument cost per session from day one. Discovering how far off our estimates were, late in development, strained both budget and timeline. Worse, we didn’t understand our own revenue model’s failure mode until we lived through an off-season. Planning for peak load matters; understanding how the business behaves in every scenario matters more.
Build the snapshot cache first. It ended up solving latency, cost, and API load simultaneously. Designing the data flow properly at the start would have saved significant money.
Build evaluation from the beginning. We could tell whether the output was well-formed. We couldn’t tell whether it was good. Visibility into what users were actually getting would have saved us guesswork and freed time for revenue work.
Outcomes
Over 50,000 users at peak, purchasing hundreds of items a week. I cut operating costs 38% ($650 → under $400/month), reduced latency 75% (p90 from ~3 minutes to 45 seconds), and raised purchase-after-search conversion from 13% to 24%. Coverage across five national and regional outlets.
NeuNet wound down in 2025 and ceased all operations, including the Cheerful app and website.