Cold email still works — but generic blasts don't. The difference between a 0.5% reply rate and an 8% reply rate is personalization, and that's exactly where AI earns its keep.
In this post I'll walk you through a complete cold email pipeline I built using:
- Google Maps scraping for local business leads
- Claude to generate personalized opening lines
- Resend to deliver the emails
- A simple reply tracking setup
All with code you can run today.
Step 1: Scraping Local Business Leads from Google Maps
Google Maps is a goldmine for local business leads. Here's a lightweight scraper using Playwright:
from playwright.sync_api import sync_playwright
def scrape_google_maps(query: str, limit: int = 50) -> list[dict]:
leads = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(f"https://www.google.com/maps/search/{query.replace(' ', '+')}")
page.wait_for_selector('[role="feed"]', timeout=10000)
results = page.query_selector_all('[role="feed"] > div')
for result in results[:limit]:
try:
name = result.query_selector('a[aria-label]').get_attribute('aria-label')
website_el = result.query_selector('a[data-value="Website"]')
website = website_el.get_attribute('href') if website_el else None
leads.append({"name": name, "website": website})
except:
continue
browser.close()
return leads
leads = scrape_google_maps("plumbers in Austin TX", limit=50)
print(f"Scraped {len(leads)} leads")
This gets you business names and websites. Run it for any niche + city combo. For email addresses, pair it with Hunter.io's domain search API or scrape the contact page directly.
Step 2: Generating Personalized Openers with Claude
Generic openers get ignored. Claude reads the business website and writes something specific to them in under a second.
import anthropic
import httpx
client = anthropic.Anthropic()
def generate_opener(business_name: str, website_url: str) -> str:
try:
resp = httpx.get(website_url, timeout=5, follow_redirects=True)
snippet = resp.text[:2000] # First 2KB is usually enough
except:
snippet = f"Business: {business_name}"
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=150,
messages=[{
"role": "user",
"content": f"""Write a 1-sentence personalized cold email opener for {business_name}.
Website content: {snippet}
Rules:
- Reference something specific from their site
- Sound human, not AI-generated
- No emojis, no "I hope this finds you well"
- Max 25 words
Only output the opener sentence, nothing else."""
}]
)
return message.content[0].text.strip()
# Example output:
# "Noticed you specialize in tankless water heater installs — that's a niche most plumbers skip."
Run this across your lead list and you get personalized first lines at scale — no manual research required.
The key prompt engineering trick here: give Claude the raw website HTML snippet rather than a cleaned version. Claude is good at extracting the signal (services offered, unique angles, customer types) even from messy markup.
Step 3: Sending with Resend
Resend is the cleanest transactional email API available right now. Setup takes 5 minutes, deliverability is excellent, and the free tier covers 3,000 emails/month.
import resend
import time
resend.api_key = "re_YOUR_API_KEY"
def send_cold_email(to_email: str, business_name: str, opener: str) -> dict:
body = f"""Hi there,
{opener}
I build AI-powered automation tools for local service businesses — specifically ones that handle lead follow-up, quote generation, and customer communication automatically.
Would it make sense to jump on a 15-min call this week to see if it's a fit?
Best,
Henry"""
response = resend.Emails.send({
"from": "Henry <henry@yourdomain.com>",
"to": [to_email],
"subject": f"Quick question for {business_name}",
"text": body,
})
return response
# Send with rate limiting to avoid spam flags
for lead in leads:
if lead.get("email"):
result = send_cold_email(lead["email"], lead["name"], lead["opener"])
print(f"Sent to {lead['name']}: {result['id']}")
time.sleep(2) # 2s delay between sends
One thing that matters: send plain text, not HTML. Plain text emails land in primary inboxes far more reliably than HTML newsletters. Resend supports both — use "text" not "html" for cold outreach.
Step 4: Reply Tracking
For quick tracking, poll your inbox via IMAP and match reply subjects against your sent log:
import imaplib
import email as email_lib
def check_replies(mail_host: str, username: str, password: str) -> list[str]:
mail = imaplib.IMAP4_SSL(mail_host)
mail.login(username, password)
mail.select("inbox")
_, msgs = mail.search(None, 'UNSEEN')
replied = []
for num in msgs[0].split():
_, data = mail.fetch(num, '(RFC822)')
msg = email_lib.message_from_bytes(data[0][1])
subject = msg.get('subject', '')
sender = msg.get('from', '')
if subject.startswith('Re:'):
replied.append({"subject": subject, "from": sender})
mail.logout()
return replied
For production, skip IMAP polling and use Resend webhooks instead — they fire on opens, clicks, and replies automatically and are far more reliable.
Putting It All Together
The full pipeline:
- Scrape leads for your target niche + city
- For each lead with a website, call Claude for a personalized opener
- Queue emails and send via Resend with 2s delays between sends
- Check replies daily — move responders to a simple CSV or CRM
Running this against local service businesses (plumbers, electricians, HVAC contractors), I've been seeing 6–9% reply rates on cold outreach without any manual research. The personalized opener is doing the heavy lifting.
The total API cost per lead is roughly $0.001 for Claude + $0.0006 for Resend = under $0.002 per email sent.
Want the Full Browser Automation Kit?
If you want to take this further — automating the lead research, website scraping, and outreach sequencing inside a single persistent agent — I packaged everything up in the Claude Browser Agent Starter Kit.
It includes the complete agent setup, prompt templates, and all the scraping patterns from this post, ready to deploy.
Get the Claude Browser Agent Starter Kit ($7) → https://payhip.com/b/Gu
Seven dollars. If it saves you one hour of setup time, it's paid for itself 10x over.
Questions about the pipeline or the API costs? Drop them in the comments — I check daily.
Top comments (0)