Everyone can now smell AI copy. "Revolutionize your workflow." "Seamlessly unlock powerful solutions." "Elevate your experience." The moment a stranger reads that on your hero section, they file you under generated and bounce.
So I built a tool that scores it for you: paste a headline, a subhead and a button label, get a 0–100 number for how generic/AI-templated the copy reads — plus the exact lines to fix.
The interesting constraint: no LLM, no backend, no network call. The whole thing is deterministic and runs in your browser. Your text never leaves the page. That's not a privacy gimmick — it's the point. To detect "sounds AI-generated" you don't need AI. You need a list of tells and a scoring function.
Try it: 1h-money-store.vercel.app/grader
Here's every heuristic, and the code.
The model: 5 dimensions, 100 points
I split "does this read human?" into five measurable dimensions:
| Dimension | Max | What it measures |
|---|---|---|
| Anti-hype | 25 | Buzzwords, exclamation marks, emoji, ALL-CAPS |
| Specificity | 25 | Is there a concrete number / proof? |
| Clarity | 25 | Filler-word density |
| Headline shape | 13 | Word count of the headline |
| CTA | 12 | Is the button generic ("Submit") or specific? |
Each dimension starts at its max and loses points for tells. Sum them, that's the score. No black box — you can trace every point.
1. Anti-hype (25 pts) — the #1 tell
Hype words are the single strongest "AI wrote this" signal. LLMs reach for them by default because they're statistically safe and say nothing. I keep a wordlist and penalize each hit hard.
var HYPE = ['revolutionize','revolutionary','unlock','unleash','seamless',
'game-changer','cutting-edge','next-level','supercharge','effortless',
'elevate','empower','transform','best-in-class','world-class',
'state-of-the-art','robust','synergy','disruptive','innovative','leverage',
'harness','turbocharge','skyrocket','10x','paradigm','frictionless','holistic'];
// count whole-word hits, case-insensitive, respecting word boundaries
function countHits(t, list){
var l = ' ' + t.toLowerCase() + ' ', n = 0;
list.forEach(function(w){
var re = new RegExp('(^|[^a-z])' + w.replace(/[-\/\\^$*+?.()|[\]{}]/g,'\\$&') + '([^a-z]|$)','g');
var m = l.match(re);
if (m) n += m.length;
});
return n;
}
Then hype isn't the only shouting tell. Exclamation marks, emoji in a hero headline, and ALL-CAPS words all read as templated. So the penalty stacks:
var hype = countHits(all, HYPE);
var excl = (all.match(/!/g) || []).length;
var emo = emojiCount(all);
var caps = words(h + ' ' + s).filter(function(w){
return w.length > 2 && w === w.toUpperCase() && /[A-Z]/.test(w);
}).length;
var antiPen = hype*7 + excl*5 + emo*4 + caps*4;
var antiHype = Math.max(0, 25 - antiPen);
Weights are opinionated: one hype word (−7) costs more than one exclamation (−5). A single "revolutionize" can tank this whole dimension, which is exactly the behavior I want.
2. Specificity (25 pts) — one number changes everything
Generic copy floats. "We help teams do their best work." Specific copy lands. "Cut standups from 30 minutes to 6." The cheapest signal of specificity is a digit. So:
function hasNumber(t){ return /\d/.test(t); }
var spec = hasNumber(all) ? 25 : 8;
// partial credit for quantifier words even without a digit
if (/%|\bx\b|×|hours?|days?|minutes?|\bno\b|zero/i.test(all) && spec < 25)
spec = Math.min(25, spec + 9);
No number at all = you start at 8/25. This is intentionally blunt: a number isn't sufficient for good copy, but its absence is a reliable smell.
3. Clarity (25 pts) — filler tax
Second wordlist: the vague nouns and intensifiers that add length, not meaning. "Solutions." "Platform." "Powerful." "Simply just really very."
var FILLER = ['solutions','platform','powerful','amazing','great','awesome',
'stuff','things','simply','just','very','really','stunning','beautiful',
'ultimate','premium','quality','value','experience','journey','ecosystem',
'suite','toolkit','all-in-one','one-stop'];
var filler = countHits(all, FILLER);
var clarity = Math.max(0, 25 - filler*6);
Same countHits machine, −6 per hit. Four filler words zeroes the dimension.
4. Headline shape (13 pts) — length is a proxy
You can't measure "is this a good headline" without a model, but you can measure length, and length correlates with clarity at the extremes. Too long = you crammed two ideas in. Too short = it's probably vague.
var hw = words(h).length, hlScore = 13;
if (hw === 0) hlScore = 0;
else if (hw > 12) hlScore = 5; // rambling
else if (hw > 10) hlScore = 9;
else if (hw < 3) hlScore = 7; // too thin to carry the offer
// sweet spot 3–10 words keeps full marks
5. CTA (12 pts) — "Submit" is a wasted button
Last wordlist: dead button labels. If your CTA is in it, you're leaving the most-clicked element on the page saying nothing.
var WEAKCTA = ['submit','learn more','click here','read more','get started',
'sign up','continue','next','go','here','more info','discover','explore'];
var ctaW = c.trim().toLowerCase();
var weak = WEAKCTA.indexOf(ctaW) >= 0 || ctaW === '';
var ctaScore = c.trim() === '' ? 0 : (weak ? 4 : 12);
"Start a project", "Grade my page", "Get the 42 prompts" → full marks. "Learn more" → 4.
Turning the score into fixes
A number alone is a party trick. The value is telling you which line to change. Every penalty that fired becomes a concrete instruction:
var fixes = [];
if (hype > 0) fixes.push({t:'Cut the hype words',
d:'Found ' + hype + '. Replace each with a plain, concrete verb. Hype words are the #1 AI tell.'});
if (!hasNumber(all)) fixes.push({t:'Add one number',
d:'No concrete figure anywhere. A single %, count or timeframe raises believability instantly.'});
if (filler > 0) fixes.push({t:'Delete filler',
d:'Found ' + filler + ' vague words. They add length, not meaning.'});
if (hw > 12) fixes.push({t:'Shorten the headline',
d:hw + ' words is too long. Aim for ≤10 — the single idea a stranger would repeat.'});
if (weak && c.trim() !== '') fixes.push({t:'Rewrite the CTA',
d:'Use action + outcome, not "Submit/Learn more".'});
The output reads back the count (Found 3) so the feedback is falsifiable — you can go find the three words.
Why deterministic beats an LLM here
I could have shipped this as a prompt to GPT/Claude. I didn't, and I'd argue you shouldn't for this class of tool:
- Same input → same score, forever. No temperature, no drift. You can screenshot a score and it'll reproduce.
- Zero cost, zero latency, zero infra. It's a static HTML file on a CDN. No API key to rotate, no rate limit, no bill.
-
Privacy is structural, not promised. The text physically can't leave — there's no
fetch. That's a stronger claim than any privacy policy. - It's honest about what it is. It's a checklist of known tells, not an oracle. Users can read the rules and disagree with a weight — which is the right relationship to have with a linter.
The whole grader is ~90 lines of vanilla JS in one file. No framework, no build step.
Try it / take the wordlists
Paste your own hero copy and see what it flags: the Landing Page Grader — free, no signup, runs entirely client-side.
If you want the flip side — prompts that force ChatGPT/Claude to stop producing the words on these lists — I packaged 7 of them free here: 1h-money-store.vercel.app/free.
What tells would you add to the wordlists? I'm collecting them.
Top comments (0)