Dupe Detective
Dupe Detective is an app I am building for my sister, Tilly (@amillennialsaver), a money-saving creator with 100k followers. The idea is you give it a product and it finds the cheaper versions or 'dupes', without sacrificing too much on quality.
It looks like a similarity-search problem, and it is, but the catch is that 'similar' isn't one function. The perennial issue is that it's not good enough to simply surface things that look similar; for this to be valuable you need to go deep. You need to understand the materials that make up the product, the craftsmanship etc. Sometimes you are paying a premium for brand — a $5000 Bottega Veneta bag clearly does not have $5000 of materials and labour, but that isn't to say that showing the user a $100 pleather alternative is a good buy either. There's a middle ground.
By the end of this hobby project I realised I was competing with Series B funded companies. I also feel I have built something that is, at least on the accuracy dimensions, better. I guess that is all startup building is, finding one dimension where the competitive landscape is beatable.
The library: entries Tilly has actually bought and compared
The one rule
Fairly early on I worked out that the failure mode of AI shopping tools isn't that the model is stupid, it's that it's confident. Ask Claude what a bag is made of and it will tell you something plausible, which is worse than nothing when someone's about to spend money on the answer. It's the thing you need to keep front and centre whenever working with AI: in the vast majority of cases you are asking a probabilistic model for a deterministic answer.
So the app has one rule that everything else runs off: the model is never the source of a product fact. If a material, a price or an ingredient wasn't retrieved from somewhere real — a retailer's page, a search result, a reviews database — it doesn't get shown. Unknowns are displayed as unknowns. To be honest, this product is more an API problem than an AI problem.
In practice that rule splits the app into two planes. Everything below the line is deterministic: search APIs, scrapers, price recovery, filters — code that gathers evidence, and can be cached, replayed and tested. Everything above the line is probabilistic: model calls that read, rank and judge, but only over the evidence they're handed. The discipline is keeping facts flowing strictly upwards. The model is allowed to say that a candidate's claimed polyester makes it a poor match for leather; it is never allowed to be the one who says it's polyester.
*Fig. 1 — Hypotheses go down and get tested; only evidence comes back up*How a lookup works
A lookup goes through five stages: identify the product, check the curated library, search the live web, check the facts, score and judge. Haiku does the cheap reading jobs, Sonnet does the judging. Here's each bit.
Fig. 2 — The five stages of a lookup
Identifying the thing
The first model call turns your query or photo into a profile of abstractions: brand, type, colour, materials, plus two decisions that shape everything downstream. One is which single dimension matters most — the look, the scent, the formula, the material, or the function. The other is to what extent the product is "abundant" or "distinctive". A designer hand wash is abundant; what matters is what's in the bottle, so it gets judged on contents. A cutwork leather bag is distinctive, so it gets judged by eye, including a reverse-image search on the original's photo.
For distinctive products it also writes a checklist of physical details to compare, and this produced one of my favourite lessons. For a pair of Common Projects it wrote "serial number on tongue" and "gold-tone side stamp". An honest dupe is never going to carry the brand's own markings, so every single comparison was rigged before it started. Brand identifiers are now banned from checklists in the prompt, and stripped again in code, because the prompt alone didn't reliably hold.
Finding candidates
Early on in the process it was clear cost was becoming prohibitive. I was running SerpAPI for reverse image searches and shopping data. Each dupe was running perhaps 20 of these searches. After some research I found Serper (ah, the imagination of devtools branding). Serper was 10x cheaper but had no reverse image API. I decided to run on two providers: Serper for volume, and SerpAPI for the things it alone can do (reverse-image search and buyer reviews). The highest-value query isn't written by the model at all. It's built in code: the brand, the product name, and the word "dupe". I originally let the model suggest all the queries and it kept being too creative; the boring query is the one that finds things.
There's also a step that mines press coverage. This exists because I once asked Haiku from memory what dupes existed for an Elemis balm and it suggested Dolce & Gabbana, while never mentioning No7, which the beauty press crowned as the dupe years ago. The model's memory is patchy in exactly the way you can't predict, so instead the app searches for dupe articles, pulls the product names out, and ranks them by how many different articles mention them. A name that appears everywhere is the community's answer. Then each name gets verified against live listings, so a hallucinated product matches nothing and quietly disappears.
Candidates get filtered before anyone judges them: the saving has to be at least 30%, the original brand is excluded, and marketplaces, resale sites and charity shops are banned outright (more on why below).
Fig. 3 — Candidates are filtered before anyone judges them
Checking the facts
Each surviving candidate then gets a final check: what is this actually made of, what does it actually cost. There's a strict order of where facts are allowed to come from: the retailer's own page, then Google's quoted snippets of pages that block bots, then open ingredient databases, and if all of that fails the card just says unverified. Every card carries a little line, "per retailer's claim, checked 4 Aug", so you know where a number came from and when.
Fig. 4 — Where a fact is allowed to come from, in order
Scoring
At some point in every AI project, you are probably going to have to take a leap of faith, and put your trust in the black box that is the AI. We've reached that step.
Armed with all the data we've collected deterministically — materials, crowd consensus, price, reviews — one big Sonnet call judges the whole shortlist at once, with the original's photo and each candidate's photo attached. One call is deliberate: judged together, every score is calibrated against its rivals rather than drifting card by card, and it's a fraction of the cost of judging candidates one at a time. It produces the three bars you see on a card, and each one has different rules about what it's allowed to know:
Looks is scored from the images. A colour mismatch caps it at 55, which is a rule that exists because testers kept saying, reasonably, "I searched for a black one, why is this pink."
Materials is scored from the retailer's claims, and this has my favourite rule in the app. The instinct is to discount the score because claims are unverified. That turns out to be backwards. If a candidate claims polyester and the original is leather, that's strong evidence — nobody lies in the direction that loses them sales — so score it low with confidence. If the claims match, take it at face value and let the "per retailer's claim" label carry the uncertainty. Discounting the number just muddies it; the caveat belongs next to the score, not inside it.
Quality is the strictest: it stays empty unless the app finds at least two separate buyer reviews talking about build quality or durability, because a product listing cannot tell you how something holds up. When there's no quality evidence, the third bar becomes "Value" instead, a blend of the look and the saving, with a label saying exactly that. I'd rather show a smaller true thing than a bigger guess.
Fig. 5 — The three bars, and what each is allowed to know
Scores are 0 to 100, and the prompt spells that out with anchors, because in an early version the model quietly decided scores were out of 10 and gave two identical cotton throws "materials: 4".
To be honest, this is probably the part of the app I'd be curious to build out more. I think if you ever raised cash or went full bore on a project like this, building out some proprietary scoring would be a good little moat.
The verdict
Last comes the editorial verdict — is the real one worth it — and it deliberately runs after the search so it can see what the search found. In an early version it ran independently, and it once declared a pair of trainers "worth full price" on the strength of some invented waffle about leather quality, directly above a full-grain alternative at £190 that the app itself had just found. Now, if candidates claim the same materials at a fraction of the price, the verdict isn't allowed to rest on quality. It has to say what the extra money actually buys, and if that's mostly the name, it has to say so plainly.
Fig. 6 — One lookup, end to end: the press knew the answer; the app checked it
What counts as a dupe
With the scores explained, the classification is simple. A candidate is a true dupe only if materials and quality both hit 70 or more. Those are the only ones that can carry the "Detective-Approved" badge, and only if they've been through the review queue. Everything else is a lookalike: the badge reads "Looks for less", the tradeoffs line is always visible, and the word "dupe" doesn't appear on the card at all. There's a test that checks for the word's absence, which I think is a reasonable way to encode a promise.
There's a third tier going the other way — trade-ups, properly-made alternatives in the real materials, for when the honest answer is "spend a bit more on the version that isn't pretending".
Fig. 7 — The three tiers a candidate can land in
A full lookup: the verdict, then each candidate with its bars
Things that broke
The £154 Brunello Cucinelli. To work out the saving, the app has to recover the original's price from search results, and loose matching let Vestiaire's used listings drag a £4,000 jacket's reference price down to £154.50, at which point every candidate looked like a rip-off. The reference price is now the median of the brand's own listings, not the maximum, because special editions inflate the ceiling (one £925 bag has a £1,452 watermelon edition), and resale sites are excluded entirely.
Secondhand slipped through me twice. The filter caught "secondhand" and "second-hand", then Oxfam wrote "Second Hand" with a space. Fixed that, and Oxfam came back with a vintage Schott jacket that had no secondhand wording anywhere in the listing. You can't regex your way out of a problem like that, so there's now a flat list of retailers that are never candidates, whatever their listings say.
The judging call kept returning zero cards for distinctive products and it took a while to find out why. We were passing image URLs and letting Anthropic fetch them, but their fetcher respects robots.txt, so one bot-hostile retailer anywhere in the batch made the whole call fail. The app now downloads every image itself and sends the bytes directly, with a timeout and size limit per image so one bad host can't stall the lot.
The worst one wasn't a model problem at all. The search API's quota ran out, and for 10 hours the app dressed up empty search results as a confident editorial verdict: "no real dupe exists". A dead API and a genuine no-result looked identical from the inside. Now an all-errored search is treated as a fault, the page says "that's our fault, not a verdict", and the editorial verdict is suppressed entirely, because an opinion sitting next to zero results reads as absurd as it is.
And one for anyone deploying long AI calls on Vercel: the standard settings kill connections that send no bytes in a minute, regardless of your function timeout. We watched a request die at 60 seconds while the function behind it completed fine at 100. I manually changed these settings to 300s. The result now streams progress events with a heartbeat every 15 seconds, which had a nice side effect: every stage is timestamped, so I can see that judging is where the time goes, 25 to 36 seconds of a cold run.
The taste document
The editorial taste document is forty-seven lines distilled from 62 dupe verdicts Tilly published on Instagram over five months — I guess this was the training data — how she defines "similar" per category, which budget brands she trusts, hard lines like no counterfeits ever. It lives in the database, versioned, editable from the admin panel, and gets injected into every judging and verdict prompt. Change the document and the app's taste changes, no deploy needed. In effect the system prompt is split in two: the mechanics live in code, the taste lives in the database as versioned data — which is where I'd argue most of a product like this actually lives. Every time Tilly creates a reel manually, the taste doc gets updated. This has led to some interesting discussions between my sister and I on what she classifies as a 'dupe', because every manual dupe uploaded directly affects the app.
The writing side has one rule I'd recommend to anyone building with AI: the copy talks to the buyer, never about the machine. Nothing a shopper reads says what the app fetched or couldn't reach; an unknown is mentioned once, as a thing for the buyer to check in person. All the machine-talk goes to a collapsed log at the bottom of the page, labelled "what we couldn't reach on this run", for anyone who wants it.
Testing without going broke
Two layers. The first is a set of replay tests against recorded search results, no API calls at all, and the test file opens with the honest sentence: every invariant here is a bug we actually shipped once. One test literally asserts that the recovered Bottega price is above £1,500, because the day it isn't, junk has leaked into price recovery again. The second layer replays four real lookups through the real pipeline against cached search data, which costs pence, and asserts things that should always hold rather than exact scores, because judge numbers wobble a few points between runs and that's fine.
Beyond that, the versions were driven by three testers rating cards inside the app. Their loudest complaint became the colour rule; the flattest bug they caught was a primer offered as a dupe for a foundation.
The library itself is small on purpose, 62 entries, each one a product Tilly actually bought and compared, because generated product data is banned by the spec's most senior rule. Growing it is editorial work, not engineering, which I think is the right way round.