The system, broken into build stages with ready-to-paste prompts for your own Claude Code session. Finds local businesses with no website, auto-builds them a real one, deploys it live, and emails the owner the link — then tracks replies and follows up automatically. Assumes you're already comfortable driving Claude Code; this is the spec, not a tutorial on the tool.
Five stages, run end to end once a day:
You find businesses in a niche with no website, generate a real (not mockup) site for them using a template + their business info, host it on a free static host, then email the owner a link to "the site we built for you" as a low-friction opener. It's a foot-in-the-door offer, not a spam blast — treat every send like it's going to a real person, because it is.
Start a fresh folder and give Claude Code the shape of what you're building so it scaffolds sensibly instead of guessing structure as you go.
Set up a Python project called spec-site-outreach with this structure: - lead_scraper.py (finds businesses with no website in a given niche/city) - site_builder.py (fills an HTML template with a business's info) - deploy.py (pushes the built site to a free static host) - sender.py (sends the outreach + follow-up emails) - sequences.py (holds the email copy templates) - run_daily.py (runs the whole pipeline once a day, end to end) - templates/ (HTML site templates, one per niche style) - sites/ (output folder for built sites, gitignored) - config.json (API keys, sending account, daily limits — gitignored) - requirements.txt Add a .gitignore that excludes config.json, sites/, any *.token/*.pkl files, and local logs.
Finds businesses in your chosen niche/city that don't already have a website, and grabs an email address for the owner where possible.
Write lead_scraper.py using Playwright to search Google Maps for businesses matching a --niche and --city argument. For each result: - Skip it if it already has a website listed - Pull business name, phone, address, and Google Maps profile URL - Try to find a personal-domain email (gmail/yahoo/hotmail/outlook/icloud) via a web search/scrape API — skip the lead entirely if no email is found - Deduplicate against rows already in the tracking sheet by business name - Write new leads as rows in the tracking sheet Add a --headless flag and a --max flag to cap how many leads it pulls per run.
Tip: niches with low website adoption (handyman, cleaning, landscaping, pressure washing, small trades) yield far more usable leads per run than niches where most businesses already have a site.
One Google Sheet is your single source of truth — leads, send status, replies.
| Column | Purpose |
|---|---|
| Business Name | From the scraper |
| Owner Name | If found; else blank, never a placeholder |
| Phone / Email / Address / City | Contact + location |
| Niche | Drives which template/copy is used |
| Site URL | Filled in after deploy |
| Site Sent / Site Sent Date | Marked before the send fires, not after (see Gotchas) |
| Followup Count | Increments per follow-up; cap it (e.g. at 5) |
| Status | Active / Replied / Dead |
Set up Google Sheets access using a service account (not user OAuth) via gspread. Write a small sheet_client.py wrapper with read_rows(), append_row(), and update_cell() helpers, with retry + exponential backoff around every network call so a flaky connection doesn't crash a run mid-pipeline.
One clean HTML template per niche style, filled with placeholders. Keep it simple — a real site with the business name, city, services, and a contact section beats anything flashy.
Build a single responsive HTML template (templates/general.html) for a small
local-service business landing page: hero with business name + niche + city,
a services section, and a contact/call section. Use placeholders like
{{BUSINESS_NAME}}, {{CITY}}, {{NICHE_DISPLAY}}, {{SERVICE_1}}, {{SERVICE_2}},
{{SERVICE_3}}, {{PHONE_DISPLAY}}, {{PHONE_DIGITS}}, {{YEAR}}.
If phone digits are missing or too short, hide phone elements with JS and
point the call-to-action at the contact section instead — the site should
never render broken if a field is missing.
Then write site_builder.py that takes a row of business data, fills the
right template based on niche, and saves the output to sites/{slug}/.
Push each built site live to a free static host so the link in your email actually works.
Write deploy.py that takes a sites/{slug}/ directory and deploys it to
[Cloudflare Pages via wrangler CLI / Vercel CLI] — return the live URL on
success. Handle non-UTF8 CLI output safely on Windows, and treat a deploy
failure as skip-and-retry-next-run rather than a crash.
Either host works fine at this volume. Cloudflare Pages and Vercel both have free tiers with effectively unlimited static deploys.
Short, plain, human. You built them something real — say that, and give them the link.
Write sequences.py with an initial outreach email and up to 4 follow-up
variants (short, plain text, no salesy formatting). Every template must
include a real physical mailing address and a one-line opt-out
("reply STOP/unsubscribe and I'll take you off this list") — this is a
CAN-SPAM requirement, not optional copy.
Write sender.py that:
- Sends via the dedicated Gmail account (SMTP + app password, or Gmail API)
- Marks "Site Sent" = Y in the sheet BEFORE sending, not after
(so a crash mid-send can't cause a duplicate email on the next run)
- Spaces sends across a daily window (e.g. 9am-6pm) instead of firing
them all at once
- Enforces a hard daily send cap
- Logs every send locally to a JSONL file immediately, independent of the
sheet, so a crashed process still has a ground-truth record of what
actually went out
- Increments Followup Count on each follow-up and marks Status = Dead once
a lead hits the cap, skipping it on all future runs
Write run_daily.py that in one run: tops up new leads if the pipeline is low, sends follow-ups to qualifying leads, then sends new outreach up to the daily cap. Add --dry-run (must not send anything for real), --status (prints pipeline counts only), and --force (bypass the already-ran-today guard). Wrap the whole run in retry/backoff for network calls.
Schedule it with Windows Task Scheduler (or cron on Mac/Linux) to run once a day. Start conservative — 10-15 sends/day per account while you dial in copy and deliverability.
The practical risk of skipping this isn't a lawsuit — it's your sending account getting reported and suspended, which kills the whole system overnight. Bake it into the templates from day one.
errors="replace") or you'll get random UnicodeEncodeError crashes on Windows.