Investigation

SmartCart

Comparing grocery prices across apps is a basket problem, not a search problem.

Category
Product
Status
Shipped, MVP
Role
Solo builder
Tech stack
Next.js (App Router), TypeScript, Gemini API, Tailwind CSS
Timeline
[PLACEHOLDER: total weeks/months from first commit to now]
Scope
4 platforms, 5000+ SKUs, <5s comparison target
Read time
9 min

Context

Urban shoppers increasingly split groceries across 2 to 4 quick-commerce apps because no single platform is reliably cheapest or fastest across every category. The result is a manual process that doesn't scale: opening three or four apps, searching the same item repeatedly, and mentally tracking which app had what price, a process that gets abandoned for convenience well before a full weekly shop is actually optimized.

None of the platforms themselves will build a cross-comparison tool, that's a structural conflict of interest. General price-comparison sites are built for one-off purchases, not recurring multi-item baskets. No existing product treats the basket, not the individual item, as the unit of comparison, or reasons about the real convenience cost of splitting an order across checkouts.

The actual decision a user is making isn't 'what's the cheapest price for this item.' It's 'given my whole list, where should I buy each thing, and is splitting my order actually worth it.' SmartCart is built around that decision, not around single-item search.

Decisions

  1. We assumed

    Building against live scraped pricing data from day one would produce the most realistic, credible comparison.

  2. We found

    None of the four platforms offer a public pricing API, and scraping at production scale carries real legal and reliability risk, risk that shouldn't be taken on before the core comparison and allocation logic is even validated. Building against live data first would also couple two unresolved risks together, data reliability and logic correctness, making bugs from either one hard to isolate from the other.

  3. Therefore, we changed

    Built a static, realistic mock catalog first, so the actual hard problem, matching free-text items to SKUs and reasoning about tradeoffs, could be fully tested against stable, known-good data. The data source is an intentionally swappable seam: a future live-data layer only needs to satisfy the same `Product[]` shape, without touching the prompt construction, pre-filter, or any UI component.

  1. We assumed

    A deterministic matcher, exact or fuzzy string matching, could handle turning a user's free-text list into catalog SKUs.

  2. We found

    Deterministic matching can't reliably do two things this product depends on: mapping colloquial input like 'curd' or 'milk' to specific branded SKUs, and reasoning about tradeoffs like whether a ₹3 price difference is worth sacrificing 10 minutes of delivery speed.

  3. Therefore, we changed

    Delegated matching and allocation reasoning to Gemini, operating over a small, pre-filtered, pre-validated candidate set rather than the full catalog, so the model can only choose among items already confirmed to exist, reducing the risk of it inventing a plausible but nonexistent product.

  1. We assumed

    It's acceptable for an item the matching logic can't resolve to simply not appear in the result.

  2. We found

    A user who typed an item and got no visible result had no way to tell whether the app failed silently or the item genuinely wasn't in the catalog, a trust failure, not a minor bug. This was observed directly during development when compound item names like 'Redbull' failed to match anything.

  3. Therefore, we changed

    Built a structural, not a patched, guarantee: every submitted item is either matched, substituted from a curated fallback list, or explicitly surfaced in an `unmatchedItems` section in the user's own words, never silently dropped. Fixed the root matching gap too, adding whitespace-and-punctuation-normalized matching alongside word-level matching.

  1. We assumed

    Saved lists and accounts would make the product feel more complete.

  2. We found

    Usage is bursty and infrequent, a weekly grocery comparison, not a daily habit, so the drop-off cost of an account wall outweighed the value of persistence, and no per-user server-side state exists at all in v1.

  3. Therefore, we changed

    Shipped fully stateless and no-auth. Authentication and saved lists moved to the future roadmap, to be revisited only if usage patterns change.

The product

SmartCart is a single-page tool: paste a free-text grocery list, and it returns a full basket allocation, which items to buy on which platform, total cost, savings versus sticking to one app, fastest combined delivery time, and an explicit callout when splitting the order across multiple checkouts is required to get that price.

The empty state. Paste a list, nothing more, while a rotating live preview shows a real catalog item, price, and ETA.

Request lifecycle

The allocation logic itself follows a fixed set of rules: for each matched item, filter to platforms where it's actually available, take the lowest price, and if two available platforms are within ₹3 of each other, prefer the faster one. ₹3 was chosen deliberately as a flat, absolute threshold rather than a percentage, so the tie-break means the same thing whether the item is ₹15 or ₹900. If nothing is available anywhere, the item's curated substitute list is tried next. If that fails too, the item is surfaced by name in a dedicated 'not found' section, never silently dropped.

