Almost nobody hires me because they woke up wanting a redesign. They hire me because something broke: the domain lapsed and the site went dark, a plugin update white-screened the homepage, the SSL certificate expired and browsers started warning customers away, or the “Book Now” button has pointed at a dead page for two years. So I stopped waiting for broken sites to find me, and built a pipeline that goes looking for them.
This is a walkthrough of that system as it runs today: a scanner I call Lead Hunter, a single CRM that acts as the ledger, a cohort gate that refuses to mail bad addresses, Resend for delivery, and a signed webhook that closes the loop. I'll also share the real numbers from the first thousand emails, including the part that didn't go well.
The pitch has to be true, or none of this works
Cold outreach only earns a reply when it's specific. “I can build you a website” goes to trash. “Your contact page returns a 404 and your Facebook link points at a deleted page” gets read, because the owner can verify it in ten seconds. Every architectural choice below follows from one rule: the email must cite something real that I actually found on that business's site.
The offer itself is plain. I do senior web development, repair, and modernization at $40/hour. For owners who just want small edits handled, there's a $10/month AI-assisted website assistant. And for people whose real problem is hosting, there's a $15/month NVMe hosting bundle with email accounts, SSL, backups, and free migration.
Stage 1: Discovery
Lead Hunter is a module inside my job-runner service, and it runs on its own machine, separate from the server that hosts my websites. Scraping is bursty and memory-hungry, and I never want a headless browser competing with production traffic.
It has two discovery sources:
- Local search scraping. A Playwright-driven scanner works through niche + city queries (“plumber Burbank”, “nail salon Pasadena”) and pulls name, category, rating, phone, address, and website for each listing. This produces established businesses with a public presence, which are exactly the people who lose money when their site breaks.
- Certificate Transparency streams. Every publicly trusted TLS certificate is written to append-only CT logs (RFC 6962). A second hunter polls those logs and catches root domains the moment they get a certificate, which often means a brand-new business that hasn't hired anyone yet. These leads are noisy, so they're quarantined in their own collection and never enter the production CRM until they've been vetted.
Stage 2: The flaw audit
Every website gets probed before a human ever sees the lead. The auditor emits typed findings, and the pitch generator is only allowed to reference findings that exist:
- Dead DNS (
dead_dns): the A-record lookup fails inside a 1.5-second timeout. What the owner hears: your website doesn't load at all right now. - Insecure HTTP (
insecure_http): no working HTTPS, or the certificate is invalid. What the owner hears: browsers are showing “Not Secure” to your visitors. - Broken link (
broken_link): internal links are checked for 4xx/5xx responses. What the owner hears: your contact or booking page is returning an error. - Broken social (
broken_social): social profile links resolve to deleted pages. What the owner hears: your Instagram or Facebook link goes nowhere. - Slow server (
slow_server): the response time exceeds the threshold. What the owner hears: your homepage takes several seconds to respond. - Missing feature (
missing_feature): no booking, no mobile viewport, no click-to-call. What the owner hears: customers on phones can't easily reach you.
A separate tech-stack detector fingerprints the CMS, hosting provider, and plugins from headers, script sources, and meta tags. An outdated WordPress install on a bargain shared host is a different conversation than a Wix site that's simply missing a booking form.
One decision I'd make again: a lead with a dead website is not discarded if we still have its phone number or address. Those are the highest-intent prospects in the whole system. Their site is down, and they may not even know.
Stage 3: One business, one record
The same plumber shows up under three search queries, with a phone number formatted four different ways and both a www and an apex domain. Without deduplication you email them three times, and they report you as spam. Every lead is reduced to a deterministic canonical ID before anything is written:
export function getCanonicalId(lead: Partial<LeadTarget>): string {
const phone = extractCanonicalPhone(lead.phone);
const email = normalizeEmail(lead.contactEmail);
const domain = extractApexDomain(lead.domain || lead.url);
if (phone && phone.length === 10) return `phone_${phone}`;
if (email) return `email_${md5(email).slice(0, 12)}`;
if (domain) return `domain_${domain.replace(/[^a-z0-9]/g, '_')}`;
// fallback: normalized business name + ZIP
}
Phone comes first on purpose. A business changes its website and email far more often than its main phone line.
Stage 4: A single CRM as the source of truth
Qualified leads land in a self-hosted Frappe CRM. Every lead, every outbound message, every click, and every scrub is recorded there as a timeline entry. I learned this rule the hard way. Early on, I had leads spread across local JSON files, a SQLite cache, and a retired CRM instance, and reconciling them produced duplicates and double sends. Now there's one CRM, on one machine, and every campaign queries it directly. If it isn't in the CRM, it doesn't get mailed.
Stage 5: The cohort gate
Emails scraped from small-business websites are filthy. Real examples from my data: a phone number glued onto the local part (touch646-983-0085name@…), the word “email” fused to the front of an address, URL-encoded spaces, footer navigation text stuck onto the TLD, and template placeholders like email@mybusiness.com that the owner never replaced.
Every address passes through one sanitizer before it can join a cohort. That sanitizer strips the junk, rejects placeholders and known bounce traps, and finally checks the address against the master scrub list (more on that below). In the first run, the CRM held 28,687 lead records. Only 1,877 survived as unique, plausibly valid business addresses. That ratio alone tells you how much of “lead generation” is really data cleaning.
Stage 6: Sending through Resend
I send from a dedicated domain with its own DNS records: a DKIM public key published as a TXT record, a return-path CNAME so SPF aligns with the visible From domain, and a tracking CNAME for opens and clicks. One gotcha worth sharing is that my old mail. hostname was an A record for inbound mail, and the sending provider wanted mail. as a CNAME. A name can't be both. I moved inbound MX to a new hostname before cutting over, so incoming mail never broke.
The cohort is imported as contacts with custom properties (company name, website, CRM lead ID), and the broadcast template personalizes on those fields. Every message carries a one-click unsubscribe link. After sending, a sync job writes a communication record onto each of the 1,000 CRM leads and marks them contacted, so the next cohort can't include them again.
Stage 7: Listening with a signed webhook
Resend reports delivery events (delivered, opened, clicked, bounced, complained) by POSTing to a webhook on my server. An unauthenticated webhook that can mark leads as lost is a liability, so every request is verified with Svix's HMAC-SHA256 scheme before anything is trusted:
const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const signedContent = `${svixId}.${svixTimestamp}.${rawBody}`;
const expected = crypto.createHmac('sha256', secretBytes)
.update(signedContent, 'utf8')
.digest('base64');
// header may carry several "v1,<sig>" entries during key rotation
for (const entry of svixSignature.split(/\s+/)) {
const [version, sig] = entry.split(',');
if (version === 'v1' && sig && sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return true;
}
}
Three details matter here. First, the HMAC has to run over the raw request bytes. If Express parses and re-serializes the JSON first, whitespace changes and every signature fails. Second, timestamps more than five minutes old are rejected, which blocks replay of captured requests. Third, events are upserted keyed on the svix-id, so Resend's automatic retries never create duplicate CRM entries.
What the first 1,000 emails taught me
- Unique opens: 238 (23.8%). Directional only. Apple Mail Privacy Protection preloads images and inflates opens.
- Unique clickers: 45 (4.5%). The real signal. 42 of them clicked through to the actual offers.
- Bounces: 85 (8.5%), 75 permanent and 10 transient. Too high. Healthy lists stay under about 2%.
- Unsubscribes: 4. Honored automatically and permanently.
- Spam complaints: 0. Specific, truthful pitches don't get reported.
The click numbers made me happy. Each of the three offer links drew roughly 42 to 43 unique leads, so interest was spread evenly across development, the assistant plan, and hosting. Every clicker was logged to their CRM record as a high-intent note for personal follow-up.
The bounce rate did not. An 8.5% bounce rate is the kind of number that gets a sending domain throttled if it repeats. The root cause is clear in hindsight: addresses scraped from site footers are often years stale, especially on the neglected sites this pipeline deliberately targets. The businesses that need me most are, almost by definition, the ones with outdated contact info. That's a structural tension, and the system has to compensate for it.
The scrub list: making every mistake non-repeatable
Cleaning up once is easy. The real work is guaranteeing that an address which bounced or opted out can never come back, whether through a re-scrape, a CRM re-import, or a future campaign. The answer is a cumulative master scrub list that grows every time Resend is cleaned out. Three different writers feed it:
- Post-broadcast cleanup downloads every bounced, unsubscribed, and suppressed recipient, removes them from Resend contacts, adds them to Resend's suppression list, and merges them into the scrub list.
- A standalone sync pulls Resend's suppressions on demand, for anything that changed between broadcasts.
- The webhook itself appends bounces, complaints, and unsubscribes in real time.
The sanitizer from Stage 5 reads that list. A scrubbed address is rejected before it can join any cohort, whatever path it took to get back in. On the CRM side I deliberately do not delete the lead. The business is still real, and its phone number and address are still useful. The bad email is cleared, the status is set to lost, and a timeline comment records exactly which address was removed and why, so the history stays auditable.
Under CAN-SPAM, a commercial sender has up to ten business days to honor an opt-out. With a webhook in the loop, an unsubscribe here takes effect within seconds of the click.
What's next
- Pre-send validation (now live). Every address gets an MX lookup and an SMTP
RCPT TOmailbox probe before it can reach a cohort. Its first pass caught 68 of the 538 addresses queued for the second batch (12.6%) before they were ever imported for sending. - Smaller, steadier batches. A few hundred a day instead of a thousand at once, to build domain reputation gradually.
- Reply-aware follow-up. Inbound replies are already routed to the CRM. The next step is pausing any automated follow-up the moment a human answers.
If you're reading this because your own site is down, throwing errors, or just looks like it's from 2012, you don't need to wait for my scanner to find you. Book a time with me and we'll look at it together.
