Every November the same thing happens: a "-50%" badge on a product that cost the same three weeks ago. In the EU this is supposed to be impossible. Since 2022, the Omnibus Directive (2019/2161) says the crossed-out "old price" of a sale must be the lowest price the shop charged in the previous 30 days.
Shoppers have no easy way to check that. So I built truediscount, a small open-source extension for Chrome and Firefox that does the check right on the product page.
What it shows
- 🟢 Real discount: the old price matches the 30-day low
- 🔴 Inflated discount: the product was cheaper recently, so "-50%" is overstated
- 🟠 Cheaper recently: today's price is above the 30-day low
- A note when the crossed-out price is only the manufacturer's RRP (not covered by the 30-day rule)
- A mini price graph
Everything stays in storage.local. No account, no analytics, no network requests.
The cold-start problem, and the trick that fixes it
A price-history tool is useless on day one: you have no history. v0.1 had exactly that problem.
But the law helps here. Many EU shops now print their own disclosure next to a sale:
Niedrigster Preis der letzten 30 Tage: 79,99 €
Prix le plus bas des 30 derniers jours : 79,99 €
Najniższa cena z 30 dni przed obniżką: 349,00 zł
If the shop discloses a 30-day low of €79.99 and crosses out €159.99, the "-50%" is inflated by the shop's own numbers. Since v0.3, truediscount reads these labels in about 15 languages, so you get a verdict on the very first visit:
When your own browser has seen a lower price, that wins.
Finding the label without killing performance
Product pages can have tens of thousands of nodes. Calling textContent on every element is quadratic. Instead I walk text nodes only, keep the ones that contain a "lowest" word, and climb at most three levels to the smallest element that holds both the label and a "30":
const LOW_WORD = /(lowest|niedrigste|tiefstpreis|le plus bas|più basso|más bajo|laagste|najniższa|cel mai mic|najniža)/iu;
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
if (!LOW_WORD.test(node.nodeValue)) continue;
let el = node.parentElement;
// climb until the element contains the whole "... 30 days: 79,99 €" label
...
}
The annoying bug: the "30" in "30 days" is itself a number, so a naive price parser happily returns €30. The fix is to strip 30 Tage / 30 days / 30 derniers jours / 30 dni ... before parsing, and to sanity-check the value against the current price. "30 Tage Rückgaberecht" (30-day returns) is in the tests for exactly this reason.
Parsing European prices is its own hobby
1.299,99 €, €1,299.99, 19,-, 1 299,00 zł, and MediaMarkt's UVP 299,– €299,00€ (label, dash notation and a second copy, all in one element). The parser takes the first price-looking token and decides decimal vs thousands separator from position and digit count. A few dozen test cases keep it honest.
Extraction order
- schema.org
Productin JSON-LD, including nested ones (BuyAction.object,ProductGroup.hasVariant) andStrikethroughPrice/MSRPprice specifications - Open Graph / microdata price tags
- Small per-shop adapters (Amazon, Zalando, MediaMarkt, Otto, eMAG, Allegro, bol.com)
- Generic heuristics:
<del>,line-through, and labelled prices like "statt", "UVP", "van", "PRP"
Tests run with node:test + linkedom on fixtures trimmed from real product pages.
MV3 notes
- Content scripts can't be ES modules, so the build concatenates the core modules into one classic script (no bundler, nothing for store reviewers to puzzle over).
- The badge lives in a closed Shadow DOM so shop CSS can't break it.
- Explicit host permissions for supported shops; any other shop is an optional permission requested only when you click "Enable on this site".
Closing the loop: Report a problem
Shop markup changes all the time, so v0.4 adds a Report a problem button to the popup. It opens a prefilled GitHub issue with only the shop's domain, the extension version and what was detected (price, crossed-out price, 30-day label). It doesn't include the full URL or any price history, and nothing is sent until you submit the form yourself. The plan is to turn each report into a test fixture.
If a shop near you shows a 30-day label that truediscount misses, that button is the most useful thing you can click. Translations for more EU languages are very welcome too.
Try it
- Website: https://emirmrkaljevic.github.io/truediscount/
- Code (MIT): https://github.com/emirmrkaljevic/truediscount
- Latest release, v0.4.0 (Chrome + Firefox zips): https://github.com/emirmrkaljevic/truediscount/releases/latest
This article was written with AI assistance. The code, tests and examples come from the repo.


Top comments (0)