DEV Community

Really Offers
Really Offers

Posted on

Scoring a "You Won!" Message: The Pattern Checks Behind Our Scam Checkers

Prize scams run on scripts. The wording changes, but the skeleton doesn't: you won something, you were chosen, act fast, and — the load-bearing step — pay a small fee or hand over a code to "release" your prize. Regulators like the FTC count the losses from scams that start with a message or a social post in the billions, and most of those losses begin with text a person could have been warned about in the ten seconds before they tapped.

We built two small checkers around that observation: one scores a pasted message, the other dissects a link. Both are pure client-side pattern checks. This post is about how the scoring works, where it deliberately stops, and what it returns on real inputs — including the ones that make it look bad.

Design constraints first

Three decisions shaped everything else:

  1. Pure functions, no DOM, no network. smcAnalyze(message, link) and lrcAnalyze(url) take strings and return { score, tells, level, levelLabel }. Nothing is fetched — critically, the link checker parses a URL with the URL constructor but never opens it. A tool for inspecting malicious links that visits malicious links would be a bad tool.
  2. Every point must be explainable. Each hit appends a plain-language "tell" ("It asks you to PAY something…"), and the UI lists them. A risk score the user can't interrogate is just a coloured light.
  3. Three levels, carefully named: Looks safer, Be careful, High risk — likely a scam pattern. There is no "Safe" anywhere in the code. That wording choice is a feature, discussed below.

The message model: weighted tells and two decisive flags

The message checker lowercases the message plus an optional link, then runs a table of regex checks through one helper:

function hit(re, label, weight, isStrong) {
  if (re.test(text)) { tells.push(label); score += weight; if (isStrong) strong = true; }
}
Enter fullscreen mode Exit fullscreen mode

The weights, in order of appearance:

Pattern Weight Decisive
Prize / winner / lottery claim 2 –
"You were (randomly) selected" 2 –
Payment ask: release/processing/delivery fee, gift cards, crypto, wire transfer 3 ✔
Credential ask: password, OTP, PIN, bank or card details, CVV 3 ✔
Shortened link (bit.ly, t.co, rb.gy, …) 2 –
Urgency or threats ("act now", "account will be closed") 1 –
Official-sounding impersonation words / "Dear customer" 1 –
Pressure to move to WhatsApp/Telegram or reply instantly 1 –

The level logic is one line of consequence:

if (strong || score >= 5) { level = 2; }      // High risk
else if (score >= 2)     { level = 1; }      // Be careful
else                     { level = 0; }      // Looks safer
Enter fullscreen mode Exit fullscreen mode

The strong flag exists because addition alone gets the ethics wrong. A message whose only sin is asking for a "release fee" scores just 3 — below the high-risk threshold — yet "pay to receive your prize" is the single most reliable scam signature there is. So payment asks and credential asks are decisive: either one forces High risk on its own, regardless of total.

The link model: read the host, not the story

The link checker normalises the input (prepending https:// for parsing if no scheme was pasted), then works almost entirely on the hostname:

  • IP-literal host or a : in the host — 3, decisive. Real banks don't send login links as raw numbers.
  • @ in the authority — 3, decisive. Everything before the @ is discarded by the browser; it's a hiding trick.
  • Brand name in the wrong host — 3, decisive. A small table maps 15 brands to their real domains (paypal → paypal.com, amazon → amazon.com, DHL, Netflix, Binance…). The rule is exact:
if (host.indexOf(brand) !== -1 && host !== real &&
    host.slice(-(real.length + 1)) !== '.' + real) {
  hit('"paypal" appears in the address, but this is NOT the real paypal.com', 3, true);
}
Enter fullscreen mode Exit fullscreen mode

Containing the brand name proves nothing; being the domain or a subdomain of it is the only pass.

  • Supporting signals: plain http:// (2), a known URL shortener (2), three or more stacked subdomains (2), punycode xn-- (2), the word "bank" in an unlisted domain (2), no visible scheme (1), length over 75 characters (1), three or more hyphens (1), five or more digits (1).

Worked examples — actual outputs

These are the functions' real results (the logic has no DOM dependencies, so it runs unchanged under Node):

Input Score Verdict
"Congratulations! You have won a $1000 cash prize. You were randomly selected… pay a small release fee of $25 by gift card within 24 hours. Dear customer, act now — contact us on WhatsApp…" 10 (6 tells) 🚩 High risk
"Hi Sam, your parcel is on its way and should arrive Thursday. You can follow it in the app any time." 0 ✅ Looks safer
"Your order has shipped. Track your parcel here:" + https://bit.ly/3xYzAbc 2 (shortener only) ⚠️ Be careful
http://192.168.1.55/paypal-login 6 (http 2 + IP 3 + digit count 1) 🚩 High risk
https://www.amazon.com 0 ✅ Looks safer
paypal-verify-account.com 4 (no scheme 1 + brand mismatch 3) 🚩 High risk

The last row is the design in miniature: the score alone (4) sits in "Be careful" territory, but the decisive brand rule overrides — exactly the case a pure threshold would have under-called.

The part we refused to rig

Shortener lists are where a tool like this can quietly cheat, so we didn't: t.co is on both checkers' shortener lists. That's the shortener used by X — and the one used for our own shared links. Paste one of our own shortened links into the Link Checker and it comes back "Be careful: the real destination is hidden until you click." That is, inconveniently, true — for everyone's links, including ours. A warning tool that exempts its author isn't a warning tool.

The same stance produced the rest of the honesty surface:

  • Every result carries the note: "This is a pattern check, not a verdict on this specific sender or website."
  • The green state says "Looks safer", never "Safe" — a formulaic scam written carefully enough can score low, and the UI shouldn't claim otherwise.
  • The link checker explicitly defers to Google Safe Browsing and VirusTotal for live checks, because shape analysis cannot see what a page actually does.
  • Empty or unparseable input returns "Be careful" with an explanation — never a pass, never a crash.

What this approach can't do matters as much: regexes are evadable, honest messages trip tells (a genuine delivery text with a shortened tracking link is "Be careful" — by design, because the reader genuinely can't see where it leads), and no pattern score can verify a sender. It's a warning light, not a judge. But warning lights are cheap, instant, and private — and for the ten seconds before someone pays a "release fee," they're often the only check that happens at all.

Both tools run live as working demos — the Scam Message Checker and the companion Link Checker — as pure page snippets: no server, no database, nothing you paste leaves the browser.

Top comments (0)