Every price-tracking system starts the same way. You have a catalogue of products, a feed of retailer listings, and a function that decides which listing belongs to which product. Almost everyone writes that function as string matching, because for a while it works.
Then you try it on PC hardware, and it stops working in a way that is very hard to see.
The failure is invisible by construction
Here is the whole problem in one line:
catalogue: AMD RX 9060
listing: PowerColor Reaper Radeon RX 9060 XT 16GB
Every token of the catalogue name appears in the listing. Any matcher built on "do our words appear in their title" scores this a perfect hit. It is a different card, it costs more, and nothing in the string tells you so.
Now flip it. The RX 9060 XT sells in 8GB and 16GB versions with names that differ by two characters. If your price rule is "cheapest live listing wins" - which is the sane rule - the smaller card wins every single day, and the bigger card gets advertised at a price nobody charges for it. Your system is not broken. It is confidently, consistently wrong.
Hardware is dense with these:
| Trap | Looks like |
|---|---|
| Performance suffix | RX 9060 / RX 9060 XT, RTX 5070 / 5070 Ti |
| Capacity variant | RTX 5060 Ti 8GB / 16GB |
| Board revision | MAG Z890 Tomahawk WIFI / WIFI II |
| Colourway SKU | X870E Aorus Pro / Aorus Pro ICE |
| Channel SKU | PRIME B860M-A WIFI / WIFI-CSM |
| Form factor, hidden inside the model code | X870 / X870*I, Z890 / Z890M* |
That last row is the nastiest. X870I is a mini-ITX board and X870 is ATX. One letter, buried mid-token, and a human skims straight past it.
Memory kits hide the spec entirely
RAM deserves its own paragraph, because the identifying attribute is usually not in the title at all.
Corsair Vengeance DDR5-6400 CL32 32GB (2x16GB)
CORSAIR Vengeance 32GB (2 x 16GB) DDR5 6400 ... Model CMK32GX5M2B6400C36
Same brand, same line, same speed, same capacity, same module count. Different kit. The CAS latency is CL32 for ours and CL36 for theirs, and the only place that appears is the trailing C36 of the manufacturer part number. Match on the words and you take the wrong kit, at the wrong price, forever.
The fix is to stop matching in the hot path
The rule we settled on is one sentence:
Identity is resolved once, behind a gate, and never re-derived from names.
Concretely, that means splitting one job into two that had been tangled together:
Resolution happens rarely, is allowed to be slow and expensive, and produces a durable binding of product -> exact retailer ID. Name matching lives here and only here, wrapped in whatever gates you need. A proposal that fails any gate goes to a human instead of into the catalogue.
Pricing happens every morning and does no matching whatsoever. It asks each retailer "what is item B0F8PSH3Q9 selling for today", because we already decided months ago what that item is. There is no string comparison left in the daily path to get wrong.
The reason this works is not cleverness, it is that the expensive, error-prone step now runs a thousand times less often, and its output is reviewable. You can look at a registry of bindings and audit it. You cannot audit a matcher that re-decides on every run.
Bindings rot, so check them
A durable binding introduces its own failure: a retailer can change what sits behind an ID. A listing gets repurposed, a product page becomes its successor, an ASIN quietly turns into a different model.
So the daily fetch does one comparison after all, but it is a much weaker one. It re-reads the listing title and asks whether our model's tokens still appear in it. Not "is this the right product" - that decision is already made - but "has this listing become something else". When it drifts, the listing stops feeding prices and goes to a queue. It is a smoke alarm, not a matcher.
Then add whatever domain check actually separates your siblings. For graphics cards ours is capacity: whatever VRAM the title states must be the VRAM we list, or the binding is wrong regardless of how well the names line up. One rule, and the entire 8GB-priced-as-16GB class stops being possible.
A wrinkle worth knowing if you copy this: card titles do not write capacity consistently. 16GB, 16G, and a trailing -6GD inside a part number all mean the same thing, and a regex that only knows \d+GB will sail straight past two of the three.
What I would tell my past self
Do not try to make the matcher smarter. That is the instinct, and it is a trap - every new rule you add is another rule that fires on the wrong edge case at 5am while you are asleep.
Make the matcher rarer instead. Resolve identity once, store the exact ID, gate the promotion to "verified" behind a human, and let the daily job be boring. Boring is the goal. The daily job should be incapable of being creative.
The honest admission: this costs you coverage. Parts nobody has resolved yet sit unpriced, and there is a real temptation to auto-approve the backlog just to fill the catalogue. We have that backlog. It is better than the alternative, which is a catalogue that looks complete and lies in places you cannot predict.
We build BottleneckPC, a free CPU and GPU bottleneck calculator, and the engine behind it is open source under MIT if you want to read the honest-range scoring rather than the pricing side.
Top comments (0)