Splitting an order across platforms is never presented as a free win. The UI explicitly states either 'Requires N separate checkouts' or 'Single checkout', styled distinctly, so the real convenience cost of chasing the lowest price is always visible, not buried as a neutral stat.

Design evolution

The final interface wasn't the first attempt. It went through a structured, section-by-section critique process, treating each round as if reviewing a junior designer's submission, naming specific problems rather than iterating on vague 'make it look better' feedback.

From generic MVP to an ownable interface

  1. Round 1
    Functional MVPA plain, unstyled single page validating the technical pipeline end to end, functional, not designed.
  2. Round 2
    First visual passStat cards and a gradient CTA made the page look more finished, but introduced a generic purple-to-blue gradient used identically for logo, headline, and button, meaning nothing specific.
  3. Round 3
    Structured critiqueA deliberate critique named ten specific problems: a templated squircle logo, one color doing three unrelated jobs, badge overuse across six unrelated components, and a layout that buried the actual comparison result beneath marketing copy.
  4. Round 4-5
    Applying the fixesReplaced the gradient with real platform brand colors doing the grouping work, reduced badge overuse, compacted the hero, and swapped in the actual Blinkit, Zepto, Instamart, and Amazon logo files once available.
  5. Round 6
    Research passA deeper critique researched Google Flights, Linear, Stripe, and Apple's HIG. Adopted one accent color for exactly one job, right-aligned monetary values, and a signature connector-line pattern unique to SmartCart's result rows.
  6. Round 7
    The 'too empty' correctionThe restrained version felt hollow next to references like Stripe's marketing page. Restraint had been over-applied. Added back a bold, ownable gradient mesh and a real floating product-preview card stack.
  7. Round 8
    Motion and physical detailFinal polish: a genuine card-stack animation, hover-lift on the primary button, and a full-viewport gradient tinted toward whichever platform wins the comparison.
Before. Input-focused empty state, minimal, waiting for a list.
After. The moment a comparison completes, the input collapses to a single-line summary and the result becomes the dominant element on the page.

The interface is built around a Google Flights-inspired state model: an intentionally minimal, input-focused empty state that reorganizes into a results-dominant view the instant a comparison completes, rather than showing both states at once or burying the result beneath a static layout.

Principles

The comparison is the product, so the result must be the visually dominant element, not a footnote beneath marketing copy. AI should disappear into the workflow, invoked once and silently, never surfaced as a chat interface. Users optimize baskets, not individual items, so the total and the savings lead, not a per-item price list. And trust is earned structurally: never silently drop a user's input, and always make the real tradeoffs, like a multi-checkout split, visible rather than neutral.

Results

00Platforms compared
00+SKUsCatalog size
00%Item coverage
00<sComparison time target
Pre-filter scalingConstant per-request reasoning load

As the catalog grows, the LLM never sees the full catalog, only a small, plausibly-relevant candidate set, so per-request cost and latency stay roughly flat instead of scaling with catalog size.

Source: Local, deterministic pre-filter (lib/gemini.ts) run before every Gemini call

Constraints

SmartCart v1 is intentionally stateless and no-auth, with no saved lists or history, since no per-user server-side state exists at all. Pricing comes from a static, realistic mock catalog rather than live or scraped data, since none of the four platforms expose a public pricing API and production-scale scraping carries real legal and reliability risk. There's no location-awareness, no checkout or cart integration, SmartCart recommends, it never transacts, and no price-per-unit normalization across differing pack sizes yet, though the data model already supports adding it without a schema change.

Learnings

Treating design critique as a distinct, named step, not a vague pass, produced the single biggest quality jump in the project. Specific, named problems produce specific, targeted fixes. Vague 'make it look better' feedback produces vague changes. The Round 7 correction also matters as its own lesson: restraint and emptiness are not the same thing, and a design process needs to actively check for both over-decoration and over-restraint, not assume removing decoration automatically produces good design.

External dependencies turned out to be less stable than assumed. The project moved through three different Gemini model identifiers as earlier ones were deprecated mid-build, each requiring a live debugging session from a raw deprecation error, alongside a Tailwind v3-to-v4 config mismatch between local and deployed environments. Centralizing the shared TypeScript contract between the API, the AI prompt layer, and the UI paid off directly here: whenever the result shape needed to change, the compiler surfaced every place that needed updating, rather than the mismatch surfacing later as a runtime bug.

Notes

  1. Catalog and comparison logic run against a static, realistic mock dataset, not live platform pricing, due to the absence of public pricing APIs and the legal/reliability risk of production-scale scraping. See Constraints.

Related