<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: carlos lopez</title>
    <description>The latest articles on DEV Community by carlos lopez (@carlos_lopez_e0907403c1b4).</description>
    <link>https://dev.to/carlos_lopez_e0907403c1b4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3989274%2Fbfee9a09-766d-4f0e-84af-f3619943a266.png</url>
      <title>DEV Community: carlos lopez</title>
      <link>https://dev.to/carlos_lopez_e0907403c1b4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlos_lopez_e0907403c1b4"/>
    <language>en</language>
    <item>
      <title>I built a Chrome extension that catches every dark pattern trick on shopping sites. Here's exactly how.</title>
      <dc:creator>carlos lopez</dc:creator>
      <pubDate>Fri, 19 Jun 2026 21:08:32 +0000</pubDate>
      <link>https://dev.to/carlos_lopez_e0907403c1b4/i-built-a-chrome-extension-that-catches-every-dark-pattern-trick-on-shopping-sites-heres-exactly-9ef</link>
      <guid>https://dev.to/carlos_lopez_e0907403c1b4/i-built-a-chrome-extension-that-catches-every-dark-pattern-trick-on-shopping-sites-heres-exactly-9ef</guid>
      <description>&lt;p&gt;A few months ago I was about to buy a flight. The page showed "Only 2 seats left at this price" in red letters. I hesitated, then refreshed the page out of curiosity.&lt;/p&gt;

&lt;p&gt;The counter said "Only 2 seats left" again. Same number. It had been resetting on every page load the whole time.&lt;/p&gt;

&lt;p&gt;That's when I started cataloguing every trick I'd seen on shopping sites and built a Chrome extension that flags them automatically, in real time, on any page.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't just my opinion — it's documented research
&lt;/h2&gt;

&lt;p&gt;In 2019, Princeton and University of Chicago researchers scraped 11,000 shopping sites and found dark patterns on more than 1,250 of them. The FTC has since fined several companies specifically for fake countdown timers and pre-checked subscription boxes. This isn't a grey area anymore — it's a known, studied, and increasingly regulated practice.&lt;/p&gt;

&lt;p&gt;The extension targets four categories that account for most of what's out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fake urgency.&lt;/strong&gt; Countdown timers that reset, "X people are viewing this" badges that never change, "only N left in stock" that's been static for a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap checkboxes.&lt;/strong&gt; A checkbox for "Yes, sign me up for the newsletter" that's pre-checked and styled to blend into the page so you don't notice it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmshaming.&lt;/strong&gt; The decline button reads "No thanks, I don't want to save money" instead of just "No thanks."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Psychological pricing.&lt;/strong&gt; Prices ending in .99 or .95 designed to register as a lower price bracket than they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no AI this time
&lt;/h2&gt;

&lt;p&gt;My phishing detector used Claude because language and intent are genuinely ambiguous — you need a model that understands context. Dark patterns are different. They're structural. A countdown timer either resets on reload or it doesn't. A checkbox is either pre-checked or it isn't. That's a DOM query, not a judgment call.&lt;/p&gt;

&lt;p&gt;So this one runs entirely on regex and DOM inspection. No API calls, no latency, no cost per scan, works offline. Sometimes the boring solution is the correct one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting the urgency pattern
&lt;/h2&gt;

&lt;p&gt;The core check looks for known urgency phrases and cross-references them against actual page state:&lt;/p&gt;

&lt;p&gt;const URGENCY_PHRASES = [&lt;br&gt;
  /only \d+ left/i,&lt;br&gt;
  /\d+ people (are )?(viewing|watching)/i,&lt;br&gt;
  /offer ends in/i,&lt;br&gt;
  /flash sale/i,&lt;br&gt;
  /limited time/i&lt;br&gt;
];&lt;/p&gt;

