I Built an AI Wine Deal Finder — Here's What 50 Bottles Taught Me
How I built a wine price comparison tool for Singapore that compares Platinum Wine Club vs Grand Cru vs Vivino, using web scraping, Brave-powered Vivino resolution, bundle-aware pricing, and a guarded daily pipeline with local review for edge cases.
The idea — spend smarter on wine
It started with a simple question: where's the best value?
If you have an American Express Platinum card in Singapore, one of the perks is S$400 in annual wine credits — two S$200 statement credits (Jan–Jun and Jul–Dec), each triggered by a minimum S$300 spend on the Platinum Wine website. Spend $300, get $200 back. That's a solid deal on its own.
But the key question is: are Platinum's base prices competitive? If a bottle is $150 on Platinum but $100 on Grand Cru, then even with the Amex credit your effective price ($100 after credit on a $300 spend) is about the same. The credit works hardest when Platinum's prices are already competitive. I wanted to find exactly which wines give you the biggest win — great base price plus the Amex credit on top.
The obvious move is to check each wine on Grand Cru (another popular Singapore retailer) and Vivino (the global wine marketplace with community ratings). Doing this manually for one wine takes about five minutes. For 50 wines, that's over four hours of tab-switching and squinting at price labels. So I did what any reasonable engineer would do and automated the whole thing.
Try it yourself
Head to wine.kooexperience.com. The live catalogue changes as retailers add and remove stock, but the app shows the current Platinum, Grand Cru, and Vivino comparison side by side. Platinum is the anchor price: if Platinum sells a 3-bottle bundle for S$600, the app shows S$600, and Grand Cru plus Vivino are scaled to the same 3-bottle quantity before comparison. Each wine gets a deal score from 0 to 100, colour-coded so the best deals pop out immediately.
You can sort by deal score, filter by wine type (red, white, sparkling), or click into any wine to see a breakdown of why it scored the way it did. There's also a map showing where each wine comes from, because I'm a sucker for Leaflet maps and any excuse to add one.
The scrape/import path refreshes daily via cron. For tricky Vivino identity cases, I keep a guarded local review loop, because a missing price is still better than a wrong match.
The numbers
I stopped hard-coding exact catalogue stats here because they go stale quickly as retailer inventory changes. The durable patterns are:
- Platinum is genuinely cheaper on a meaningful slice of the catalogue. It is not just an Amex-credit gimmick.
- Bundle math changes rankings. Some apparent deals disappear once every competitor is scaled to Platinum's listing quantity.
- Same-price listings are common. Those are convenience plays, not true price edges.
- Grand Cru still wins on part of the catalogue. The useful part is seeing that before checkout.
- Live numbers belong in the app, not frozen in the post. The write-up should explain the system, not pretend to be a dashboard.
The takeaway: Platinum has genuinely good deals on a meaningful slice of the catalogue, but the bundle-aware math matters. The value of the tool is that it shows the listing you can actually buy, then scales the other benchmarks to match before you checkout.
How the pipeline works
The system runs as a daily cron job on Railway. Here's the flow:
┌────────────────────────────────────────────────────────────┐
│ Daily Pipeline (cron @ 06:00 SGT) │
│ │
│ 1. SCRAPE │
│ ├── Selenium → Platinum Wine Club (catalog + pricing) │
│ └── Shopify API → Grand Cru (catalog + pricing) │
│ │
│ 2. MATCH │
│ ├── Fuzzy string matching across retailers │
│ ├── Bundle detection + quantity normalization │
│ └── Platinum listing price stays as the anchor │
│ │
│ 3. BRAVE RESOLVE │
│ ├── Brave Search API → find the exact Vivino URL │
│ └── Identity cache skips known wines │
│ │
│ 4. HTML SCRAPE │
│ ├── Fetch Vivino page HTML │
│ ├── Parse rating, review count, and SGD price │
│ └── Extract grapes, region, and tasting context │
│ │
│ 5. IMPORT │
│ └── Merge sources, score deals, write PostgreSQL │
│ │
│ 6. SERVE │
│ └── FastAPI + static frontend │
└────────────────────────────────────────────────────────────┘
The high-level pipeline today is intentionally simple:
Scrape → Match → Brave Resolve → HTML Scrape → Import.
I removed the Gemini fallback after it started returning wrong-vintage, wrong-currency,
and outright fabricated prices.
The matching step is trickier than it sounds. "Chambolle-Musigny 1er Cru Les Amoureuses 2020" on one site might be listed as "Domaine Serveau Chambolle Musigny Amoureuses Premier Cru 2020" on another. Fuzzy matching with a similarity threshold of 0.75 catches most of these, but some wines need manual overrides stored in a CSV. The other subtle rule lives here too: Platinum is the pricing anchor. I show Platinum's listing price exactly as sold, then scale Grand Cru and Vivino up to that same quantity. Earlier comparison CSVs had some stale divided-per-bottle rows, which looked tidy but broke apples-to-apples comparisons.
The Vivino problem (and the boring fix)
Vivino data — ratings, review counts, average prices — is essential for the deal score to be meaningful. The challenge is that Vivino employs aggressive bot detection. Standard HTTP clients, headless browsers, and stealth-mode scrapers are all blocked from datacenter IPs.
The current resolver is deliberately boring: use the Brave Search API to find the exact Vivino URL, fetch that page's HTML, and parse the structured data directly. Rating, review count, price, grapes, region, and tasting context all come from the resolved page itself, not from a model summary.
Most daily runs skip search entirely thanks to an identity cache of validated wine-to-Vivino mappings. Once a URL is proven correct, the pipeline reuses it and goes straight to HTML extraction on the next run.
Earlier versions had a Gemini + Google Search grounding fallback for cases where Brave failed or ran out of credits. It sounded clever, but it was too risky for price data. Wrong vintages and wrong currencies were bad enough; one run returned a surreal S$35,020 Vivino price for a bottle that should have been around S$390.
So the fallback is gone. If Brave can't resolve a trustworthy page and the HTML doesn't contain parseable pricing, the system would rather show no Vivino price than invent one. Missing data is annoying. Fabricated price data destroys trust.
The deal score
Every wine gets a score from 0 to 100. The formula weights four components:
- Retailer discount (30 points): How much cheaper is Platinum compared to Grand Cru? Bigger savings = more points.
- Market discount (30 points): How much cheaper is Platinum compared to Vivino's average market price? This catches cases where both retailers are overpriced relative to the wider market.
- Vivino rating (25 points): Higher-rated wines score higher. A well-rated wine at a discount is more compelling than a mediocre wine at the same discount.
- Rating confidence (10 points): Scales with review count (logarithmic). A 4.2 backed by 10,000 reviews is more reliable than a 4.2 with 12 reviews.
- Bonus (5 points): Extra points when a wine beats both Grand Cru and Vivino by more than 5%.
The result is a single number that answers: is this wine a good deal on Platinum? Scores are colour-coded on the frontend so the best opportunities are immediately visible.
Surprising finds from the data
After staring at 50 wines for longer than is healthy, a few patterns jumped out:
Rosé has the best value. The Miraval Rosé 2024 — yes, the one from Brad Pitt and Angelina Jolie's Provence estate, a.k.a. the world's most famous divorce wine — is consistently cheaper on Platinum than anywhere else. With 58,336 Vivino reviews and a solid rating, it's the highest-confidence deal in the dataset.
Champagne is mostly the same price everywhere. The big Champagne houses (Moët, Veuve Clicquot, etc.) have tight distribution agreements. Price differences across retailers are usually under $5. Not much to optimise here.
Italian wines need checking. The biggest price variances showed up in Italian bottles — Barolo, Barbaresco, Gattinara. Some were great deals on Platinum; others were significantly overpriced. Italian wine pricing in Singapore appears to be all over the place.
Burgundy is where the real deals hide. The Gevrey-Chambertin by Domaine Claude Dugat at 83% cheaper is the extreme case, but several other Burgundies showed 20-40% savings. These tend to be smaller producers where Platinum's direct import relationships actually matter.
The stack
Backend: Python 3.12 + FastAPI + Uvicorn
Scraping: Selenium + Shopify catalog API
Resolve: Brave Search API + identity cache
Parsing: Vivino HTML + JSON-LD extraction
Database: PostgreSQL (Railway managed)
Frontend: Vanilla JS + Leaflet.js (wine origin map)
Hosting: Railway (backend) + static frontend
Scheduling: Railway cron (daily 06:00 SGT)
Cost: ~$0/month (free tiers)
As a side project, keeping costs at zero was a constraint from the start. Railway's free plan covers the backend and PostgreSQL. Brave Search's free tier provides enough URL discovery for the current catalogue, and the identity cache keeps repeat lookups low. Removing the Gemini fallback also removed a whole class of bad outputs. Paid services (such as Wine-Searcher's API for global price data) were evaluated but aren't justified at this scale.
Lessons learned
Prefer deterministic extraction over clever fallbacks. When you're showing user-facing prices, boring beats magical. Brave + HTML parsing is less flashy than an LLM fallback, but it's auditable, reproducible, and much easier to debug.
Listing semantics matter as much as the raw price. If one retailer sells a 3-bottle bundle and another sells singles, per-bottle math will quietly lie to users. Showing Platinum's listing price as-is, then scaling the comparison set to match, turned out to be the only convention that felt honest in the UI.
Free tiers are viable at small scale. Railway and Brave's free tiers handle this workload comfortably. The identity cache keeps API usage well within limits even as the wine catalogue grows. Paid APIs become necessary only at significantly larger scale.
Data accuracy is the product. The Wine-Searcher experiment reinforced this. Wrong prices erode trust faster than missing prices. Every data point shown to users should be verifiable against its source.
Build for a real use case. This tool exists because I use it before every wine purchase. Solving your own problem produces better design decisions than building for hypothetical users.
What's next
- More retailers: Wine Connection, 1855 The Bottle Shop, and other Singapore wine stores.
- Alert system: Get notified when a high-rated wine drops below a price threshold.
- Better Vivino coverage: Push that automation rate higher. Some wines just need better search queries or alternative data sources.
Update: Pricing & resolver hardening
A few resolver changes shipped in late March 2026.
- Bundle-aware pricing is now explicit. Platinum's listing price is the anchor shown as-is. If Platinum sells a 3-bottle bundle for S$600, the app shows S$600. Grand Cru and Vivino are scaled to the same 3-bottle quantity before comparison. I regenerated the comparison CSV after finding stale rows that had quietly divided bundles back down to per-bottle figures.
- Gemini grounding is out of the resolver path. When Brave Search ran out of credits, the old fallback used Gemini with Google Search grounding. It occasionally hallucinated absurd Vivino prices, including one S$35,020 quote for a wine that should have been around S$390. That fallback is gone.
- AI review moved to the local operator loop, not the runtime path. I use local coding agents such as Codex or Claude Code as review assistants for suspicious resolver output, candidate-page checks, and override diffs before anything is pushed. They help with verification; they do not write production prices directly.
The architecture today is Scrape → Match → Brave Resolve → HTML Scrape → Import,
plus a guarded local review loop for edge cases. In other words: automate the boring parts,
review the risky parts. Missing data is acceptable. Fabricated price data isn't.
Update: Global market prices — what worked and what didn't
After launch, I explored adding Wine-Searcher's global average as a fourth price reference. The intent was to give users a broader market benchmark alongside the Platinum, Grand Cru, and Vivino prices. The implementation revealed two data integrity challenges worth documenting.
The first was wine identity matching. Automated search-based resolution (via Brave Search API) frequently returned incorrect Wine-Searcher pages for wines with similar names. A village-level Gevrey-Chambertin (~S$158) was matched to the Premier Cru (~S$375) from the same producer. Wines from "Hudelot-Baillet" were confused with "Hudelot-Noellat" — separate domaines that share a surname. Full item-by-item verification flagged 17 out of 54 wines with incorrect matches.
The second was regional pricing discrepancies. Wine-Searcher computes separate averages per region, drawing from different retailer pools. Their USD average (which Brave indexes) and SGD average (shown to Singapore visitors) can differ meaningfully — roughly 5% for widely distributed wines, but up to 37% for wines with significant US import premiums. Unfortunately, the Brave Search API does not currently support Singapore as a target region, so there is no way to retrieve SGD-denominated results programmatically. A direct USD-to-SGD conversion does not produce the same figures users see on Wine-Searcher's Singapore page.
Alternative approaches were evaluated: headless browser scraping (blocked by Wine-Searcher's PerimeterX protection), the Scrapling framework (also blocked), and Wine-Searcher's own API (paid, not cost-effective at this scale). None produced reliable SGD prices in a fully automated pipeline.
The decision was to remove market prices rather than display approximate figures. The Vivino price — already sourced in SGD through the existing pipeline — is the market reference for 35 of 54 wines. For the remaining wines where Vivino has no listed price, no market reference is shown.
The exercise did produce lasting infrastructure improvements:
- A URL validation layer that checks resolved URLs against the wine's identity (producer, vineyard name, classification level) before data reaches production.
- A permanent identity cache for validated wine-to-URL mappings, reducing redundant API calls by approximately 84% across the resolver pipeline.
- A resolve → validate → retry workflow where failed matches trigger progressively tighter searches, and persistent failures are flagged rather than silently accepted.
If you made it this far, thanks for reading. And if you're a wine drinker in Singapore, go check wine.kooexperience.com before your next purchase. Your wallet might thank you. Or you'll discover that your favourite bottle was overpriced all along, in which case, sorry about that.
Questions, suggestions, or wine recommendations? Reach out. I'm always looking for new bottles to add to the tracker.