All projects

Case study

Pear

An iPhone app with a Google Cloud backend that checks a food or cosmetic against each family member's allergies, diet and health, in English, Vietnamese or Spanish.

Zero missed allergens across 1,800+ tests, built solo in about 6 hours

Who had the problem#

Whoever shops for a household carries everyone's list in their head. In Pear's fictional demo family, that shopper is Linh, who has fragrance-sensitive skin, and her list is long: Mia, 8, has a strict peanut and sesame allergy and eczema; Bà, 72, has high blood pressure and prediabetes; Leo, 16, is vegan. The label in her hand knows none of it.

The list comes in 3 languages, too: Bà reads Vietnamese, Leo reads Spanish, Mia reads English, and Linh reads English and Vietnamese. And the stakes are lopsided. A false alarm is fine, but a missed allergen is not.

What I built#

Pear answers per person. Scan a barcode, snap the ingredient label, or ask a question like "Bà ăn mì này được không?" ("Can Bà eat these noodles?"), and Pear gives one verdict for each family member: "Not for Mia: contains sesame, may contain peanuts. Fine for Linh, Bà and Leo."

In about 6 hours I built both halves. The backend is a FastAPI service for Google Cloud Run with a sourced knowledge base (FDA, NIH, ASPCA and others) covering 17 allergens, 11 diets, health signals like sodium and added sugars, and skin triggers like fragrance. The SwiftUI iPhone app has onboarding for each family member, one camera for barcodes and labels, dictation for questions, and a result sheet that reads each line aloud.

When a food is a no for someone, Pear suggests up to 3 better picks that fit everyone, found by vector search and re-checked by the same rules, with prices found through Google Search. Photos are never stored, profiles stay on the phone, and Pear never calls a product "safe" in any of its 3 languages.

How it works#

Code routes, Gemini reads, rules decide: that rule shapes the whole system. The endpoint is the router, so no model ever picks a path. A barcode gets its verdict with zero AI calls (Firestore cache first, then a race between Open Food Facts and Open Beauty Facts, with USDA FoodData Central as backup), and a label photo or a question gets one generative Gemini call before the verdict, never more. In the diagram, all 3 inputs flow into the same rules engine.

Code routes, Gemini reads, rules decideA barcode, a label photo and a question all reach the same endpoint, which routes them. A barcode gets its verdict with zero AI calls; a photo or a question gets one Gemini call. Every source fills the same RawProduct record, and the rules engine (a normalizer and a pure evaluate function) gives one verdict.RULES ENGINEBarcodeLabel photoQuestionEndpointthe router: nomodel ever picksa pathFirestorecachefirst, zero AI callsOpen FoodFactsraces Open BeautyFacts, USDA asbackupGeminione generativecall, never moreRawProductrecordevery source fillsthe same oneNormalizerhits: id, exact printedwords, strengthevaluate()pure function of thehits and eachperson's profileOne verdictAllergy, Unsure, Diet, Skipit, Easy does it, RipeCode routes, Gemini reads, rules decideA barcode, a label photo and a question all reach the same endpoint, which routes them. A barcode gets its verdict with zero AI calls; a photo or a question gets one Gemini call. Every source fills the same RawProduct record, and the rules engine (a normalizer and a pure evaluate function) gives one verdict.RULES ENGINEBarcodeLabel photoQuestionEndpointthe router: no modelever picks a pathFirestore cachefirst, zero AI callsOpen FoodFactsraces Open BeautyFacts, USDA asbackupGeminione generative call,never moreRawProduct recordevery source fills the same oneNormalizerhits: id, exact printed words, strengthevaluate()pure function of the hits andeach person's profileOne verdictAllergy, Unsure, Diet, Skipit, Easy does it, Ripe
Code routes, Gemini reads, rules decideDashed box: an AI model or agent
  1. A barcode, a label photo or a question reaches the endpoint, which is the router: no model ever picks a path.
  2. A barcode checks the Firestore cache first, then races Open Food Facts and Open Beauty Facts, with USDA FoodData Central as backup. Zero AI calls.
  3. A label photo or a question gets one generative Gemini call, never more.
  4. Every source fills the same RawProduct record.
  5. The normalizer turns it into hits: a knowledge-base id, the exact printed words and a strength.
  6. The pure evaluate() function maps the hits and each person's profile to one verdict: Allergy, Unsure, Diet, Skip it, Easy does it or Ripe.

Every source fills the same RawProduct record. A normalizer turns it into hits, each with a knowledge-base id, the exact printed words (so the app can highlight them) and a strength: contains, may contain or possible. A pure evaluate() function then maps the hits and each person's profile to one verdict, with precedence Allergy, Unsure, Diet, Skip it, Easy does it, Ripe, and grades health signals against FDA Daily Values, with rinse-off cosmetics counting half.

The safety design lives in a few hard rules. When Gemini reads a label, Pear's allergen flags are the union of its own dictionary match and the model's picks, so the model can add a flag but never remove one. A partial, missing or name-matched ingredient list never earns Ripe for a strict allergy, and the same product and family always get the same answer.

The hardest part#

Pear's first promise is to never miss an allergen, and real labels arrive messy. Vietnamese labels that lose their accent marks show why: Open Food Facts entries and OCR can drop diacritics, so one red-team test is a snack cake label where trứng (egg) shows up as "trung" and đường (sugar) as "duong". The accents carry meaning, too: mè is sesame, but me is tamarind.

The fix is a second matcher pass on accent-folded text, and like every extra matcher pass in Pear it can only add hits, never remove one. Before it runs, Pear measures the label: if at least half the words carry accents, the label counts as well accented, so a bare "me" stays tamarind. If the accents are mostly gone, one-syllable words like "me" and "trung" fire as possible hits on a Vietnamese label, which still flags exactly the person with that allergy.

That pass was one of several red-team fixes, and the round left 114 test cases behind: Spanish without accents ("ATUN", "CAMARON"), "peanut butter" broken mid-word across a line break, footnote marks glued to words ("Milk¹"), and bare additive codes like "Preservative (220)". Of those, 108 now sit in the allergen gate, so the same miss would fail the gate if it ever came back.

Results in numbers#

On the final commit, the backend passes more than 3,000 offline tests with a fake Gemini, an in-memory store and mocked HTTP, so they need no network or credentials. That includes the allergen gate: more than 1,800 tests with zero misses, built from 231 allergen scenarios in English, Vietnamese, Spanish and cosmetic INCI names, each checked 8 ways, from the expected verdict to same input, same answer.

Speed was in the plan before any code existed: the barcode path reaches its verdict with no AI call, and every path makes at most one generative Gemini call before the verdict. A contract and latency smoke test passed 68 of 68 checks against a local server.

At judging, the iPhone app ran barcode, photo and voice scans end to end against an earlier 0.1 version of the API, built the same day. Google sign-in was still a placeholder (guest mode), and this repo's 0.3.0 backend wasn't deployed yet, so its Gemini paths had only run offline against a fake model.

What's next#

The app and the backend need to meet first: make the 0.3.0 API answer in the wire format the app already speaks, deploy it next to the old service, point the app at it, and check every Gemini path live on Cloud Run. Then real Google sign-in replaces the guest-mode placeholder, and the demo product sheet gets its real barcodes.

After that, my roadmap calls for a TestFlight beta and 10 interviews with families who shop for relatives. Pear isn't medical advice, so an allergist or dietitian should review the knowledge base before any real family relies on it.