From 7565264b8185c8a28a5f6ccfdb5631da3a745faa Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 26 Jul 2026 08:14:06 +0000 Subject: [PATCH] fix(leads): don't pitch a rescue at sites that already score well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dry run over the real audience surfaced "share.google scored 91/100 — want us to fix it?". Offering to fix a site scoring 91 reads as a canned mailshot and costs credibility with exactly the people most likely to be competent buyers. Strong scorers now get "— the last few gaps" and a softened CTA. Note the inverted slop dial: strong means LOW slop and HIGH AEO, so `kind` had to be carried on the row rather than comparing the number alone. Only 1 of the current 106 recipients is affected, but the mix will differ on future sends. Co-Authored-By: Claude Opus 5 (1M context) --- app/api/admin/lead-campaign/route.ts | 3 +++ lib/email.ts | 7 +++++-- lib/leadCampaign.ts | 14 +++++++++++++ tests/lead-campaign.test.ts | 30 ++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/api/admin/lead-campaign/route.ts b/app/api/admin/lead-campaign/route.ts index 3ed21c93..c2fff274 100644 --- a/app/api/admin/lead-campaign/route.ts +++ b/app/api/admin/lead-campaign/route.ts @@ -7,6 +7,7 @@ import { recordLead } from "@/lib/marketing"; import { campaignSubject, hireUrlFor, + isStrongScore, selectRecipients, type LeadRow, type Segment, @@ -111,6 +112,7 @@ async function loadAudience(): Promise { reportToken: (r.share_token as string | null) ?? null, score: card.score, scoreLabel: card.label, + kind: card.kind, scaleHint: card.scaleHint, topIssues: issuesByAudit.get(String(r.id)) ?? [], isCustomer: customerEmails.has(email), @@ -218,6 +220,7 @@ export async function POST(req: NextRequest) { reportUrl: `${base}/r/${row.reportToken}`, hireUrl: hireUrlFor(row, base), isCustomer: row.isCustomer, + strong: isStrongScore(row), }), unsubscribeToken: contact.unsubscribe_token as string, }); diff --git a/lib/email.ts b/lib/email.ts index 7964c39a..0c59752f 100644 --- a/lib/email.ts +++ b/lib/email.ts @@ -848,6 +848,8 @@ export function leadCampaignEmailHtml(input: { reportUrl: string; hireUrl: string; isCustomer: boolean; + /** Already scoring well — don't pitch a rescue at them. */ + strong?: boolean; }): string { const issues = input.topIssues.length ? `
    @@ -896,8 +898,9 @@ export function leadCampaignEmailHtml(input: {

    - We also fix these directly. Tell us about the site and we'll come - back with what it takes to move that number — no obligation. + ${input.strong + ? `That's a strong result — most sites we scan don't get there. If you want the remaining gaps closed properly, we do that work directly.` + : `We also fix these directly. Tell us about the site and we'll come back with what it takes to move that number — no obligation.`}

    diff --git a/lib/leadCampaign.ts b/lib/leadCampaign.ts index 8272d188..4260641d 100644 --- a/lib/leadCampaign.ts +++ b/lib/leadCampaign.ts @@ -11,6 +11,8 @@ export type LeadRow = { reportToken: string | null; score: number | null; scoreLabel: string; + /** Which dial the score is on — the two run in opposite directions. */ + kind: "aeo" | "slop"; scaleHint: string; topIssues: string[]; /** Has a profiles row — an existing relationship, not a cold lead. */ @@ -68,8 +70,20 @@ export function selectRecipients( return { send, excluded }; } +/** + * A site that already scores well is the wrong audience for a "want us to fix + * it?" pitch — it reads as a canned mailshot and costs credibility with + * exactly the people most likely to be competent buyers. Note the inverted + * slop dial: strong means LOW there and HIGH for AEO. + */ +export function isStrongScore(row: LeadRow): boolean { + if (row.score === null) return false; + return row.kind === "slop" ? row.score <= 25 : row.score >= 80; +} + export function campaignSubject(row: LeadRow): string { if (row.score === null) return `Your CrawlProof scan of ${row.host}`; + if (isStrongScore(row)) return `${row.host} scores ${row.score}/100 — the last few gaps`; return `${row.host} scored ${row.score}/100 — want us to fix it?`; } diff --git a/tests/lead-campaign.test.ts b/tests/lead-campaign.test.ts index e9a2326d..4afbd9fc 100644 --- a/tests/lead-campaign.test.ts +++ b/tests/lead-campaign.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { campaignSubject, excludeReason, + isStrongScore, hireUrlFor, selectRecipients, type LeadRow, @@ -14,6 +15,7 @@ function lead(over: Partial = {}): LeadRow { reportToken: "tok123456", score: 54, scoreLabel: "AEO Score", + kind: "aeo", scaleHint: "out of 100 · higher is better", topIssues: ["Blocks GPTBot", "No structured data"], isCustomer: false, @@ -127,6 +129,34 @@ describe("campaignSubject", () => { }); }); +describe("isStrongScore / subject framing", () => { + it("does not pitch a rescue at a site that already scores well", () => { + // "want us to fix it?" to a 91/100 site reads as a canned mailshot and + // costs credibility with the most competent buyers. + const row = lead({ host: "share.google", score: 91, kind: "aeo" }); + expect(isStrongScore(row)).toBe(true); + expect(campaignSubject(row)).toBe("share.google scores 91/100 — the last few gaps"); + }); + + it("still pitches the fix at a weak score", () => { + const row = lead({ host: "acme.com", score: 40, kind: "aeo" }); + expect(isStrongScore(row)).toBe(false); + expect(campaignSubject(row)).toBe("acme.com scored 40/100 — want us to fix it?"); + }); + + it("inverts 'strong' for the slop dial", () => { + // Low slop is good; high slop is bad. The same number flips meaning. + expect(isStrongScore(lead({ score: 12, kind: "slop" }))).toBe(true); + expect(isStrongScore(lead({ score: 12, kind: "aeo" }))).toBe(false); + expect(isStrongScore(lead({ score: 90, kind: "slop" }))).toBe(false); + expect(isStrongScore(lead({ score: 90, kind: "aeo" }))).toBe(true); + }); + + it("is never strong without a score", () => { + expect(isStrongScore(lead({ score: null }))).toBe(false); + }); +}); + describe("hireUrlFor", () => { it("prefills the form and tags the source", () => { const url = new URL(hireUrlFor(lead({ host: "acme.com", email: "p@x.com" }), "https://crawlproof.com/"));