Skip to content

Instantly share code, notes, and snippets.

@parweb
Last active July 25, 2026 00:29
Show Gist options
  • Select an option

  • Save parweb/247fd8832cee337f860aee2a39faf8db to your computer and use it in GitHub Desktop.

Select an option

Save parweb/247fd8832cee337f860aee2a39faf8db to your computer and use it in GitHub Desktop.
The 9 AI-writing tells, with real frequencies from 239 landing pages (deterministic scorer, no LLM). Top tell: 81% of heroes contain no concrete number.

The 9 AI-writing tells, with real frequencies from 239 landing pages

Most "how to spot AI writing" lists are vibes. This one is measured.

I ran a deterministic scorer (regex + wordlists, no LLM) over the hero copy — headline, subhead, CTA — of 239 real SaaS landing pages, fetched 2026-07-24. Below is how often each of the 9 tells actually fired.

The headline result: the single most common tell is not a word. It's an absence. 81% of landing pages (194/239) have no concrete number anywhere in the hero. That beats every hype word, every "seamless", every em-dash theory.


The frequencies

# Tell What fires it Frequency Precision
4 No concrete number in hero no digit in headline+subhead+CTA 81.2% (194/239) 81.2–81.6%
2 Filler words "solutions / platform / powerful / streamline"… 34.3% (82/239) exact
3 Weak CTA "Learn more", "Get started", "Discover"… 14.6% (35/239) exact
9 ALL-CAPS words in hero word >2 chars, fully uppercase 13.0% (31/239) 13.0–23.0%
1 Hype words "revolutionize / unlock / seamless / leverage"… 6.7% (16/239) exact
6 Headline too short < 3 words 5.0% (12/239) 5.0–12.6%
5 Headline too long > 12 words 2.9% (7/239) 2.9–9.6%
7 Exclamation marks any ! 2.9% (7/239) 2.9–11.7%
8 Emoji in hero any emoji codepoint 2.1% (5/239) 2.1–12.1%

Why some rows say "≥" — read this before quoting me

The published CSV stores at most 3 flags per page, and it keeps them in the scorer's fixed emission order (hype, filler, weakcta, nonum, longhl, shorthl, excl, emoji, caps). I verified this: all 239 rows are in canonical order, 0 exceptions, and no row has more than 3 flags.

So the truncation is not random — it bites the tail of that order:

  • hype, filler, weakcta are positions 1–3. They can never be cut. Exact.
  • nonum is position 4. It's unknown for exactly 1 page (the one row whose three slots were already full before reaching it). Effectively exact.
  • Positions 5–9 are unknown for 16–24 pages each. Those are lower bounds, and I've given the upper bound too. The truth is somewhere in the interval.

I'm not going to round a lower bound up and call it a finding. If you want the exact tail frequencies, the scorer is client-side and you can re-run it.

Not measured (people ask, I have no data): em-dash rate, "delve", "tapestry", "it's not X, it's Y" constructions, tricolons, sentence-length variance. The scorer doesn't look for them, so I'm not reporting numbers on them.


Score distribution

Deterministic 0–100 across 5 dimensions: Anti-hype (25), Specificity (25), Clarity (25), Headline shape (13), CTA (12).

100     ███████████████████ 19
90-99   ████████████████████████ 24
80-89   ███████████████████████████████████████████████████████████████████████████ 75
70-79   ██████████████████████████████████████████████████████████████████████████████████████████ 90
60-69   ██████████████████████████ 26
50-59   ████ 4
40-49   █ 1

Median 79, mean 80.1. 19 pages at 100 (calendly.com among them). 31 pages below 70. Median hero length: 151 characters.

A 100 means zero flags fired — not that the copy is good. The scorer can only detect the tells it knows about.

The 8 lowest scores

Score Domain Flags
41 copy.ai filler, nonum, shorthl
51 beehiiv.com filler, nonum, caps
51 bitwarden.com filler, nonum, caps
57 optimizely.com filler, nonum, longhl
58 unit.co hype, filler, nonum
61 close.com filler, nonum, caps
61 jotform.com filler, nonum, caps
61 stripe.com filler, weakcta, nonum

copy.ai — an AI copywriting company — has the most AI-sounding hero in the dataset. I did not go looking for that. It fell out of the sort.

And stripe.com at 61 is the useful counterexample: a page can convert extremely well and still score badly here. The scorer measures how templated the copy reads, not revenue. Don't confuse the two.

Some pages you'd recognise

Score Domain Flags
83 notion.so nonum
83 jasper.ai nonum
79 linear.app nonum, emoji
77 vercel.com nonum, shorthl
75 anthropic.com weakcta, nonum
75 figma.com weakcta, nonum
62 asana.com hype, filler, weakcta

Note how many land on nonum alone. That's the whole point: the modal "AI-sounding" landing page in 2026 isn't full of "revolutionize". It's clean, grammatical, confident — and says nothing you could check.


Method, and its limits

  • Sample: 239 SaaS/dev-tool landing pages, homepage hero only. 303 candidate URLs were collected; 239 survived extraction. The 64 that dropped out are a selection effect, not a random one — they failed precisely because their hero was harder to extract, which skews toward JS-heavy pages. Read every number below as describing statically-extractable landing pages.
  • Fetch: static HTTP GET + regex extraction of headline/subhead/CTA (static-fetch-regex-v1), 2026-07-24. No JS execution.
  • Scoring: deterministic. Same input → same score, forever. No model call.
  • Known biases, stated plainly:
    • Static fetch means JS-rendered heroes may be mis-extracted or missed.
    • Wordlists are hand-built and English-only. They encode my judgment about what "filler" is. Reasonable people will disagree — the lists are readable.
    • Sample is convenience-selected toward SaaS/dev tools, not random.
    • "AI-sounding" ≠ "AI-generated". A human writing on autopilot produces the same tells. This measures the output, and makes no claim about the author.
    • nonum is a blunt instrument: it fires on any hero with no digit, including pages where a number would be dishonest.

Data

  • Full CSV, 239 rows (url, domain, score, top_flags, hero_chars, extracted_at, method): https://gist.github.com/parweb/5ed569ba76c365f7b789a979ad6090e7
  • Reproduce these numbers — the analysis above is just csv + collections.Counter over that file. Grouping by top_flags, plus the truncation accounting described above.

Related

Disclosure: this is my own dataset and my own scorer. Everything above is checkable against the CSV; I'd rather be corrected than quoted.

CC0 — take the numbers, no attribution needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment