/meal-receipt — Receipt OCR & Price Tracker
Process grocery receipt images to build a price history ledger for cost estimates and loss leader detection.
Announce at start: "Processing grocery receipts..."
Step 1: Get Receipt Images
The user provides receipt images in one of two ways:
Option A: Folder Scan
Scan data/receipts/images/ for image files (.jpg, .jpeg, .png). Ignore files in the processed/ subfolder.
Group images into receipts by set ID — everything before the last -N in the filename:
mb-1.jpg,mb-2.jpg,mb-3.jpg→ set "mb" (one receipt, 3 pages)hannafords-1.jpg→ set "hannafords" (one receipt, 1 page)mb2-1.jpg,mb2-2.jpg→ set "mb2" (second receipt, 2 pages)
The -N suffix determines page order within a set.
Option B: Chat
If the user provides image paths directly (e.g., /meal-receipt @mb-1.jpg @mb-2.jpg):
- If multiple images, ask: "Are these all from the same receipt?"
- If yes, treat as one set ordered by the filenames
- If no, ask the user to clarify grouping
If no images are found or provided, say: "No receipt images found. Drop photos in data/receipts/images/ (e.g., mb-1.jpg, mb-2.jpg) or pass them directly: /meal-receipt @receipt.jpg"
Step 2: Load References
Read these files in parallel:
data/canonical-ingredients.yaml— for matching items to canonical IDsdata/receipts/aliases.yaml— for learned abbreviation mappings (if file exists)
If canonical-ingredients.yaml does not exist: Stop and tell the user: "data/canonical-ingredients.yaml is missing. Run /meal-setup first to initialize the project, then re-run /meal-receipt."
Step 3: OCR Receipt Images
For each receipt set, read the images in page order using the Read tool (Claude vision).
Extract from each receipt:
- Store name — from the receipt header
- Date — from the receipt header (fall back to file modification date if unreadable)
- Line items — each with:
- Item description (as printed)
- Quantity (if weight-based, e.g., "2.13 lb")
- Unit price (e.g., "$2.99/lb")
- Total price
Lines to skip:
- Tax lines, subtotals, totals, tender/payment, change
- Savings summary lines, coupon lines, rewards/loyalty lines
- Store header/footer info (address, phone, etc.)
Price normalization:
- Weight-based: "2.13 lb @ $2.99/lb = $6.37" → extract per-unit price $2.99/lb
- Multi-for: "2 @ 2/$5" → $2.50/each
- Per-item: "$3.99" → $3.99/each
If store or date cannot be determined from the receipt, ask the user.
Step 4: Match Items to Canonical Ingredients
For each extracted line item, attempt to match to a canonical ingredient ID using this priority:
- Exact alias match — check
data/receipts/aliases.yamlfor the item description - Fuzzy match against canonical-ingredients.yaml — compare to display names
- Common abbreviation patterns — apply known grocery shorthand:
- CKN/CHKN = chicken, BRST = breast, THGH = thigh
- EVOO = olive oil, EV OLV OIL = olive oil
- GRK = greek, YOG = yogurt
- ORG = organic (modifier, match base item)
- FRSH = fresh (modifier, match base item)
- GRND = ground
- BKLS/BNLS = boneless, SKNLS = skinless
- No match — mark as
?
Step 5: Preview and Confirm
Show the full mapping before saving anything:
Receipt: [Store Name] — [YYYY-MM-DD] ([N] images)
Matched items:
1. [RECEIPT TEXT] $[price] → [canonical_id] ($[unit_price]/[unit])
2. [RECEIPT TEXT] $[price] → [canonical_id] ($[unit_price]/[unit])
...
Unmatched:
N. [RECEIPT TEXT] $[price] → ? ([suggestion or "non-food, skip"])
...
Confirm? (yes / fix [#] [canonical_id] / skip [#])
User corrections:
- yes — save all matched items to the price ledger
- fix 6 pasta — changes item 6's mapping to
pasta; saves the abbreviation → canonical_id pair todata/receipts/aliases.yaml - skip 5 — marks item as non-food or irrelevant, excluded from ledger
- Allow multiple corrections before final yes
- After corrections, re-show the updated mapping for final confirmation
Step 6: Update Price Ledger
Write confirmed items to data/receipts/price-history.yaml.
Note: data/receipts/price-history.yaml is gitignored. See samples/receipts/price-history.yaml for a sanitized example.
Ledger format:
chicken_breast:
- date: 2026-03-22
store: Market Basket
price: 2.99
unit: /lb
receipt: mb
- date: 2026-03-15
store: Hannaford's
price: 4.49
unit: /lb
receipt: hannafords
olive_oil:
- date: 2026-03-22
store: Market Basket
price: 9.99
unit: /25 oz
receipt: mb
Rules:
- Append-only — never overwrite or remove previous entries
- Organize by canonical ID (or human-readable name for non-canonical items)
- Entries sorted by date descending within each ID
- If the file doesn't exist yet, create it
- If the file exists, read it first and append new entries
Step 7: Update Aliases
Write any new alias mappings (from user corrections or high-confidence matches) to data/receipts/aliases.yaml.
Aliases format:
# Learned receipt abbreviation → canonical ingredient ID
# Added automatically when user corrects a fuzzy match
'MB CKNBRST': chicken_breast
'PERDUE CKNBRST': chicken_breast
'MB GRAPE TOMATO': cherry_tomato
'EVOO 25OZ': olive_oil
'CHOBANI GREEK YOG': greek_yogurt
Rules:
- Always single-quote alias keys — receipt text is unpredictable and may contain
#,:,%, or other YAML-special characters - If the file doesn't exist yet, create it with a header comment
- If the file exists, read it first and append new entries
- Never overwrite existing aliases (if a conflict arises, keep the existing one and warn)
- Only save aliases from user corrections (fix command) and high-confidence matches that the user confirmed
Step 8: Move Processed Images
After successful processing, move all images in the receipt set from data/receipts/images/ to data/receipts/images/processed/ to prevent re-processing while preserving originals.
Step 9: Confirm
Output a summary:
Receipt processed: [set ID] ([Store Name], [YYYY-MM-DD])
Items matched: [N]
Items skipped: [M] (non-food)
New aliases learned: [K]
Price ledger updated: data/receipts/price-history.yaml
Notable prices vs. history:
- [canonical_id]: $X.XX/unit (avg $Y.YY, ZZ% below/above average)
- [canonical_id]: $X.XX/unit (first entry — no comparison yet)
...
If multiple receipt sets were processed, show a summary for each.