# Meal Receipt

> Use when processing grocery receipt images for the Meal OS system. Triggered by "/meal-receipt" or "process receipts" or "scan receipts".

- Skill: `scurry/meal-receipt` (Agent Skill)
- Install (CLI): `npx skillmds@latest add scurry/meal-receipt`
- Raw SKILL.md: https://api.skillmd.com/api/skills/scurry/meal-receipt/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: scurry (https://skillmd.com/u/scurry)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/scurry/meal-receipt

---


# /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 IDs
- `data/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:

1. **Exact alias match** — check `data/receipts/aliases.yaml` for the item description
2. **Fuzzy match against canonical-ingredients.yaml** — compare to display names
3. **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
4. **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 to `data/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:

```yaml
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:

```yaml
# 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.

