Product case study
PriceScan Cyprus answers that question in one photo: snap a receipt or a shelf tag, and see how the price compares against your own history and every store's catalog. I led the product from concept to live — this is the full story: discovery, specs, prioritisation, data, and the trade-offs in between.
01 · The problem
Cyprus has a handful of supermarket chains, no public price-comparison infrastructure, and persistent food inflation. Shoppers have no way to know whether €4.19 for a specific detergent is a good price, a normal price, or a rip-off — the information exists only as a memory of past shopping trips.
The insight: every shopper already collects the data that would answer this — their receipts — and throws it away. A receipt is a structured price list, timestamped and store-attributed. If capturing it were effortless, each user would build a personal price history, and the aggregate would become a live market database no retailer publishes.
Target user: the household's primary shopper. Core jobs-to-be-done: "Tell me if this shelf price is worth paying" (in-store, seconds matter) and "Tell me where my basket is cheapest" (planning, depth matters).
02 · Discovery
Before committing engineering effort to a full application, I validated the entire concept as an AI-assistant prototype — a prompt-and-JSON build that read receipt photos, tracked price history in a flat file, and gave buy/wait verdicts on shelf tags, at a fraction of the cost of a real build.
The prototype answered the three riskiest questions:
Yes — including Greek-language thermal receipts with abbreviations and crumples. Accuracy was good enough to build on.
Yes — "you paid €2.85 for this last month at Lidl" changes purchase decisions in the aisle. The value moment was real.
Product identity. "Whole Milk 1L" vs "Full Fat Milk 1lt" vs "ΓΑΛΑ ΦΡΕΣΚΟ" — matching products across wordings, languages and stores is the actual hard problem.
That third finding shaped the entire roadmap: the moat isn't the OCR (commodity) or the UI (table stakes) — it's a clean, deduplicated, cross-store, cross-language product catalog. Most of the 47 backlog items serve that thesis.
Discovery produced dozens of candidate stories; these five were prioritised to the top of the backlog — each with explicit acceptance criteria, and each traceable to the tickets that shipped it:
As the household's primary shopper, I want to photograph my receipt and have every line captured automatically, so that I build a price history without any manual data entry.
Accepted when: a legible receipt photo — including Greek thermal receipts — produces structured line items (product, quantity, price, store, date) with no typing.
As a shopper standing in the aisle, I want to snap a shelf tag and get an instant verdict on whether the price is good, so that I can decide before the item goes in the trolley.
Accepted when: a shelf-tag photo returns a verdict against my history and the market in seconds, and the observed price is saved without being counted as a purchase.
As a returning user, I want the app to recognise the same product across stores, wordings and languages, so that my comparisons are about prices — not spelling.
Accepted when: one physical product never forks into duplicates across retailers or languages, and two different pack sizes are never silently merged.
As a budget-conscious shopper, I want to see which store is cheapest for the products I actually buy, so that I can plan where to do my weekly shop.
Accepted when: any matched product expands to a ranked cross-store price list with "cheapest" and "you paid" clearly flagged, only showing stores with real price data.
As a user who trusts the app with my data, I want protection against duplicates and wrong matches, so that one bad scan can never corrupt my price history.
Accepted when: re-saving the same receipt is blocked with a clear message, and a misread barcode is distrusted and demoted instead of silently overwriting a match.
03 · Inside the product
The product lives on the phone, in the aisle and at the kitchen table. These are the screens where it earns its keep — each one shaped by the identity and trust work described below.
All screens are illustrative recreations of the live product, with representative data.
04 · Delivery
Next.js + Supabase + Vercel. The data model was designed around a 4-level product-match hierarchy from day one — because discovery proved identity was the hard problem, the schema anticipated it before the first feature shipped.
Confidence tiers on every match (Perfect → Almost → Fuzzy), a shelf-tag flow that saves price observations without a purchase, barcode-first matching, and back-office tooling for catalog curation — built early, because a catalog you can't curate rots.
Automated importers brought in three retailer catalogs (11,000+ items processed, 1,666 competitor prices committed from the first source). Scale immediately exposed data-model gaps — multipacks, loose produce sold by weight, unit-price drift — each fixed at the database level, not patched in the UI.
Duplicate-receipt blocking, a self-reinforcing product-duplication loop root-caused and fixed, barcode misread distrust, and a 53% cut in AI cost per user. This phase was deliberately dedicated to making the data trustworthy — because a price comparison built on dirty data is worse than none.
05 · The hard problem
Every price comparison stands or falls on one question: are these two rows the same item? Get it wrong one way and the catalog forks into duplicates (four identical "Bananas", each with one price — a real bug we root-caused). Get it wrong the other way and different products merge, silently corrupting every comparison.
The governing principle I adopted, and encoded into the system as hard rules:
Barcode is evidence. Attributes are hypothesis. A valid barcode that resolves to a product outranks any amount of name/brand/pack similarity — and two products with different barcodes are never merge candidates, no matter how alike they look.
The matching pipeline this produced, with every match labelled by an honest confidence tier. When a barcode's resolved brand clashes with what's printed on the label, the system distrusts its own read and falls back — trust the barcode, but verify it:
The unsolved frontier is documented too: Greek and English catalog entries are disjoint "script islands" that only barcodes bridge ("Village Flour" ↔ "Αλεύρι Χωριάτικο"). The embedding-retrieval spec proposes multilingual embeddings as a retrieval layer — while deliberately keeping the identity rules unchanged, because recall and identity are different problems.
06 · Decisions & trade-offs
Build strategy
Trade-off: a discovery phase with nothing deployable to show for it.
Why: the riskiest assumptions (OCR viability, value moment) were testable for near-zero cost.
Result: the app was built against validated behaviour, and the hard problem (identity) was known before the schema was designed.
Data integrity
Trade-off: a UNIQUE constraint would be bulletproof; a fingerprint check can be bypassed.
Why: OCR is nondeterministic — an exact-contents key produces false negatives, and a hard constraint removes the override path legitimate edge cases need. I also rejected the "obvious" time-based key because receipts don't reliably capture time.
Result: exact duplicates blocked with a clear message; review screen warns early via the same shared fingerprint module, so warning and block can never drift apart.
Unit economics
Trade-off: a 500-row catalog dump in every scan prompt improved name reuse but was ~65% of input tokens.
Why: the database already did that matching better. But "should be fine" isn't evidence — the removal shipped gated behind a flag so it's an A/B flip, not a rewrite.
Result: $3.06 → $1.45 per user/month, on a path below $1 — the difference between a freemium tier that bleeds money and one that doesn't.
Data model
Trade-off: comparing totals is simpler; but 0.62 kg of tomatoes vs 0.48 kg of tomatoes on total price is a meaningless comparison.
Why: the comparison basis must come from what the product is (loose, sold by weight) — sourced from the matched catalog product, not from the vision model's per-line guess. Fixing that dual source of truth was its own ticket.
Result: produce prices are finally comparable across stores and trips.
Scope discipline
Trade-off: keeping a "switch to receipt/label mode" control felt safe.
Why: auto-detection had zero observed misreads. A control nobody needs is friction and surface area — and the observation data existed to prove it.
Result: one less decision for the user in an in-aisle flow where seconds matter.
Schema restraint
Trade-off: "add an origin column" was the obvious fix for Chiquita vs local bananas looking identical.
Why: origin behaves exactly like brand in matching (it differentiates price points), so a naming convention rides the existing brand-aware matching with zero migration risk.
Result: different-priced produce is distinguishable, identical produce matches — no schema change.
07 · Big data
Personal price history is useful; a market database is a product. I specified, prioritised and shipped automated importers for three retailer catalogs — 1,666 competitor prices committed from Alphamega, 9,005 items parsed from SupermarketCy, 455 from Wolt/Metro — normalised into the same product identity model the scans use.
Every importer produces a preview CSV first and writes nothing without an explicit --commit. Data pipelines that write on first run destroy catalogs.
Unit prices (€/kg, €/L, €/item) are computed by Postgres triggers — editing a product's pack size recomputes its entire price history automatically. One source of truth, zero drift.
Imported rows pass through the same barcode → alias → attribute matching as user scans. An importer that mints duplicates is worse than no importer.
Scale surfaced silent defects — a database client that quietly capped results at 1,000 rows, imports that stored pack size as purchase quantity. Each got a root-cause write-up and a structural fix, not a patch.
08 · Trust
An entire phase of the roadmap was deliberately spent on data-quality systems rather than new features — because every corrupted row silently degrades every future comparison:
Early warning on the review screen, hard block at save, one shared fingerprint module powering both.
A swallowed database error was being read as "no match", minting a duplicate product on every scan — self-reinforcing after the first accident. Found it, fixed it structurally, then built an admin duplicate finder with hard merge vetoes.
When barcode reads failed, the logs couldn't say why. Diagnostics shipped first (raw digits, rejection reasons), then the fixes — decisions from data, not anecdotes.
Badges distinguish "Barcode match" (verified) from "Exact match" (inferred). Users should never trust the system more than the system trusts itself.
09 · Where it stands
Live in production: receipt scanning, in-store price checks, cross-store price breakdowns, a curated multi-retailer catalog, and admin tooling to keep it clean. Documented and prioritised next: multilingual embedding retrieval to bridge the Greek/English catalog islands, more retailer sources, and the freemium pricing the unit-economics work was done to enable.
Just as important, the programme is built to hand over: all 47 items live in an indexed docs folder — specs with problem statements, goals and non-goals; write-ups with root causes and rejected alternatives. A new engineer or PM could pick it up from the documentation alone.
An honest read: PriceScan is in the Introduction stage — and the strategy for every later stage is already mapped, because the decisions that make Growth affordable (unit economics) and Maturity possible (a defensible data asset) had to be taken now, not later.
Focus: retention and data quality over reach — a price database users distrust is dead on arrival. North-star metric: weekly scans per active user. Free while the catalog seeds; every scan makes the product better for the next user.
Every user's scans improve everyone's comparisons — classic data network effect. Freemium launches here, which is why AI cost per user was driven from $3.06 toward <$1 before scale: a free tier that bleeds money kills you exactly when you're winning.
Consumer side: premium tier (basket planning, price alerts). Business side: the curated, cross-retailer price database becomes market intelligence no single retailer owns — valuable to FMCG brands and retailers themselves. The curation and provenance work exists to make this data sellable.
The identity engine (barcode → alias → attributes) is category-agnostic: pharmacy, pet supplies, DIY, fuel — and geography-agnostic beyond Cyprus. The renewal decision gets made at the top of the maturity curve, not on the way down.