Build SOP

Automated "Free Spec Site" Cold Outreach System

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.

How the pipeline works

Five stages, run end to end once a day:

Scrape leads Build site Deploy live Email owner Log + follow up

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.

What you need before you start

Python 3.10+Runs the scraper, builder, and sender
Node.js + npmNeeded for the static-site deploy CLI
A dedicated Gmail accountNever your personal inbox — separate account just for outreach
Gmail App PasswordGoogle Account → Security → 2-Step Verification → App Passwords
Google Sheet + Service AccountFree tracking CRM via the Sheets API
A free static hostCloudflare Pages or Vercel — both have generous free tiers
A web-scraping API keyFirecrawl free tier is enough to start
Before you send a single email: confirm your local/state cold-email rules. In the US this means CAN-SPAM compliance (see the Compliance section below) — this is not optional and not a "fix it later" item.

1Scaffold the project

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.

Prompt to give Claude Code
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.

2Lead scraper

Finds businesses in your chosen niche/city that don't already have a website, and grabs an email address for the owner where possible.

Prompt to give Claude Code
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.

3Tracking sheet (your CRM)

One Google Sheet is your single source of truth — leads, send status, replies.

ColumnPurpose
Business NameFrom the scraper
Owner NameIf found; else blank, never a placeholder
Phone / Email / Address / CityContact + location
NicheDrives which template/copy is used
Site URLFilled in after deploy
Site Sent / Site Sent DateMarked before the send fires, not after (see Gotchas)
Followup CountIncrements per follow-up; cap it (e.g. at 5)
StatusActive / Replied / Dead
Prompt to give Claude Code
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.

4Site builder

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.

Prompt to give Claude Code
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}/.

5Deploy the built site

Push each built site live to a free static host so the link in your email actually works.

Prompt to give Claude Code
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.

6Email sender + sequences

Short, plain, human. You built them something real — say that, and give them the link.

Prompt to give Claude Code
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

7Daily runner + scheduling

Prompt to give Claude Code
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.

Compliance — non-negotiable

CAN-SPAM (US) requires, on every commercial email, regardless of volume:
  • A real physical postal address
  • A clear, working opt-out method, honored promptly
  • A non-deceptive subject line and accurate sender info

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.

Deliverability basics:
  • Always use a dedicated sending account, never your personal or client-facing inbox
  • Warm up a brand-new Gmail account gradually before ramping volume
  • Keep daily volume modest and cap total follow-ups per lead (unanswered follow-up spam is what triggers spam complaints)
  • Check your local/state laws beyond CAN-SPAM if you're outside the US, or emailing internationally

Lessons learned (save yourself the debugging)