&lt;p&gt;function isLikelyFake(element) {&lt;br&gt;
  const text = element.textContent;&lt;br&gt;
  const matchesPattern = URGENCY_PHRASES.some(p =&amp;gt; p.test(text));&lt;br&gt;
  if (!matchesPattern) return false;&lt;/p&gt;

&lt;p&gt;// Countdown timers that are actually fake almost always&lt;br&gt;
  // reset to the same value across page loads — checked via&lt;br&gt;
  // localStorage comparison against the previous visit&lt;br&gt;
  const stored = localStorage.getItem('dpd_seen_' + location.pathname);&lt;br&gt;
  const isStatic = stored === text;&lt;br&gt;
  localStorage.setItem('dpd_seen_' + location.pathname, text);&lt;/p&gt;

&lt;p&gt;return matchesPattern &amp;amp;&amp;amp; isStatic;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The trick isn't the regex — it's storing what you saw on the last visit and comparing. A genuine "3 left in stock" notice changes over time. A fake one doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting trap checkboxes
&lt;/h2&gt;

&lt;p&gt;This one's simpler — just check if a checkbox tied to subscription/marketing language defaults to checked:&lt;/p&gt;

&lt;p&gt;const TRAP_KEYWORDS = /newsletter|subscribe|offers|insurance|protection plan/i;&lt;/p&gt;

&lt;p&gt;function findTrapCheckboxes() {&lt;br&gt;
  return [...document.querySelectorAll('input[type="checkbox"]')]&lt;br&gt;
    .filter(cb =&amp;gt; cb.checked &amp;amp;&amp;amp; TRAP_KEYWORDS.test(getLabelText(cb)));&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The hard part isn't the logic, it's getLabelText() — checkbox labels are wrapped, nested, or associated via for= attributes in about six different inconsistent ways depending on the site. That function ended up being three times longer than the detection logic itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marking it on the page
&lt;/h2&gt;

&lt;p&gt;Detection is half the work. The other half is surfacing it without breaking the page layout:&lt;/p&gt;

&lt;p&gt;function markElement(el, label) {&lt;br&gt;
  const badge = document.createElement('span');&lt;br&gt;
  badge.textContent = '⚠ ' + label;&lt;br&gt;
  badge.style.cssText = &lt;code&gt;&lt;br&gt;
    position: absolute; background: #ef4444; color: white;&lt;br&gt;
    font-size: 11px; padding: 2px 8px; border-radius: 10px;&lt;br&gt;
    z-index: 999999; pointer-events: none;&lt;br&gt;
&lt;/code&gt;;&lt;br&gt;
  el.style.position = 'relative';&lt;br&gt;
  el.appendChild(badge);&lt;br&gt;
}&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching dynamically injected patterns
&lt;/h2&gt;

&lt;p&gt;Plenty of these patterns get injected after the initial page load — countdown widgets, exit-intent popups with fake scarcity. A one-time scan on page load misses them.&lt;/p&gt;

&lt;p&gt;const observer = new MutationObserver(() =&amp;gt; scanPage());&lt;br&gt;
observer.observe(document.body, { childList: true, subtree: true });&lt;/p&gt;

&lt;p&gt;This runs the same detection logic on every DOM mutation, debounced to avoid hammering the page on sites with constant re-renders (looking at you, infinite scroll feeds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I ran it against 30 ecommerce sites I had open in old tabs — a mix of fast fashion, flight booking, and a few subscription services. It flagged something on 19 of them. Two false positives: a genuinely real "low stock" indicator on a small print-on-demand shop, and a checkbox for "remember me" that the keyword filter misread as marketing consent.&lt;/p&gt;

&lt;p&gt;Everything else held up. The fake countdown timers in particular were depressingly consistent — same number, every single reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this legal to build and use?&lt;/strong&gt;&lt;br&gt;
Yes. You're reading and analyzing the public DOM of a page you're voluntarily visiting in your own browser. No data leaves your machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it slow down the pages I visit?&lt;/strong&gt;&lt;br&gt;
Not noticeably. The regex checks are cheap and the MutationObserver is debounced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this in a commercial product?&lt;/strong&gt;&lt;br&gt;
The source is MIT licensed in the free version. Build on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just block the patterns instead of flagging them?&lt;/strong&gt;&lt;br&gt;
Some checkboxes are legitimately useful (extended warranty you actually want). The goal is informed choice, not removing functionality you might want.&lt;/p&gt;




&lt;p&gt;Full source code: &lt;a href="https://carlosdevlop.gumroad.com/l/dark-patterns-detector" rel="noopener noreferrer"&gt;https://carlosdevlop.gumroad.com/l/dark-patterns-detector&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a phishing detector into Chrome using Claude AI. Here's exactly how.</title>
      <dc:creator>carlos lopez</dc:creator>
      <pubDate>Wed, 17 Jun 2026 14:15:23 +0000</pubDate>
      <link>https://dev.to/carlos_lopez_e0907403c1b4/i-built-a-phishing-detector-into-chrome-using-claude-ai-heres-exactly-how-2d6c</link>
      <guid>https://dev.to/carlos_lopez_e0907403c1b4/i-built-a-phishing-detector-into-chrome-using-claude-ai-heres-exactly-how-2d6c</guid>
      <description>&lt;p&gt;My mother called me last week. Someone had sent her an SMS &lt;br&gt;
claiming to be from DHL, asking her to pay a £2.99 customs &lt;br&gt;
fee via a link. She almost clicked it.&lt;/p&gt;

&lt;p&gt;That was enough. I spent a weekend building a Chrome extension &lt;br&gt;
that lets you paste any suspicious message and get an instant &lt;br&gt;
verdict. Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture (and why Cloudflare Workers)
&lt;/h2&gt;

&lt;p&gt;The obvious approach is to call the Claude API directly from &lt;br&gt;
the extension. Don't do this. Your API key lives in the &lt;br&gt;
extension code, which anyone can extract from the Chrome Web &lt;br&gt;
Store in about 30 seconds.&lt;/p&gt;

&lt;p&gt;The right pattern: extension → Cloudflare Worker → Claude API. &lt;br&gt;
The Worker lives server-side, holds the API key as an &lt;br&gt;
environment variable, and acts as a proxy. Cloudflare's free &lt;br&gt;
tier handles 100,000 requests/day, which is more than enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worker
&lt;/h2&gt;

&lt;p&gt;export default {&lt;br&gt;
  async fetch(request, env) {&lt;br&gt;
    const { prompt } = await request.json();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'x-api-key': env.ANTHROPIC_API_KEY,
    'anthropic-version': '2023-06-01',
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 350,
    messages: [{ role: 'user', content: prompt }]
  })
});

return response;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;I'm using Haiku, not Opus. For a classification task like &lt;br&gt;
this — is this phishing or not — Haiku is faster, 10x cheaper, &lt;br&gt;
and gets the same result. Opus is overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;After a dozen iterations, this is what actually works:&lt;/p&gt;

&lt;p&gt;"You are an expert cybersecurity analyst specializing in &lt;br&gt;
phishing detection. Analyze the following message and &lt;br&gt;
determine if it is PHISHING, SUSPICIOUS, or LEGITIMATE.&lt;/p&gt;

&lt;p&gt;Pay special attention to impersonation of financial &lt;br&gt;
institutions (PayPal, Chase, Barclays), government agencies &lt;br&gt;
(IRS, HMRC, DVLA), delivery services (UPS, FedEx, Royal Mail) &lt;br&gt;
and major tech companies (Amazon, Apple, Microsoft, Netflix).&lt;/p&gt;

&lt;p&gt;Respond ONLY in this format:&lt;br&gt;
VERDICT: [PHISHING / SUSPICIOUS / LEGITIMATE]&lt;br&gt;
CONFIDENCE: [High / Medium / Low]&lt;br&gt;
SIGNALS: [comma-separated list, max 4]&lt;br&gt;
ADVICE: [one clear action sentence]"&lt;/p&gt;

&lt;p&gt;One thing worth knowing: parse only the VERDICT line, &lt;br&gt;
not the whole response. Otherwise txt.includes("PHISHING") &lt;br&gt;
will always return true because the word appears in the &lt;br&gt;
template itself.&lt;/p&gt;

&lt;p&gt;const verdictLine = txt.split('\n')&lt;br&gt;
  .find(l =&amp;gt; l.startsWith('VERDICT:')) || '';&lt;br&gt;
const isPhishing = verdictLine.includes('PHISHING');&lt;/p&gt;

&lt;p&gt;Obvious in hindsight. Took me longer than I'd like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Tested against 50 real phishing attempts. Claude got 48 right. &lt;br&gt;
The two it missed were unusually well-crafted — &lt;br&gt;
legitimate-looking domains with no obvious red flags. &lt;br&gt;
For anything with a suspicious link or an urgency pattern, &lt;br&gt;
it's essentially perfect.&lt;/p&gt;




&lt;p&gt;If you want the full source code — extension, Worker, and &lt;br&gt;
deploy instructions — I packaged it here: &lt;a href="https://carlosdevlop.gumroad.com/l/ai-phishing-detector-bundle" rel="noopener noreferrer"&gt;https://carlosdevlop.gumroad.com/l/ai-phishing-detector-bundle&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
