Product case study

Do you know if you're overpaying for groceries?
Most people in Cyprus don't.

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.

47ticketed items, all documented
65+production releases
11,000+market items processed
$3.06→$1.45AI cost / user / month
2languages matched (EL/EN)

01 · The problem

Grocery prices in Cyprus are opaque by design

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).

The core value moment: a verdict in the aisle, in seconds.
(Illustrative recreation — representative data.)

02 · Discovery

Prototype before product: validate with the cheapest possible build

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:

Can AI vision read real receipts?

Yes — including Greek-language thermal receipts with abbreviations and crumples. Accuracy was good enough to build on.

Is the verdict useful?

Yes — "you paid €2.85 for this last month at Lidl" changes purchase decisions in the aisle. The value moment was real.

Where does it break?

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.

The five user stories that framed the MVP

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

Three moments that carry the experience

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.

Receipt review: every line matched, every match labelled with honest confidence.
Cross-store breakdown: one tap from any matched product to the whole market.
The duplicate guard: dirty data is refused before it can skew a single comparison.

All screens are illustrative recreations of the live product, with representative data.

04 · Delivery

Foundations first, trust last: four phases

05 · The hard problem

Product identity: when is "the same product" the same product?

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.
Identity model — encoded as hard vetoes in the matching engine and the admin merge tooling

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:

1 · Barcode GTIN, check-digit verified 2 · Learned aliases each retailer's own wording 3 · Attribute scoring name · brand · pack · unit 4 · User confirms honest confidence badges strongest evidence weakest evidence Hard veto: a candidate whose own barcode conflicts with the scanned one can never rank as a match — no matter how similar it looks.
The identity pipeline. Each stage only runs if the stronger evidence above it produced nothing.

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

Six decisions that show how I work

Build strategy

Prototype as an AI skill before building the app

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

Duplicate receipts: app-level fingerprint, not a DB constraint

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

Cut AI cost 53% — behind an A/B flag, not a leap of faith

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.

$3.06 $1.45 <$1 before shipped target

Data model

Loose produce compares on €/kg, not receipt total

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

Shipping a removal: the manual mode switch

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

Origin-in-brand convention instead of a new column

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

From one user's receipts to a market-wide database

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.

Receipt photo purchases Shelf tag / barcode observations Retailer catalogs ×3 11,000+ items AI vision OCR Greek + English Import pipeline dry-run by default Matching engine barcode → alias → attributes Postgres · €/unit via triggers In-store verdict buy / wait, in seconds Cross-store comparison cheapest · you paid · avg Admin curation merge · dedupe · provenance
One identity model, two directions: user scans and bulk imports both flow through the same matching engine into the same database.

Dry-run by default

Every importer produces a preview CSV first and writes nothing without an explicit --commit. Data pipelines that write on first run destroy catalogs.

Normalise at the database, not the UI

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.

Respect the identity model

Imported rows pass through the same barcode → alias → attribute matching as user scans. An importer that mints duplicates is worse than no importer.

Learn from failure at scale

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

A price comparison is only as good as its worst data

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:

Duplicate-receipt defence in depth

Early warning on the review screen, hard block at save, one shared fingerprint module powering both.

Root-caused duplication loop

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.

Instrumentation before optimisation

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.

Honest confidence language

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

Outcomes and what's next

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.

Where PriceScan sits on the product lifecycle

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.

VALUE / USERS TIME renewal: new verticals PriceScan is here INTRODUCTION GROWTH MATURITY DECLINE
The lifecycle isn't something that happens to a product — each stage is prepared for one stage earlier.

Introduction — earn trust, seed the asset

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.

Growth — let network effects compound

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.

Maturity — monetise both sides

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.

Renewal — extend before decline

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.