From 3300570efa520d417a6fcbcb77ce4349318124ee Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 28 Jul 2026 03:06:46 +0000 Subject: [PATCH] fix(leads): stop scanning the businesses a project discovers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding leads queued a free CrawlProof scan of every domain it turned up, and new campaigns did the same on every tick. That only ever made sense for one pitch. The scan exists to supply findings for "we scanned your site, here is what we found" — so a project pitching its own offer gets nothing from it, while still paying for it: worker time spent on evidence nobody will cite, and a scanner pointed at people whose only involvement was appearing in a search result. It also stranded prospects, since a lead sat at "new" until a scan landed that nothing was waiting for. One-shot lead finding no longer scans at all, and new campaigns default to scanning off. Selecting the audit pitch turns it back on, because that pitch cannot be written without findings. The lead finder's own description promised a scan it no longer performs, so that goes too. Co-Authored-By: Claude Opus 5 (1M context) --- app/actions/leads.ts | 18 ++++++++++-------- components/leads/campaign-panel.tsx | 6 +++++- components/leads/lead-finder.tsx | 13 +++++++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/actions/leads.ts b/app/actions/leads.ts index b2c378c0..65ff587a 100644 --- a/app/actions/leads.ts +++ b/app/actions/leads.ts @@ -79,7 +79,7 @@ export async function findLeadsAction(input: { } let added = 0; - let scanning = 0; + const scanning = 0; for (const candidate of found.prospects.slice(0, limit)) { const res = await researchProspect({ userId: auth.userId, @@ -87,11 +87,15 @@ export async function findLeadsAction(input: { url: candidate.url, discoveredVia: candidate.via, discoveryLabel: candidate.label, + // Finding leads for a project does not scan them. The scan exists to + // supply findings for the CrawlProof audit pitch, and firing one at + // every discovered business spends worker time on evidence nobody is + // going to cite — and points a scanner at people who only turned up in + // a search. Campaigns that do pitch an audit turn scanning back on + // explicitly. + skipScan: true, }); - if (res.status === "scanning") { - scanning += 1; - added += 1; - } else if (res.status === "researched") { + if (res.status === "researched") { added += 1; } } @@ -101,9 +105,7 @@ export async function findLeadsAction(input: { ok: true, added, scanning, - note: scanning - ? `${added} leads added — ${scanning} are scanning now. Refresh in a minute to see scores.` - : `${added} leads added.`, + note: `${added} leads added.`, }; } diff --git a/components/leads/campaign-panel.tsx b/components/leads/campaign-panel.tsx index 22a2321c..e3851403 100644 --- a/components/leads/campaign-panel.tsx +++ b/components/leads/campaign-panel.tsx @@ -76,7 +76,11 @@ export function CampaignPanel({ const [pitchIntro, setPitchIntro] = useState(""); const [pitchAsk, setPitchAsk] = useState(""); const [pitchFacts, setPitchFacts] = useState(""); - const [scanProspects, setScanProspects] = useState(true); + // Scanning is off by default now: a project-level campaign is pitching the + // project's own offer, not a CrawlProof audit, so there is nothing for a + // scan of the prospect to contribute. Selecting the audit pitch turns it + // back on, because that pitch is built from findings. + const [scanProspects, setScanProspects] = useState(false); const [senderName, setSenderName] = useState(""); const [replyTo, setReplyTo] = useState(""); diff --git a/components/leads/lead-finder.tsx b/components/leads/lead-finder.tsx index 796c0bca..c5b9c6f8 100644 --- a/components/leads/lead-finder.tsx +++ b/components/leads/lead-finder.tsx @@ -6,9 +6,14 @@ import { findLeadsAction } from "@/app/actions/leads"; /** * The "add leads" box. A search query finds businesses; a directory URL mines - * one page's outbound links. Both queue a free scan per domain, because a - * lead with no report is not a lead — every claim in the eventual email has - * to trace back to one. + * one page's outbound links. + * + * Neither scans the businesses it finds. Scanning exists to supply findings + * for the CrawlProof audit pitch, and a project pitching its own offer has no + * use for them — so firing a scan at every discovered domain would spend + * worker time on evidence nobody cites, and point a scanner at people whose + * only involvement was appearing in a search. A campaign that does pitch an + * audit turns scanning back on explicitly. */ export function LeadFinder({ projectId }: { projectId: string }) { const router = useRouter(); @@ -88,7 +93,7 @@ export function LeadFinder({ projectId }: { projectId: string }) {

{mode === "query" - ? "Finds businesses, then queues a free scan of each so the pitch can cite real findings." + ? "Finds businesses and looks up a contact address for each. It does not scan them." : "Every outbound link on that page becomes a candidate. Platforms and aggregators are filtered out."}

{note &&

{note}

}