← Back to blog

I Built an AI Wine Deal Finder — Here's What 50 Bottles Taught Me

How I built an automated wine price comparison tool for Singapore that compares Platinum Wine Club vs Grand Cru vs Vivino, using web scraping, Gemini AI with search grounding, and a self-sustaining daily pipeline.

Tech Stack
Python FastAPI Selenium Gemini AI Brave API PostgreSQL Leaflet.js Railway

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.

🇸🇬
Singapore only. This tool compares Singapore wine retailers (Platinum Wine Club and Grand Cru). Prices are in SGD. Vivino prices reflect the Singapore market where available.

Try it yourself

Head to wine.kooexperience.com. You'll see a table of ~50 wines with prices from Platinum, Grand Cru, and Vivino side by side. 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 data refreshes daily via a cron pipeline. No manual intervention. I built it once and it just runs, like a Roomba for wine deals.

The numbers

After tracking 50 wines across three sources, here's what the data says:

The takeaway: Platinum has genuinely great deals on about a third of their wines. The rest are competitively priced — sometimes identical, sometimes Grand Cru edges them out. The value of this tool is knowing which category each wine falls into before you buy.

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 (prices + labels)       │
│  └── Selenium → Grand Cru (matched wines + prices)         │
│                                                            │
│  2. MATCH                                                  │
│  ├── Fuzzy string matching (wine names across retailers)   │
│  └── Manual overrides for tricky labels                    │
│                                                            │
│  3. ENRICH (Vivino)                                        │
│  ├── Brave Search API → find Vivino URLs                   │
│  ├── Gemini AI (search grounding) → extract:               │
│  │   ├── Vivino rating + review count                      │
│  │   ├── Average price (SGD)                               │
│  │   └── Wine metadata (region, grape, style)              │
│  └── Fallback: manual CSV for wines Gemini can't resolve   │
│                                                            │
│  4. SCORE                                                  │
│  └── Compute deal score (0-100) per wine                   │
│                                                            │
│  5. SERVE                                                  │
│  ├── FastAPI → /api/wines (JSON)                           │
│  ├── PostgreSQL (Railway) for persistence                  │
│  └── Static frontend (vanilla JS + Leaflet.js)             │
└────────────────────────────────────────────────────────────┘

The scraping layer uses Selenium with headless Chrome. Both Platinum and Grand Cru render their catalogues with JavaScript, so a simple HTTP request won't cut it. Selenium waits for the page to fully render, then extracts wine names, vintages, regions, and prices from the DOM.

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 Vivino problem (and the AI 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 solution was Gemini AI with Google Search grounding.

Google's Gemini API has a feature called search grounding where you can ask Gemini a question and it will search the web in real time to find the answer. Crucially, Google's own crawlers can access Vivino just fine. So instead of scraping Vivino directly, I ask Gemini:

"What is the Vivino rating, number of reviews, and average Singapore price for Domaine Claude Dugat Gevrey-Chambertin 2021?"

Gemini searches Google, finds the Vivino page, reads it, and returns structured JSON with the rating, review count, price, and tasting notes. It works because Google's own infrastructure does the web access — not my server. Vivino trusts Google's crawlers, so the data comes through cleanly.

Technically, it's just one extra parameter in the API call: "tools": [{"google_search": {}}]. That enables "search grounding" — Gemini can search the live web before responding. The free tier gives you 500 grounded requests per day, which is more than enough for 50 wines.

To improve accuracy, I first use the Brave Search API to find the exact Vivino URL for each wine, then pass that URL as a hint to Gemini. This way Gemini looks up that specific page rather than searching broadly. If the URL hint fails, it falls back to a broad search, then tries again with a simplified wine name. Three attempts, each getting progressively less specific. The overall success rate is about 82% fully automated from a server that Vivino would normally block.

The deal score

Every wine gets a score from 0 to 100. The formula weights four components:

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 (headless Chrome)
Search:     Brave Search API (Vivino URL discovery)
AI:         Gemini 2.0 Flash (search grounding for Vivino data)
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 (all 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 1,000 queries per month. Gemini's free API handles the Vivino resolution volume comfortably. Paid services (such as Wine-Searcher's API for global price data) were evaluated but aren't justified at this scale.

Lessons learned

Work around constraints, don't fight them. When direct scraping is blocked, look for indirect paths. Gemini's search grounding accesses Vivino through Google's own infrastructure — a legitimate API feature that sidesteps the bot detection problem entirely.

Free tiers are viable at small scale. Railway, Gemini, 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

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 — serves as 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:

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.


References