Inspiration
46% of UBC students said they worried about affording groceries in the year before May 2025, according to the AMS Academic Experience Survey. The usual advice is to shop the sales. This week, that means 128 flyer pages and 1,577 listings across five chains near campus.
Nobody on a student schedule reads that. Flyer apps can match a typed list, but they don't know what you buy unless you tell them. So we built something that learns your groceries from your receipts and does the reading for you.
What it does
Savy is a phone app for UBC students on a food budget. Photograph a receipt and your usual groceries become your list. Each week, the app checks that list against every flyer near your postal code and shows only the deals on your items.
Four ways to add items: receipt photo, product photo, typing, or hold-to-talk voice ("a 4 L jug of 2% milk").
Exact deals: a match needs the same brand, item, variant and size.
Basket: the cheapest single store for your list, or the split-shop option with the dollar difference.
Proof: every deal opens the real flyer page with the item outlined.
Impact statement (Community Impact): Our community is UBC students choosing between groceries and everything else. The app needs no account, so nothing stands between a student and this week's deals. Voice entry and screen-reader labels mean nobody has to type a list. Our goal is to replace an hour of flyer reading with one receipt photo.
How we built it
Databricks (the data backbone):
- Unity Catalog: realdeal.core holds the Volumes, pipeline tables, app tables and live flyer listings.
- Lakeflow Declarative Pipelines: Auto Loader streams flyer files from a Volume through Bronze, Silver and Gold, deployed with an Asset Bundle.
- AI Functions: ai_query with Llama 4 Maverick turns each printed flyer line into brand, item, variant and size.
- Data quality: pipeline expectations drop unparseable rows and track low-confidence ones.
- Model Serving: a hosted model cleans product names and assigns categories.
- Delta tables: every flyer refresh updates live_listings and merges into price_history, so prices build up week by week.
- Databricks Apps: the FastAPI backend runs as an App and reads Gold through the SQL Statement API.
- Genie: students ask plain-language questions about this week's flyers and see the answer, the table and the SQL it wrote.
App: Next.js PWA, FastAPI, Claude for receipts and product photos, ElevenLabs for speech-to-text, and Flipp's flyer data.
One decision we measured: we tried receipts on ai_query too. It took about 25 s against Claude's 12 s and misread abbreviated lines with high confidence, so receipts stay on Claude and ai_query handles the flyers.
One shared vocabulary: flyer lines go through the same prompt as typed items, so both sides land in the same words and can be compared exactly.
Challenges we ran into
- ai_query silently dropped fields. A DDL response format made every field optional, and the model skipped price and confidence. We switched to a strict JSON schema with required, nullable fields.
- A smaller model invented products. Gemma 3 12B hallucinated items on page one. We tested on a real page and moved to Llama 4 Maverick.
- Free Edition restricts outbound internet from compute. We fetch flyers outside the workspace and write JSONL to a Volume, where Auto Loader picks them up.
- Fuzzy matching recommended the wrong products. A wrong deal costs a student real money, so we replaced vector search with exact matching and size tolerance windows.
Accomplishments that we're proud of
- All five chains run end-to-end: 1,764 listings in flyer_items and 1,147 Gold price rows.
- Real price gaps surfaced: Kicking Horse coffee (284 g) was $11.00 at one chain and $13.99 at another.
- 240 passing backend and data tests.
- Four working intake methods, on a phone, in one weekend.
What we learned
Structured output depends as much on the schema as on the prompt. We also learned to test models on our ugliest page first. Unit parsing was harder than the AI: "6 x 340 mL" has humbled all of us.
What's next for Savy
- Grow from 5 chains to 8, including Walmart and Costco.
- Use the week-over-week prices already accumulating in price_history to flag fake sales against StatCan baselines.
- Test with 20 UBC students and approach the AMS Food Bank about a pilot.
Groceries are the budget line students can actually change, and every flyer is already public.
Built With
- ai-functions
- claude
- databricks
- databricks-apps
- delta-lake
- elevenlabs
- fastapi
- flipp
- genie
- lakeflow
- llama-4
- next.js
- pwa
- python
- typescript
- unity-catalog
