DEV Community

Cover image for Catching fake Black Friday discounts with a 400-line browser extension (and the EU's 30-day rule)
Emir Mrkaljević
Emir Mrkaljević

Posted on Originally published at emirmrkaljevic.github.io

Catching fake Black Friday discounts with a 400-line browser extension (and the EU's 30-day rule)

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.

truediscount badge flagging an inflated discount

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:

First visit verdict

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
  ...
}
Enter fullscreen mode Exit fullscreen mode

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

  1. schema.org Product in JSON-LD, including nested ones (BuyAction.object, ProductGroup.hasVariant) and StrikethroughPrice / MSRP price specifications
  2. Open Graph / microdata price tags
  3. Small per-shop adapters (Amazon, Zalando, MediaMarkt, Otto, eMAG, Allegro, bol.com)
  4. 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


This article was written with AI assistance. The code, tests and examples come from the repo.

Top comments (0)