Everyone has an opinion about grocery prices. Mine was vibes. Eggs felt expensive. Milk felt expensive. Everything felt expensive.
I didn’t need to guess, though. I have kept the household books for well over a decade, and every receipt gets scanned and filed next to the ledger. From 2019 to today that is 1,754 grocery receipts from twelve stores. Costco, Superstore, Walmart, Co-op, T&T, Sobeys, Safeway, Save-On-Foods, Bulk Barn and a few others.
The ledger only records totals: $86.24 at Costco, $8.97 at Superstore. What we bought is on the receipts. So I read them.
The receipts were pictures
Every receipt is a scan from a document scanner. A PDF with an image inside and no text layer. A computer sees a photograph of a till slip.
So I wrote larder. It’s a Go CLI that reads a directory of receipt scans and works out three things:
- what we buy on a regular basis,
- how often we buy it, and
- how the price has moved over time.
Text recognition uses Apple’s Vision framework through cgo. No tesseract, no cloud OCR, no API key. Receipts never leave the machine, which matters when the documents are your family’s spending.
Every grocery receipt prints a code next to each item: a Costco item number, a Superstore UPC, a produce PLU. That code is the key that makes price tracking possible. 39036 was romaine in 2019 and it is romaine today.
39036 ROMAINE 8.99
339054 PF GOLDFISH 13.99
2014432 TPD/339054 3.00-
That third line is a Costco instant saving. Hold that thought.
Reading a receipt is harder than it looks
Text recognition on a clean document is a solved problem. Receipts are not clean documents.
- Thermal paper fades. A receipt from 2019 reads
458 MILK 49where it should say4.49. - Scans are crooked. At three degrees of tilt, the price on the right drifts onto the line below its item.
- Digits look like letters.
2.0C-,B.49,$1 4T. - Long receipts are long. Vision downsamples a tall image, and a big Costco shop turns to mush.
Each store also prints differently. Superstore puts the price on the next line when there is a multi-buy. Walmart says 2 AT $0.05. T&T prefixes sale items with (SALE) and never prints the regular price.
The trick that made this work is that every receipt checks itself. The items have to add up to the printed subtotal. When they don’t, larder tries again:
- Re-read the receipt at 300 DPI, then 150 DPI. Faint print often reads correctly at one resolution and not another.
- Straighten the page. Vision reports the slope of each line of text, so rows can be regrouped along the tilt.
- If exactly one price is unreadable, infer it from the subtotal.
- If one item reads
.39beside an identical item at8.39, try the sibling’s price and see if the receipt balances.
Here is how that went across every store:
| Store | Receipts | Reconciled | Items read |
|---|---|---|---|
| Costco | 637 | 475 (75%) | 4,663 |
| Real Canadian Superstore | 434 | 364 (84%) | 2,467 |
| Walmart | 282 | 213 (76%) | 1,538 |
| Calgary Co-op | 217 | 153 (71%) | 440 |
| Sobeys | 53 | 43 (81%) | 131 |
| T&T Supermarket | 44 | 36 (82%) | 200 |
| Save-On-Foods | 38 | 28 (74%) | 100 |
| Bulk Barn | 22 | 14 (64%) | 88 |
| Wholesale Club | 16 | 13 (81%) | 64 |
| Safeway | 8 | 7 (88%) | 24 |
| FreshCo | 2 | 2 (100%) | 7 |
| IGA | 1 | 1 (100%) | 2 |
| Total | 1,754 | 1,349 (77%) | 9,724 |
Seventy-seven percent of receipts add up to the cent. The rest still count. They are just missing a line or carrying a misread price somewhere, and larder throws out any price that is more than double or less than half of a product’s usual price.
The first pass took about six minutes. After that everything is cached and the whole analysis runs in a second.
What we actually buy
I defined a regular as anything bought on at least four separate days spanning at least two months. That gives 349 products. 191 of them we still buy; the other 158 have lapsed.
These are the twenty we buy most often:
| Product | Days bought | Every | On sale | Price (2026) | Shelf change | Paid change |
|---|---|---|---|---|---|---|
| Milk, homogenised 4 L | 227 | 9 days | 1 of 244 | $6.31 | +30% | +30% |
| Bananas | 144 | 14 days | 0 of 154 | $1.99 | +5% | +5% |
| Milk, skim 4 L | 108 | 15 days | 0 of 113 | $5.46 | +23% | +23% |
| Eggs, 5 dozen | 95 | 25 days | 1 of 97 | $20.26 | +46% | +46% |
| Chips, No Name | 76 | 25 days | 21 of 106 | $1.45 | +34% | +34% |
| Cucumbers | 60 | 18 days | 6 of 60 | $3.58 | −4% | −12% |
| Yogurt, Foremost tub | 58 | 16 days | 0 of 58 | $17.39 | +10%¹ | +10%¹ |
| Decaf coffee, Nescafé Rich | 48 | 51 days | 24 of 70 | $7.87 | +103% | +62% |
| Tortillas, 36 ct | 40 | 35 days | 21 of 56 | $9.99 | +9% | +9% |
| Romaine hearts | 38 | 19 days | 0 of 39 | $8.51 | +58% | +58% |
| Whipping cream 1 L | 36 | 46 days | 0 of 36 | $5.79 | +54% | +54% |
| Pepperoni pizza, 18" (Costco food court) | 33 | 32 days | 0 of 33 | $12.99 | 0%² | 0%² |
| Milk, 1% 4 L | 32 | 22 days | 0 of 42 | $5.96 | +18% | +18% |
| Cheddar slices | 31 | 67 days | 29 of 41 | $9.99 | +9% | +16% |
| Roma tomatoes | 31 | 18 days | 1 of 31 | $4.95 | +7% | +7% |
| Baguette | 30 | 72 days | 10 of 31 | $5.99 | +33% | +44% |
| Goldfish crackers | 28 | 86 days | 44 of 46 | $13.99 | +40% | +34% |
| Potatoes, yellow | 25 | 42 days | 3 of 26 | $7.99 | +16% | +6% |
| English muffins | 24 | 62 days | 5 of 28 | $2.06 | +4%³ | +18%³ |
| Mayonnaise | 22 | 96 days | 27 of 29 | $10.99 | +52% | +62% |
¹ Since 2024. ² Since 2023. ³ Since 2022. Everything else is since 2019. “Every” is the median gap between purchases. “On sale” counts individual purchases, so it can exceed the days bought.
The thing that surprised me is how slow “regular” is. Of the 191 products we still buy:
| How often | Products |
|---|---|
| Every two weeks or more often | 3 |
| Every two weeks to a month | 13 |
| Every one to three months | 27 |
| Less often than every three months | 148 |
Homogenised milk, bananas and bean sprouts are the only things we buy at least every fortnight. Everything else is a long tail of products we come back to every few months. That long tail is where most of a grocery bill hides.
What it costs now
Individual products are noisy. To get one number, larder builds a price index for our own basket. Each year, every regular product’s price is compared with the year before, weighted by what we spend on it, and the changes are chained together. 2019 is 100.
| Year | Shelf prices | On the year | What we paid | On the year | Products compared |
|---|---|---|---|---|---|
| 2019 | 100.0 | — | 100.0 | — | — |
| 2020 | 102.3 | +2.3% | 103.4 | +3.4% | 85 |
| 2021 | 105.4 | +3.0% | 103.7 | +0.3% | 100 |
| 2022 | 114.9 | +9.0% | 113.5 | +9.4% | 118 |
| 2023 | 123.6 | +7.6% | 122.4 | +7.8% | 131 |
| 2024 | 125.9 | +1.9% | 125.6 | +2.6% | 151 |
| 2025 | 128.8 | +2.3% | 129.0 | +2.7% | 147 |
| 2026 | 130.1 | +1.0% | 130.7 | +1.3% | 125 |
The things we buy regularly cost 30% more than they did in 2019. Most of that arrived in two years. 2022 and 2023 added 17% between them. 2026 is only through the start of October.
The biggest increases among things we still buy:
| Product | From | Then | Now | Shelf change | Paid change |
|---|---|---|---|---|---|
| Decaf coffee, Nescafé Rich | 2019 | $3.87 | $7.87 | +103% | +62% |
| Romaine hearts | 2019 | $5.39 | $8.51 | +58% | +58% |
| Small buns (T&T) | 2019 | $1.49 | $2.30 | +54% | +54% |
| Whipping cream 1 L | 2019 | $3.66 | $5.64 | +54% | +54% |
| Mayonnaise | 2019 | $7.24 | $10.99 | +52% | +62% |
| Flour 10 kg | 2020 | $6.66 | $9.79 | +47% | +47% |
| Eggs, 5 dozen | 2019 | $13.90 | $20.26 | +46% | +46% |
| Brioche buns | 2022 | $4.99 | $6.99 | +40% | +67% |
| Goldfish crackers | 2019 | $9.99 | $13.99 | +40% | +34% |
| Corn, organic | 2020 | $9.99 | $13.99 | +40% | +40% |
And a few that went the other way:
| Product | From | Then | Now | Shelf change |
|---|---|---|---|---|
| Pork blade | 2024 | $33.02 | $24.79 | −25% |
| Superfries seasoned, McCain | 2023 | $5.39 | $4.10 | −24% |
| Pizza sauce, No Name | 2021 | $1.09 | $1.00 | −8% |
| Dishwasher detergent, No Name | 2024 | $5.99 | $5.57 | −7% |
| Honey, creamed | 2022 | $19.74 | $18.49 | −6% |
Bananas deserve a mention. $1.89 at Costco in 2019, $1.99 since 2021, and not a cent more since. Five percent in seven years.
Sales change the answer
Two numbers sit side by side in every table above, and they are not the same thing.
- Shelf price is what the store charges at regular price. The Goldfish line in that Costco receipt has a shelf price of $13.99.
- Paid price is what we actually handed over after sales and instant savings. The same Goldfish cost us $10.99.
If you only track what you paid, a good sale year looks like deflation. If you only track the shelf price, you miss that you can dodge some of the increase by shopping the flyer. So larder tracks both, over the same purchases, with the same averaging. When there’s no sale, the two numbers are identical. Any gap between them is entirely sales.
Every store says “on sale” differently. Costco prints TPD instant savings. Superstore prints the regular price beside the price you paid. Walmart tags multi-buy items with a group number and prints the discount later. Sobeys and Co-op print YOU SAVED. Each needed its own parser.
Across the whole basket, sales barely moved the needle. Shelf prices are up 30.1%; what we paid is up 30.7%. We bought on sale at roughly the same rate every year, so sales lowered the bill without changing the slope.
Product by product, it is a different story:
| Product | Shelf change | Paid change | On sale |
|---|---|---|---|
| Decaf coffee, Nescafé Rich | +103% | +62% | 24 of 70 |
| Mac & cheese, white cheddar | +19% | −7% | 6 of 17 |
| Brioche buns | +40% | +67% | 11 of 13 |
| Onions, yellow | 0% | +38% | 3 of 11 |
| Superfries seasoned, McCain | −24% | 0% | 8 of 12 |
| Broccoli (large pack) | +40% | +57% | 15 of 20 |
The decaf price doubled on the shelf. We caught enough sales to hold our increase to 62%.
Brioche buns went the other way, and the reason is subtle. Costco’s instant saving on them stayed at a flat $2 while the shelf price went from $4.99 to $6.99. The sticker rose 40%. What we paid went from $2.99 to $4.99, which is 67%. A fixed-dollar discount shrinks as prices rise.
Some products we only ever buy on sale:
| Product | On sale | Typical discount | Saved |
|---|---|---|---|
| Cheerios | 29 of 29 | 29% | $69.00 |
| Multigrain Cheerios | 23 of 23 | 21% | $47.00 |
| KFI sauce | 19 of 19 | 21% | $38.00 |
| Froot Loops | 18 of 18 | 22% | $39.00 |
| Facial tissue, Scotties | 15 of 15 | 22% | $83.00 |
| Mini-Wheats | 14 of 14 | 23% | $32.50 |
| Welch’s fruit snacks, 60 ct | 14 of 14 | 21% | $44.00 |
| Dare crackers | 13 of 13 | 22% | $28.00 |
Every one of those is a Costco product, and every single purchase carried an instant saving. Either those products are permanently discounted or we only buy them when they are. The receipts can’t say which.
The staples are the opposite. Bananas: 0 of 154 on sale. Skim milk: 0 of 113. Romaine: 0 of 39. Milk is almost never on sale, and we buy it anyway.
In total, 2,662 of 9,030 items were bought on sale, just under 30%. The receipts record $7,620 in savings since 2019, a little over $1,000 a year.
What I left out
The interactive version has all 349 regular products. It shows purchase timelines, every price paid, and sales marked on each one. I was careful about what went into it:
- No health, personal-care or alcohol purchases. What a family buys for the medicine cabinet is nobody’s business. larder has a
--publicmode that drops those categories and anything whose name looks like it belongs in them. - No exact dates. Every purchase date is rounded to the start of its week. The cadence still shows; the household routine doesn’t.
- No store item codes, card numbers, member numbers or file paths. None of them were ever in the dashboard data, and the public mode strips the codes too.
- No Flashfood. Those “receipts” are app screenshots of clearance food, and clearance prices would distort every trend they touched.
Some honest limitations:
- Superstore doesn’t always print the regular price beside a sale price. When it doesn’t, the “shelf” price is really a sale price, and that product’s shelf increase is understated.
- Stores without item codes (Co-op, Sobeys, Safeway, T&T, Save-On-Foods, Bulk Barn) are matched on description. One misread letter can split a product in two.
- Pack sizes change. When a store keeps the same description but sells a bigger box, larder keeps them apart if the prices are far enough apart. Shrinkflation that keeps the price and the code is invisible.
What I learned
- The receipt checks itself. Reconciling against the printed subtotal turned OCR from “probably right” into “provably right” for 77% of receipts, and told me exactly which ones to distrust.
- Codes beat names. A UPC is stable for years. A description is whatever the till operator’s software felt like printing.
- Shelf price and paid price answer different questions. The first is about the stores. The second is about us.
- “Regular” is slower than you think. Three products a fortnight, and a long tail of things we buy every few months.
- Thirty percent since 2019. Now it is a number instead of a feeling.
larder is written in Go and runs on macOS. The code is on GitHub. It was built around one household’s receipts, and it shows.