Med Spa Agency
Inside the box
This is the real product tree — open files are free to read; the rest unlocks when you become CEO.
The file tree
Every file below ships in the box. 📄 marked files are previewed in full further down this page. 🔒 marked files are the paid product — the operator brain, the prompt pack, the sales scripts, the fulfillment templates.
med-spa-agency/ ├── START_HERE.md 📄 preview available ├── CLAUDE.md 🔒 Unlocks with purchase ├── config/ │ └── business.yaml 🔒 Unlocks with purchase ├── scraper/ │ ├── build-list-with-claude.md 🔒 Unlocks with purchase │ ├── leads.py 🔒 Unlocks with purchase │ ├── manual-list-template.csv 🔒 Unlocks with purchase │ ├── README.md 📄 preview available │ └── requirements.txt 🔒 Unlocks with purchase ├── agency-site/ │ ├── fill.py 🔒 Unlocks with purchase │ ├── index.html 🔒 Unlocks with purchase │ ├── README.md 🔒 Unlocks with purchase │ ├── script.js 🔒 Unlocks with purchase │ ├── style.css 🔒 Unlocks with purchase │ ├── EXAMPLE/ │ │ ├── daylight/ │ │ │ ├── index.html 🔒 Unlocks with purchase │ │ │ ├── script.js 🔒 Unlocks with purchase │ │ │ └── style.css 🔒 Unlocks with purchase │ │ └── eventide/ │ │ ├── index.html 🔒 Unlocks with purchase │ │ ├── script.js 🔒 Unlocks with purchase │ │ └── style.css 🔒 Unlocks with purchase │ └── variant-b/ │ ├── fill.py 🔒 Unlocks with purchase │ ├── index.html 🔒 Unlocks with purchase │ ├── script.js 🔒 Unlocks with purchase │ └── style.css 🔒 Unlocks with purchase ├── fulfillment/ │ ├── calcom-setup.md 🔒 Unlocks with purchase │ ├── care-plan-one-pager.md 🔒 Unlocks with purchase │ ├── care-plan-sop.md 🔒 Unlocks with purchase │ ├── CLAIMS.md 🔒 Unlocks with purchase │ ├── delivery-checklist.md 🔒 Unlocks with purchase │ ├── publish-a-demo.md 🔒 Unlocks with purchase │ ├── README.md 🔒 Unlocks with purchase │ ├── reminder-system.md 🔒 Unlocks with purchase │ ├── reactivation-campaigns/ │ │ ├── campaign-sop.md 🔒 Unlocks with purchase │ │ ├── message-templates.md 🔒 Unlocks with purchase │ │ ├── one-pager.md 🔒 Unlocks with purchase │ │ └── README.md 🔒 Unlocks with purchase │ ├── review-engine/ │ │ ├── ask-flow.md 🔒 Unlocks with purchase │ │ ├── bad-reviews.md 🔒 Unlocks with purchase │ │ ├── make_qr.py 🔒 Unlocks with purchase │ │ ├── monthly-cadence.md 🔒 Unlocks with purchase │ │ ├── one-pager.md 🔒 Unlocks with purchase │ │ ├── qr-card.html 🔒 Unlocks with purchase │ │ ├── README.md 🔒 Unlocks with purchase │ │ └── requirements.txt 🔒 Unlocks with purchase │ ├── website-template/ │ │ ├── index.html 🔒 Unlocks with purchase │ │ ├── README.md 🔒 Unlocks with purchase │ │ ├── EXAMPLE/ │ │ │ ├── index.html 🔒 Unlocks with purchase │ │ │ └── README.md 🔒 Unlocks with purchase │ │ └── photos/ │ │ └── README.md 🔒 Unlocks with purchase │ └── website-template-b/ │ ├── index.html 🔒 Unlocks with purchase │ ├── README.md 🔒 Unlocks with purchase │ ├── EXAMPLE/ │ │ ├── index.html 🔒 Unlocks with purchase │ │ └── README.md 🔒 Unlocks with purchase │ └── photos/ │ └── README.md 🔒 Unlocks with purchase ├── sales/ │ ├── call-script.md 🔒 Unlocks with purchase │ ├── objections.md 🔒 Unlocks with purchase │ └── outreach.md 🔒 Unlocks with purchase ├── prompts/ │ ├── 01-prep-calls.md 🔒 Unlocks with purchase │ ├── 02-research-lead.md 🔒 Unlocks with purchase │ ├── 03-fulfill.md 🔒 Unlocks with purchase │ ├── 04-invoice.md 🔒 Unlocks with purchase │ ├── 05-weekly-review.md 🔒 Unlocks with purchase │ └── 06-upsell.md 🔒 Unlocks with purchase ├── playbook/ │ ├── growth.md 🔒 Unlocks with purchase │ ├── month-1.md 🔒 Unlocks with purchase │ └── week-1.md 📄 preview available ├── docs/ │ ├── connect-claude.md 📄 preview available │ ├── FIELD_TEST.md 📄 preview available │ ├── rules-of-the-road.md 🔒 Unlocks with purchase │ ├── see-it-first.md 🔒 Unlocks with purchase │ ├── setting-up-shop.md 🔒 Unlocks with purchase │ ├── what-you-will-see.md 🔒 Unlocks with purchase │ └── your-phone-setup.md 🔒 Unlocks with purchase ├── office/ │ ├── client-tracker.csv 🔒 Unlocks with purchase │ ├── data-handling-addendum.html 🔒 Unlocks with purchase │ ├── data-handling-addendum.md 🔒 Unlocks with purchase │ ├── do-not-call.md 🔒 Unlocks with purchase │ ├── EXAMPLE-call-list.md 🔒 Unlocks with purchase │ ├── EXAMPLE-client-tracker.csv 🔒 Unlocks with purchase │ ├── EXAMPLE-invoice.html 🔒 Unlocks with purchase │ ├── EXAMPLE-proposal.html 🔒 Unlocks with purchase │ ├── EXAMPLE-service-agreement.html 🔒 Unlocks with purchase │ ├── intake-form.html 🔒 Unlocks with purchase │ ├── invoice-template.md 🔒 Unlocks with purchase │ ├── invoice.html 🔒 Unlocks with purchase │ ├── no-show-log.csv 🔒 Unlocks with purchase │ ├── onboarding-emails.md 🔒 Unlocks with purchase │ ├── proposal-onepager.html 🔒 Unlocks with purchase │ ├── quoting-guide.md 🔒 Unlocks with purchase │ ├── README.md 🔒 Unlocks with purchase │ ├── recurring-services-agreement.html 🔒 Unlocks with purchase │ ├── recurring-services-agreement.md 🔒 Unlocks with purchase │ ├── service-agreement.html 🔒 Unlocks with purchase │ ├── service-agreement.md 🔒 Unlocks with purchase │ └── tracker-guide.md 🔒 Unlocks with purchase ├── LICENSE.md 🔒 Unlocks with purchase └── README.md 🔒 Unlocks with purchase
The open files
Read them in full — this is the same text a CEO gets. Nothing here is a mockup.
📄 START_HERE.md
Open file
START HERE — Your Med Spa Web Agency
You just bought a starter system — not a business with customers, and not an idea either. It's the whole machine: a lead scraper, a call script, a premium website product to sell, the prompts and playbooks to run it, and an operator brain your Claude loads automatically. There's no revenue in the box. You bring the calls; everything else is here.
The business: local med spas and aesthetic clinics sell high-ticket services — injectables, laser, skin treatments, a single appointment can run $500+ — and most of them run on Instagram plus a website that looks like everyone else's, with no real way to book. You sell them a premium, mobile-first website with a consult-booking funnel, built with AI and live the same week. Setups like this commonly go for $1,000–$2,500 one-time (market rates, not promises — your calls and your market set your results). After a happy delivery you offer the upsells: a Google review engine, client reactivation campaigns, and a $199–$499/mo care plan. That recurring part is the real business.
You're the CEO. Your Claude is the operator.
Set your expectations before you start
Day 1 is setup and building your call list. Day 2 is calling it.
Today is roughly two hours: look at what you bought, install Claude Code and Python, fill in one config file, sort out a phone number, and run the scraper. Nobody gets dialled today, and that is the plan working — not a delay. A CEO who expected to be selling within thirty minutes feels lied to by lunchtime; one who was told the truth feels on schedule.
What you actually own, in writing — what you may do with these files, what we promise and what we deliberately don't — is
LICENSE.md. Two minutes, worth reading once.
Here's day one:
0. See what you bought (~20 min, no setup)
Before any of the below: double-click docs/see-it-first.md and open the
nine finished files it lists. A filled client website for a made-up spa, both
designs of your own agency site already built, the proposal, the agreement, the
invoice, the intake form, a call list, and the licence that says what you own.
No terminal, no Claude, no install — just look.
It is much easier to sell something you have seen. Everything in that list is clearly labelled as a demo, the businesses in them are invented, and this kit has never signed a client — every example page says so on itself.
Lost about where something lives rather than stuck on a step? README.md in
this folder is a one-page map of every folder in the box.
1. Connect your Claude and install Python (~25 min, one time)
Follow docs/connect-claude.md — it assumes you've never touched a
terminal and walks you through installing Claude Code and opening this folder,
screen by screen, including exactly what Claude Code asks you the first time
you run it.
You're done when you ask Claude "what business is this?" and it answers like it runs the place.
Nervous about the terminal? docs/what-you-will-see.md is every screen of
day one printed in advance, with "good news / bad news / just noise" written
next to each one. The most expensive thing that can happen today is that
something works fine, you don't recognise it, and you assume you broke it.
⚠️ Do not skip Step 2.5 of that guide — "Install Python". The lead scraper
is a small Python program and it is the first thing you run. Installing Python
is the single most common place people get stuck, it takes five minutes, and
connect-claude.md walks it through for Mac, Windows and Linux with the two
error messages that aren't errors. Skip it now and you will meet it in ten
minutes anyway, without the instructions.
2. Make it yours — name it, then change the words (~5 min now, more later)
Open config/business.yaml in any text editor (or just tell Claude your
details and let it fill the file). Your name, your agency name, your city and
state, phone, email. Keep the pre-filled prices — they're market-rate defaults.
Read this once, because it changes how you use everything else. This kit is sold non-exclusively. Anyone can buy it. There is no territory, we never promised you one, and we couldn't enforce it if we had. What you bought is the machine — and the whole point is that you put your own name on it.
That is not a consolation prize, it's the instruction. Your edge is your name, your city and the calls you make, not owning files nobody else can get.
So don't stop at the config file. Over your first week, replace:
- Your agency name and domain —
config/business.yamlfeeds them into every document automatically. - Your prices — the defaults are market rates, not a price list you're
bound to.
office/quoting-guide.mdshows you how to move them. - Your storefront copy —
agency-site/ships finished words so you have something to deploy tonight. They are a starting draft, not your voice. Rewrite the headline and the about section at minimum (seeagency-site/README.md, "Make it yours"). This niche judges you on polish and sameness reads as a template. - Your call opener —
sales/call-script.mdis a script to learn, then loosen. It should sound like you by call fifty. - The demo names — everything in the
EXAMPLE/folders is an invented business for you to look at. Never deploy one, never send one to a prospect.
Ask your Claude: "help me rewrite the agency site copy in my own voice" — it has all of this in front of it.
2.5 Sort out a phone number (~20 min, $0–$20/mo) — before you dial anything
docs/your-phone-setup.md. Read it today, not after your answer rate
drops.
US carriers run software that watches call patterns, and a personal cell that suddenly makes 25 outbound calls a day to strangers looks exactly like a spam robot to it. Once it decides, your number shows up on the other person's screen as "Spam Likely" and a large share of people never pick up again. It hits this niche hard: a spa's front desk is already screening calls to protect a provider mid-treatment, and a spam label makes that decision for them.
Twenty minutes and $0–$20/month buys a dedicated business line with your city's area code and your agency's name on the caller ID. The doc covers the setup, the free caller-ID registration, the two-week volume ramp, and what to do if it happens to you anyway.
3. First scrape (~5 min)
Tell Claude: "Run the lead scraper for my city."
Or run it yourself from this folder:
cd scraper
python3 -m pip install -r requirements.txt
python3 leads.py --city "Your City, Your State"
Med spas are a thinner niche than plumbers — one city often comes back with 10–25 leads, so the standard move is two or three neighboring cities in ONE run (one merged, deduped file):
python3 leads.py --city "Your City, Your State" --city "Next Town, Your State"
Free data, no API key, no account. Out comes scraper/leads.csv — the med
spas and aesthetic clinics near you, ranked by how badly their web presence
needs you — plus scraper/lookup-list.md for every lead whose phone number
the map data didn't have (a pre-built Google Maps link each, ~30 seconds to
fill in), and scraper/last-run.txt, which is how your Claude knows which file
the run just wrote.
If you use
--outto keep per-city files, that's fine. The run records the path inscraper/last-run.txtand prompts 01, 02 and 03 read it before they read any CSV, so they always work on the file you actually just made. (Before 2026-08-04 they didn't, and a run written toboise.csvleft the prompts quietly reading whatever stale file was still calledleads.csv.)
What this actually returns — read this before you judge your market
Here is every med-spa run we have measured, in full, including the bad ones.
Every row is a real run of this exact script by us; the write-ups are in
docs/FIELD_TEST.md:
| Metro (all cities merged in one run) | Rows | Dialable (has a phone) |
|---|---|---|
| Scottsdale + Tempe + Mesa, AZ | 47 | 4 |
| Boise metro, ID (4 cities, ~800k people) | 16 | 6 |
| Charlotte, NC (15th-largest US city, single city) | not recorded | 5 |
| Naples + Fort Myers + Cape Coral + Bonita Springs, FL | 34 | 5 |
| Sarasota + Bradenton + Venice + Lakewood Ranch, FL | 1 | 0 |
That is the honest range, and the bottom of it is zero. Sarasota is a wealthy 800,000-person metro with a great many real med spas in it; free map data has almost none of them. The scraper is not broken when this happens and your market is not empty — OpenStreetMap is drawn by volunteers and its coverage of small private clinics is thin and wildly uneven.
So the list you dial in week one is usually one you and your Claude built, not one the scraper handed you. That is the normal path, not the rescue path. Budget an evening for it. Two free tools do it, and neither needs a card:
scraper/lookup-list.md— every mapped lead whose number the map didn't have, with a pre-built Maps link each. Your Claude can work the whole file.scraper/build-list-with-claude.md— a ready-made prompt where your Claude searches the web and builds the list from scratch intomy-list.csv, which--from-csvthen scores identically.
Run the scraper first anyway: it costs five minutes, it sometimes hands you thirty rows, and it produces the lookup list that makes step 1 fast.
How long it takes. "~5 min" is the good case. The map step is 20–60 seconds when the free servers are healthy and can take several minutes when they're busy — the run tells you which is happening. Then it visits each lead's website, about a second or two apiece, so a 40-lead city is genuinely a few minutes more. It is not stuck, and it prints an estimate before it starts.
The two loudest things it prints at the end — neither is an error
The run finishes with a boxed number and a verdict, and both are meant to be blunt:
===============================================
DIALABLE NOW: 4
===============================================
(rows that have a phone number. This is the only number that
matters today ...)
DIALABLE NOW is rows with a phone number, and it is the only count that
decides your morning. A file of 47 rows with 4 numbers in it is a four-call
morning, not a forty-seven-call one.
Then, if that number is under 25, a ~15-line VERDICT block appears saying
"not a morning of dialling yet" with a four-step recovery plan. That is
the kit working, not the kit breaking. Med spas are thinly mapped, free map
coverage varies enormously, and the honest thing to tell you is the number you
actually have. The first three steps in that block are free and need no card —
step one, working the lookup list, is usually the biggest win. Every screen
you'll see, annotated line by line: docs/what-you-will-see.md.
Two columns do the work. weak_web_score is how weak their web presence is —
100 at the top, and the top is where you start. has_online_booking is the
second pitch in this business, and it is honest about its own limits:
| It says | It means |
|---|---|
no |
The scraper loaded their homepage and found no way to book. Your line is "I couldn't find a way to book with you online" — a question, never an accusation |
yes |
It found a booking platform or a book-now button. Pitch the site, not the funnel |
unknown |
We don't know. No website in the map data, or the site wouldn't load. Say nothing about booking to these — you have no information |
Expect a lot of unknown on your first run; most map rows have no website
recorded at all. It reads the homepage only, so a booking link buried three
pages deep is invisible to it. Your Claude re-checks properly, per lead,
before you dial (prompts/02-research-lead.md). Full detail:
scraper/README.md, "The booking column".
Map came back thin or empty? That happens — OpenStreetMap is drawn by
volunteers and some towns are barely on it. It is not a dead end and it does
not need a credit card: scraper/build-list-with-claude.md has your Claude
build the list from web searches instead, and the scraper scores it exactly
the same way (--from-csv). About ten minutes for 15–30 real names.
4. First calls (tomorrow morning)
Tell Claude to run prompts/01-prep-calls.md — it turns the CSV into a
ranked call list with a personalized opener for every lead. Print
sales/call-script.md, keep sales/objections.md open next to it,
and dial 10–15 numbers on your first day — not 25. That is the phone-safety
ramp in docs/your-phone-setup.md, and going faster on a brand-new number is
how it gets flagged "Spam Likely" in week one. Best windows are 10am–12pm and
2–4pm, Tuesday to Friday (plenty of spas close Mondays). A front desk will
answer most calls; the script handles that. Most won't connect, a few will
talk, and that's exactly on track.
One honest heads-up before you dial: map data holds the clinic far more often
than its phone number, so most of your hottest leads land in
scraper/lookup-list.md without one. Work that file first — click the
pre-built Maps link, read the number off the listing, ~30 seconds each — or
hand it to Claude. Never dial a number you didn't verify.
Second heads-up: the scripts say [FIRST NAME] and the scraper cannot give
you one. Map data has business names, not owners' names. Every opener and
template in sales/ ships with a no-name greeting as well as a named one, and
the no-name version is the one you'll use most, especially on the first dial
where a front desk answers anyway. Your Claude finds names where it can during
call prep and marks the rest unknown; a wrong name is worse than no name.
Third heads-up, and it is the one nobody expects: your openers are graded
A / B / C / D by what somebody actually checked, and on day one almost all of
them are C — the opener that claims nothing and asks instead. That is by
design. sales/call-script.md explains all four in its first two pages, and
your Claude labels every lead on the call list with the right letter.
Fourth: in some metros, most of your leads will have a perfectly good
website. We have measured metros where every checkable lead had a working
site with live booking. That is a fact about that market, not a failed scrape,
and it does not mean there's no business there — it means you sell them
something else. sales/call-script.md, "Branch D", is the whole playbook for
it: ask what they'd been meaning to change, quote a scoped page, sell a
standalone module, or ask for a referral and move on. Do not let anyone —
including your Claude — talk you into telling a spa owner her good website is
bad.
Your full first week — day by day to a first close — is
playbook/week-1.md. Your Claude knows every step. From here on, just
ask it: "what's next?"
5. When someone says yes — the paperwork comes first
This is the part first-time CEOs skip and regret. Every one of these already
exists, filled-in-and-send-ready, in office/. Your Claude fills them in five
minutes; you never write one from scratch.
| The moment | The document | Why it exists |
|---|---|---|
| They say yes to the build | office/service-agreement.md (printable: .html) |
Scope, one revision round, who owns what, the deposit, and an explicit no-guaranteed-results clause. Signed agreement + deposit = build starts. Both, not either. |
| Before any monthly charge | office/recurring-services-agreement.md |
Care plans and modules bill a card every month. This defines what's included, "unlimited reasonable edits", 30-day price-change notice, cancellation, and what the client keeps when it ends. A Stripe subscription with nothing signed is a chargeback waiting to happen. |
| Before a client list touches your laptop | office/data-handling-addendum.md |
Reactivation campaigns move real customer names and numbers. This is consent, minimisation, deletion terms, laptop safety, the honest AI-processing disclosure — and the six triggers where you stop and ask a lawyer. |
| Before the site goes live | fulfillment/CLAIMS.md |
The ten statements the website makes about your client's business. Every one is false until they confirm it in writing. In this niche an unconfirmed "licensed providers" line is an advertisement a state board can read, published under their name, on a site you built. Blocking. Two greps. Zero unconfirmed hits or it doesn't launch. |
Two rules before your first dial, and they're forever: anyone who says "stop
calling" goes in office/do-not-call.md immediately and is never contacted
again — and in this niche, never write a word that promises what a treatment
will do. No spam, no fake reviews, no medical claims, no unverified
credentials, deliver what you sell. Your name is the business — protect it.
Go make the calls.
📄 docs/connect-claude.md
Open file
Connect Your Claude — Zero-Knowledge Guide
This kit is built to be run by your Claude. You plug in your own Claude account,
open this folder, and the operator brain (CLAUDE.md) loads automatically — your
Claude instantly knows the business, the tools, and the day-one plan.
Never touched a terminal? That's fine. Every step is written for you. Total time: about 25 minutes. (There's also a no-terminal fallback at the bottom.)
The five things you're setting up, so nothing is a surprise:
| Step | What | Why | Time |
|---|---|---|---|
| 1 | A Terminal window | The place you type commands | 1 min |
| 2 | Claude Code | Your Claude, running in this folder | 5 min |
| 2.5 | Python | The lead scraper is a Python program. Nothing else in the kit needs it | 5 min |
| 3 | A Claude account | Claude Code needs to log in | 5 min |
| 4–5 | Open the folder, first run, verify | Proves the operator brain loaded | 5 min |
Nothing here costs money except your Claude plan. Python and Claude Code are free downloads.
Want to see all of it before you touch anything? docs/what-you-will-see.md
is every screen of day one printed in advance, with a note beside each saying
whether it's good news, bad news, or just noise.
Step 1 — Open a Terminal
- Mac: press
Cmd + Space, typeTerminal, hit Enter. - Windows: press the Windows key, type
PowerShell, hit Enter.
A window with a text prompt appears. That's it — you type commands, it does them.
Step 2 — Install Claude Code
Claude Code is Anthropic's official command-line app. It's what lets your Claude read this folder, run the scraper, and build client sites.
Mac (paste into Terminal, press Enter):
curl -fsSL https://claude.ai/install.sh | bash
Windows (paste into PowerShell, press Enter):
irm https://claude.ai/install.ps1 | iex
Prefer an installer-manager? Mac Homebrew users can run brew install --cask claude-code;
Windows users can run winget install Anthropic.ClaudeCode.
When it finishes, check it worked:
claude --version
You should see a version number followed by (Claude Code). If the install command
errors, close the terminal, open a fresh one, and try again — or see Anthropic's
install troubleshooting at https://code.claude.com/docs (search "troubleshoot installation").
Windows note: installing Git for Windows is recommended — it gives Claude Code a proper shell for running the scraper.
Step 2.5 — Install Python (do NOT skip this)
The lead scraper (scraper/leads.py) is a small Python program. Python is a
free programming-language runtime. Nothing else in this kit needs it — but the
scraper is day one, so five minutes now saves you an hour tomorrow.
First, check what you already have
Mac / Linux:
python3 --version
Windows (PowerShell):
py -3 --version
What you want to see: Python 3.9.6, Python 3.12.4 — any 3. number is
fine. (The Mac this kit was field-tested on runs the stock Python 3.9.6. It
works; see the warning note below.)
What means "not installed":
command not found/not recognized→ install it below.- On Windows, a Microsoft Store page opens, or nothing happens → that's the
fake
python3stub Windows ships. It is not Python. Install it below. Python 2.7.x→ too old. Install it below.
Install it — Mac
- Go to https://www.python.org/downloads/macos/
- Click the "Download Python 3.x.x" button.
- Open the downloaded
.pkgand click through the installer. - Close your Terminal window and open a fresh one (it only notices new programs on startup).
- Check again:
python3 --version
Homebrew users can run brew install python instead.
Install it — Windows
winget install Python.Python.3.12
Then close PowerShell, open a fresh one, and check py -3 --version.
No winget? Use the installer from
https://www.python.org/downloads/windows/ and on the first screen tick
"Add python.exe to PATH". That box is the difference between this working
and not working.
⚠️ Windows: type
py -3, notpython3. Everywhere this kit sayspython3 something, you typepy -3 something. Tell your Claude "I'm on Windows" once and it will fix the commands for you from then on.
Install it — Linux (Ubuntu / Debian)
sudo apt update && sudo apt install python3 python3-pip python3-venv
Now install the one thing the scraper needs
From the kit folder:
cd scraper
python3 -m pip install -r requirements.txt
(Windows: py -3 -m pip install -r requirements.txt)
Three outcomes:
1. It works. Successfully installed requests... or Requirement already satisfied. Done.
2. error: externally-managed-environment. Nothing is broken. Newer Macs
and Linux protect the system Python and want programs kept in their own little
box. Make the box — three lines, one at a time:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
(Windows: py -3 -m venv .venv, then .venv\Scripts\activate, then
py -3 -m pip install -r requirements.txt)
Your prompt now starts with (.venv) — that means the box is open. Every
future terminal that runs the scraper needs source .venv/bin/activate
from the scraper folder first. Forget it and you'll see "Missing dependency:
requests" — that's the reminder, not a failure. Tell your Claude "I set up a
venv in the scraper folder" and it will handle it for you.
3. Any other error. Copy the whole error text, paste it to your Claude, say "walk me through fixing this, one step at a time."
The macOS warning that is not an error
On a stock Mac, almost every scraper run starts with this, in scary-looking form, before anything else happens:
/Users/you/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
warnings.warn(
😐 Ignore it. Every time. Apple ships an older security library with the
system Python, and one of the scraper's dependencies grumbles about it on
startup. It is a warning, not an error: the very next line will be
[1/4] City: "..." and your scrape runs completely normally. Nothing you do
will make it go away short of installing a newer Python, and you don't need
to. Every run of the field test printed this line and every run worked.
The way to tell a warning from an error, forever: after a warning the program keeps printing. After an error it stops.
Prove it works before you move on
python3 leads.py --help
A page of options starting with usage: leads.py means Python is done, and
that was the hardest technical part of this entire kit.
Step 3 — Get an Account (Two Roads, Pick One)
Claude Code needs a Claude account. Either road works for this kit:
Road A — Claude subscription (simplest). If you have (or sign up for) Claude Pro, Max, Team, or Enterprise at https://claude.com/pricing, you log in with that account and you're done — no API key, no separate billing. Usage is included in your subscription's limits. Recommended for most CEOs.
Road B — Anthropic API key (pay as you go).
- Go to https://console.anthropic.com and create a Console account.
- Add a small amount of prepaid credit ($5–$10 is plenty to start).
- You can simply log in with this Console account when Claude Code asks (easiest),
or create a key under API Keys in the Console and set it as the
ANTHROPIC_API_KEYenvironment variable if you know what that means. If you don't — just log in; Claude Code handles it.
Step 4 — Open This Kit Folder and Log In
In your terminal, move into the kit folder, then start Claude:
cd path/to/med-spa-agency
claude
Replace path/to/med-spa-agency with wherever you unzipped this kit. Easy trick:
type cd (with the space), then drag the kit folder from Finder/File Explorer
onto the terminal window — it pastes the path for you. Press Enter.
What the first run actually asks you
This is the screen nobody warns people about, so here it is in advance. The
first time you type claude, it walks you through a short setup before
you can type anything. Four or five questions, about ninety seconds.
Claude Code updates often. What's below is the shape of each question and the answer to give — the exact wording and the order can shift between versions. If you see a question that isn't here, read it: they are all plain English, and none of them can break anything.
1. Pick a colour theme. Something like "Choose the text style that looks
best with your terminal", with a few options (dark, light, and colourblind-
friendly versions of each). Move with the arrow keys, choose with
Enter. Purely cosmetic. Pick anything; you can change it later with
/config.
2. Choose how you log in. Two options, matching the two roads in Step 3:
- Claude account with subscription → Road A (Pro / Max / Team). Pick this one if you pay Anthropic monthly.
- Anthropic Console account → Road B (pay-as-you-go API credit).
Arrow keys, Enter.
3. Your browser opens by itself. A Claude sign-in / authorise page. Log in if you aren't already, click the button that grants access, and you'll get either an automatic return to the terminal or a code to copy and paste back into the terminal. If the browser doesn't open on its own, the terminal prints the URL — copy it into your browser manually. When it lands you'll see a success line and a "press Enter to continue" style prompt.
Nothing here asks for a credit card. Road A bills through your existing subscription; Road B through credit you already loaded in Step 3.
4. "Do you trust the files in this folder?" This one matters, so read it
properly. Claude Code shows you the folder path — check that it's your
med-spa-agency folder — and asks whether you trust its contents, because
opening a folder means it can read the files in it. Options are along the
lines of "Yes, proceed" / "No, exit".
Answer yes for this folder. It's the kit you just bought: text files, markdown and one Python script. You'll see this question once per new folder, forever — that's the point of it. If a path you don't recognise ever shows up in that box, answer no.
5. Possibly a terminal-setup offer (something about configuring your terminal for multi-line input, or notifications). Either answer is fine. Yes is convenient; no changes nothing about the business.
Then the welcome panel appears: the Claude Code banner, the folder you're in
(check it says med-spa-agency), and a prompt box waiting for you at the
bottom — usually a > with a hint like "Try..." inside it. A line at the
bottom tells you /help exists.
That box is where you talk to your Claude. You type ordinary sentences, in English, and press Enter. That's the whole interface.
You only do the login part once. To switch accounts later, type /login
inside Claude Code. /exit quits. /status shows which account you're on.
Step 5 — Verify the Brain Loaded, and send a first message
You should see the Claude Code prompt with this folder as the working directory. Now the moment of truth. Type:
what business is this?
A connected Claude answers something like: "This is your med spa web agency — you sell premium websites + booking funnels ($1,000–$2,500 setup) to local med spas and aesthetic clinics, with review engine, reactivation campaign, and care plan upsells. Want to start with the day-one sequence?"
If you get that: you're live. Say let's do day one and Claude takes it from
there (setup, config/business.yaml, first scrape — the full sequence is in
playbook/week-1.md).
If Claude answers generically instead: you're in the wrong folder. Type /exit,
then redo Step 4 making sure you cd into the folder that contains CLAUDE.md
before running claude.
What talking to it actually looks like
Three things to know before your first real message, because all three surprise people:
- You type English, not commands. "Run the lead scraper for my city and
tell me what came back" is a perfectly good instruction. There's no syntax
to learn. The
/-words (/help,/login,/exit,/status) are the only special ones. - It will ask permission before it does things. The first time it wants to run a command or change a file, a box appears asking you to approve it, usually with a "yes, and don't ask again for this" option. Read what it's about to do, then approve. This is a safety feature, not an error — and there will be a few of these on day one, then far fewer.
- It thinks out loud and takes its time. A scrape takes a couple of minutes; you'll see status lines while it works. Let it finish.
A good first message once you're verified:
let's do day one
A good second one, the next morning:
what's next?
Troubleshooting
| Symptom | Fix |
|---|---|
claude: command not found |
Close the terminal, open a new one, retry. Still failing → redo Step 2. |
irm is not recognized (Windows) |
You're in CMD, not PowerShell. Open PowerShell (prompt starts with PS). |
| Login loop / wrong account | Type /login inside Claude Code and pick the right account. /status shows which one you're on. |
| The browser never opened during login | The terminal also prints the URL — copy it into your browser by hand. |
| It's asking me to trust a folder I don't recognise | Answer no. Then check you cd'd into the right place and start again. |
| Claude doesn't know the business | Wrong folder — cd into the folder containing CLAUDE.md, run claude again. |
| It keeps asking permission for things | Normal, and it settles down. Read the box, approve, and use the "don't ask again" option for commands you expect (like running the scraper). |
python3: command not found / 'python3' is not recognized |
Python isn't installed (Windows: you're hitting the fake Store stub). Do Step 2.5, and on Windows use py -3. |
The Microsoft Store opens when you type python3 |
That's Windows' placeholder, not Python. Step 2.5, then py -3 from now on. |
error: externally-managed-environment |
Your system Python is protected. Step 2.5, outcome 2 — the three venv lines. Nothing is broken. |
Missing dependency: requests |
Either the pip install line in Step 2.5 was skipped, or you made a venv and this terminal hasn't opened it: source .venv/bin/activate from scraper/ (Windows: .venv\Scripts\activate). |
NotOpenSSLWarning / LibreSSL 2.8.3 before every scraper run |
Not an error. Stock macOS Python grumbling. Step 2.5, "The macOS warning that is not an error". |
| Something else Python-shaped | Ask Claude: "fix my Python setup so the scraper runs" — paste the entire error text, don't retype it from memory. |
| Claude Code itself is misbehaving | Run claude doctor in the terminal — it checks your installation and reports what's wrong. |
Fallback — No Terminal At All (claude.ai in the browser)
If the terminal truly isn't happening today, you can still run most of the business from https://claude.ai in a normal browser. It's clunkier — browser Claude can't run the scraper on your computer or edit files directly — but it works:
- Sign in at https://claude.ai (any paid plan).
- Start a new chat (a Claude Project is even better — you can add the kit files once and every chat in the project knows them).
- Upload or paste
CLAUDE.mdfrom this kit as your first message, with: "You are the operator brain for this business. Read this and act accordingly." - Verify the same way: ask "what business is this?" — you should get the med-spa-agency answer.
- Upload the other files as you need them:
sales/call-script.mdbefore calling,prompts/01-prep-calls.mdplus yourleads.csvfor call prep,fulfillment/templates when you close a client.
For the lead list without the scraper: OpenStreetMap-based lead data needs the
terminal, so in the browser, paste browser-Claude the prompt from
scraper/build-list-with-claude.md and have it build you a list from web
searches instead ("med spa near [your city]", "botox near [your city]" — name,
phone, website into a spreadsheet). Or get a terminal-comfortable friend to run
Day 1 for you once. Twenty hand-gathered leads are enough to start calling —
and once you do get a terminal, python3 leads.py --from-csv my-list.csv runs
that same hand-built list through the full scoring machine.
Honest recommendation: do the 20-minute terminal setup. The whole point of this kit is that your Claude runs the machine — scraper, files, pipeline, builds. The browser fallback keeps you moving today; Claude Code is the business.
📄 docs/FIELD_TEST.md
Open file
FIELD TEST — med-spa-agency kit
Date: 2026-08-02
Tester: a fresh, skeptical Claude playing the buyer's AI — no prior context,
following START_HERE.md and CLAUDE.md only, exactly as a new CEO's Claude
would on day one.
Test market: Tempe + Mesa, Arizona — cities the builders had not used
(the docs' examples were Scottsdale/Phoenix).
Verdict: PASS (with the fixes below applied).
⚠️ Read the re-verification at the bottom of this file before quoting anything here. An independent QA pass on 2026-08-03 re-ran this kit and found that several lines below had been re-read rather than re-run: the template token count had moved from 45 to 49, the
--from-csvpath was verified by its printed string rather than its behaviour, and "zero off-niche rows" did not survive a bigger city. Every one of those is corrected in place below and listed in the 2026-08-03 section, with what was executed to prove it. Where the two sections disagree, the later one wins.
What was actually run
1. Day-one scraper run — live, keyless, a city the kit had never seen
Exactly the documented commands:
cd scraper
python3 -m pip install -r requirements.txt
python3 leads.py --city "Tempe, Arizona" --city "Mesa, Arizona"
Install worked first try on stock macOS Python 3.9 (no venv needed).
First run: 46 rows — and 12 of them were not med spas. Three Baskin-Robbins locations ("skin" inside "Baskin"), Fuel Injection Services Co, two auto detailers ("Auto Aesthetics"), a landscaper ("Arizona Aesthetic Trees"), an accountant ("Skinner & Company CPAs"), and four dental practices (cosmetic dentistry is tagged as an aesthetic speciality in OSM). A 26% junk rate in a call list a beginner is told to dial. Fixed — see defect 1.
After the fix, re-run live: 33 unique leads in one merged
leads.csv(Tempe 11 + Mesa 22, deduped), 31 with no website in the map data (score 100), 2 with a phone number ready to dial, and 31 hot leads written tolookup-list.mdwith pre-built Google Maps search links. Zero off-niche rows in that two-city file. That held for Tempe + Mesa; it did not hold in Chicago (2026-08-03), where a theatre, a park, a playground and a garden reached the call list through the deliberately looseskinkeyword. The filter was widened; see the 2026-08-03 section. Treat "zero off-niche rows" as a result from one run, never as a property of the filter.Overpass needed its documented retry on two of the three runs (mirror rotation recovered both times); the "free shared service, wait 5 minutes" messaging is accurate and calm.
Spot-check of 5 names for niche accuracy:
Lead City Verdict Lé Nour Medspa Tempe ✅ real med spa (opened 2021, injectables/fillers) — but has a live website OSM doesn't know about (see defect 2) Skinlogic Beauty Bar Mesa ✅ real aesthetics studio (hydrofacial, microneedling); Facebook shows it temporarily closed — lead lists decay Allure Medspa Mesa ✅ real med spa, phone + live site in the data (scored 0 — low priority for a rebuild, right call) A to Z Dermatology Mesa ✅ real dermatology practice — adjacent, sells the same aesthetic services, valid prospect Lux Injections Mesa ✅ real injectables studio Roughly a third of the file is dermatology groups rather than classic med spas. Kept on purpose (same buyer, same services) and now called out in
scraper/README.mdso nobody is surprised on the phone.Both test artifacts (
leads.csv,lookup-list.md) were deleted after the test; the kit ships clean and the buyer's first run creates their own.
2. Agency storefront build — both variants
Filled config/business.yaml with throwaway demo identity, then ran
python3 fill.py in agency-site/ and in agency-site/variant-b/:
- Both built
dist/with zero unfilled{{tokens}}and correct comma-formatted prices read from the nestedpricing:block (1,000 / 2,500 / 199 / 499). - Restored the config to pristine placeholders and re-ran both scripts: both
refuse to build, naming the exact missing keys (
agency_name, owner_name, phone, email, city), exit code 1. Documented behavior holds. dist/folders deleted,config/business.yamlbyte-identical to its shipped state (verified withdiff).
3. Fulfillment flow — can a Claude really deliver a site this week?
Walked fulfillment/delivery-checklist.md line by line against the files:
- The client deliverable (
fulfillment/website-template/index.html, 493 lines) is a complete premium one-page med-spa site: hero + credential line, 6 treatment cards with consult-first pricing language, provider section with photo fallback, studio gallery, verbatim-reviews section, Formspree consult form, tap-to-call everywhere, sticky book bar, and the "Individual results vary" disclaimer. Not a stub. - All
{{TOKEN}}s in the template are documented in its README, and the README documents no token that isn't in the template. Verified by extraction, both directions. ⚠️ The count was 45 on 2026-08-02 and is 49 today — the four{{CLAIM_*}}tokens were added afterwards and the README was not updated with them until the 2026-08-03 QA pass. Two-way extraction was re-run then and now passes at 49. - Ran the checklist's copy step for real in a scratch folder (
cp -rthe template,rm README.mdas instructed), filled all 45 tokens (of the 45 that existed that day; there are 49 now) with demo values, and the documented verification —grep -rn "{{" <client site folder>— came back empty, includingphotos/README.md(no tokens in it). The page still parses as valid HTML. fulfillment/review-engine/make_qr.pywas run (withsegno, as its header documents): produced a valid 396×396 PNG from a demo review link.- Booking fork (existing software vs Cal.com), reminder system, Netlify
drag-and-drop, the test-booking step, the invoice/handoff files, and the
7-day upsell hand-off to
prompts/06-upsell.mdall exist and agree with each other on paths and stage names.
Answer: yes — a Claude with the client's intake answers can put a real, tested site live inside the week. The gating item is the client's answers, not the kit.
4. Prompts and back office vs the real data
- The column list in
prompts/01-prep-calls.mdmatches the generated CSV exactly (15 columns,sourcelast). (Superseded 2026-08-03: the CSV now has 16 columns,has_online_bookinglast — see the addendum at the bottom of this file. Prompt 01 was updated to match and re-verified.) - Every
pricing.*key referenced anywhere in the kit (website_quote,care_low,care_default,review_engine_monthly,reactivation_monthly) exists inconfig/business.yaml. - Tracker stages and
offervalues used by prompts 03/04/05/06 all matchoffice/tracker-guide.md. office/invoice.htmltokens are all documented inoffice/invoice-template.md(including{{PROJECT_DESCRIPTION}}).
5. Full path audit
Every file path referenced in CLAUDE.md, START_HERE.md,
docs/connect-claude.md, prompts 01–06, sales files, playbooks, and every
README resolves to a file on disk. The only non-existent paths are the
runtime-generated ones — scraper/leads.csv, scraper/lookup-list.md,
agency-site/dist/, office/invoices/, office/clients/<slug>/… — all
documented as generated. docs/FIELD_TEST.md was referenced by
scraper/README.md before it existed; it exists now (this file).
What was fixed during this test
scraper/leads.py— the junk-lead problem (the big one). The name-keyword hunt dragged in 12 off-niche businesses out of 46. Three changes: (a)exclude_categorieswidened to cover dentists, auto shops, food, and professional-services map tags — the categories that actually showed up; (b)exclude_name_regexextended (\bdental\b,\bauto\b,\bcpa\b,\btrees?\b,landscap,ice cream…); (c) a new third layer,keep_name_regex, so an unmistakable med spa name survives the name filter anyway ("Palm Tree Med Spa" is kept even though "Tree" is an exclude word) while the category filter still wins ("Botox Dental Group" is a dentist). Verified on the live data: exactly the 12 junk rows dropped, zero real leads lost, and a legitimate lead the first patch would have cost us ("HFskin Beauty Bar") is back in the file.- The "no website at all" overclaim. Score 100 only means OSM has no
website for them — Lé Nour Medspa scored 100 and has a live site. A
beginner reading the old wording would open a call with a false statement.
Fixed everywhere: the CSV reason string is now
no website in the map data(both code paths), the README's score table warns and gives the safe opener wording,CLAUDE.mdandprompts/01-prep-calls.mdnow require "I couldn't find a website for you" instead of "you have no website", andlookup-list.mdtells the CEO to check the Maps listing for a website while they're grabbing the phone number. - Care-plan Standard tier was priced two ways.
$299inconfig/business.yamlandfulfillment/care-plan-sop.md,$349inoffice/proposal-onepager.html,playbook/growth.mdandoffice/onboarding-emails.md— the buyer would have quoted one number on the call and printed another on the proposal. Standardized on $299 (source of truth = the yaml), including the tracker-guide example. - The delivery promise contradicted itself. Storefront,
START_HERE.md,CLAUDE.mdand the delivery checklist said "live within the week"; the call script, outreach templates, objections, quote one-pager, onboarding emails, prompts 03/04 and two playbooks said "under two weeks". Guardrail 4 is "deliver what was sold" — so all 14 lines now say live within the week, and the pitch and prompt 03 carry the honest caveat that the clock starts when the client's intake answers come back. - Calling windows contradicted themselves.
CLAUDE.mdsaid Tue–Thu (and "Mon–Thu mornings" in the weekly rhythm) while the playbooks and prompt 01 said Tue–Fri because spas close Mondays. Aligned on Tue–Fri inCLAUDE.mdandSTART_HERE.md. CLAUDE.md's kit map was missing two fulfillment files —calcom-setup.md(the booking page) andreminder-system.md(the no-show killer). The operator brain navigates by that table, so both are now rows in it.START_HERE.mdtaught a single-city scrape while the kit's own target is 50+ leads. It now shows the multi-city command, sets honest expectations (10–25 leads per city), and warns about the phone-lookup step before the first 25 dials. Same honesty pass applied toplaybook/week-1.md(Day 1 counts, Day 2 lookup budget) andCLAUDE.md's day-one sequence.scraper/README.mdcited a field test that had never happened ("Scottsdale + Phoenix… seedocs/FIELD_TEST.md", a file that did not exist). Replaced with the real Tempe + Mesa numbers. Also updated: the filter description (two layers → three, with the real false positives named), a new note that ~a third of results are dermatology practices and why to keep them, and a troubleshooting entry for Overpass mirror snapshots differing between runs.
Open gaps (honest, not blockers)
- Phone coverage in this niche is brutal. 2 of 33 leads (6%) shipped with a phone number; the other 31 need a ~30-second Google Maps lookup each. The kit generates the lookup list and the docs now budget ~15 minutes before the first call block, but it is manual work between "scrape" and "dial".
- Score 100 ≠ no website. OSM simply doesn't record many sites. The wording is now safe everywhere, but the score itself can't tell the difference without a second source. Verify before you make it your opener.
- OSM coverage lottery. Two mid-size Arizona cities produced 33 leads; a full calling week (50+) needs three or four. A buyer in a thinner-mapped metro will need more cities, the county fallback, or the optional Google Places source.
- The niche mix isn't pure. Expect dermatology groups (valid buyers, but a different call), plus the occasional skincare brand or injector-training company. Documented; a 10-second glance at the name settles it.
- Overpass availability and drift. Free shared servers: two of three runs needed the built-in retry, and mirror snapshots differ enough that the same city came back 12 leads one run and 11 the next.
- Google Places source: not tested live. No billed key ships with the kit (same as the sibling kits). The keyless default is what was field-tested.
- Third-party pieces not exercised live: Formspree, Cal.com, and Netlify deploys were walked in the docs, not performed against real accounts; free tiers and their limits (Formspree 50 submissions/mo) can change under the kit. The client site was filled and verified locally, not deployed.
- Lead lists decay. One of the five spot-checked businesses shows as temporarily closed on its own Facebook page. Normal — rescrape monthly.
Bottom line: a fresh Claude with zero context ran day one from the shipped docs alone, pulled 33 real med spas and aesthetic clinics in a city the builders never tested, built both storefront variants from one config file, and filled the client deliverable end-to-end with the placeholder check coming back clean. The one serious defect — a lead list where a quarter of the rows were ice cream shops, dentists and car detailers — was found by actually dialing-eye-balling the output, and is fixed and re-verified live. A buyer can start calling with this kit.
ADDENDUM — 2026-08-03 buyer-audit fixes, re-verified live
A second pass driven by a buyer audit of the shipped kit. Everything below was run, not reasoned about. Same machine class as the first test: stock macOS Python 3.9.6, keyless, no API keys of any kind.
What was run
| Command | Result |
|---|---|
python3 leads.py --city "Naperville, Illinois" (a city the builders had never used) |
4 leads, 3 with a phone. 5 off-niche rows dropped by the filter. A genuinely thin map — used as the kit's honest "thin city" transcript |
python3 leads.py --city "Scottsdale, Arizona" (re-run of the shipped artifact) |
47 leads, 43 with no website in the map data, 4 dialable, 43 phoneless → lookup list. Byte-for-byte the same shape as the shipped leads.csv |
python3 leads.py --from-csv my-list.csv --city "..." --no-enrich |
Hand-built list scored end to end, map servers not used, lookup list written for both score bands. ⚠️ This check was wrong: it confirmed the printed string, not the behaviour. The code queried Overpass anyway. Fixed and re-run 2026-08-03 — see below |
make_qr.py with segno installed |
Valid 396×396 PNG written, exit 0 |
make_qr.py with segno unavailable |
Plain-English exit 1 with an install fix and a no-install browser alternative |
fill.py in agency-site/ and agency-site/variant-b/ |
Both built with zero unfilled tokens; config/business.yaml restored and verified identical (shasum match, empty git diff) |
| Client-site copy sequence from the delivery checklist | All tokens present in the client copy (45 that day, 49 now); README.md and EXAMPLE/ correctly absent |
The v2.2 "empty city" fix fired for real during the Scottsdale re-run —
maps.mail.ru answered the boundary query with a successful-looking zero, the
scraper refused to accept it on one mirror's say-so, fell back to the map
rectangle, and recovered 67 raw entries → 47 leads. That transcript is now
printed verbatim in docs/what-you-will-see.md §4b so a buyer recognises it.
Defects found and fixed
has_online_bookingwas undocumented and over-promised.START_HERE.mdtold the buyer to "notice how many have no online booking — that's the whole opportunity, in one spreadsheet." The column populates fine (yes/no/unknown) but in the Scottsdale run 44 of 47 rows areunknown, because 43 leads have no website to check. Fixed: the column and its three values are documented inscraper/README.md("The booking column"), including the four things a homepage-only check misses;START_HERE.mdnow carries the three-value table; prompt 01 forbids mentioning booking at all on anunknownlead; the scraper prints theunknowncount and the rule at the end of every run.- Every call opener and outreach template required
[FIRST NAME]and nothing in the kit produces one.leads.csvcomes from a map — business names, never owners' names — and there was no fallback line anywhere. Fixed on both sides:sales/call-script.mdopens with the rule and ships Version A (no name, ending by asking who handles the website) and Version B (verified name); every voicemail/SMS/email insales/outreach.mdcarries a printed no-name greeting; prompt 01 gained step 4b (look names up on the About page / Google profile / Instagram bio, 1–2 min each, ~half succeed,unknownotherwise, never inferred from a domain) and now forbids leaving a literal[FIRST NAME]in a line the CEO reads aloud; prompt 02 reports the name with its source. - The phone-lookup list was documented as hot-leads-only. The v2.2 code
already writes every phoneless lead in two blocks, but
scraper/README.md,START_HERE.md,CLAUDE.mdandplaybook/week-1.mdall still said "hot leads", so a buyer would never look for the second block. Verified both blocks render, then fixed the wording in all four files and in the run's own summary line. docs/connect-claude.mdnever said what the first run asks. The killer feature is "connect your Claude and go" and the first screen was undocumented — as was Python itself (the kit told buyers topip installwith no install step). Added: Step 2.5 (Install Python, all three platforms, theexternally-managed-environmentvenv fix), a full walkthrough of the first-run prompts (theme → login method → browser auth → the trust-this- folder dialog → terminal setup → the prompt box), what a first message looks like, the permission prompts, and a 14-row troubleshooting table. Newdocs/what-you-will-see.mdmatches the plumber kit's standard with real transcripts from this session.- The
NotOpenSSLWarning/LibreSSLblock. Stock macOS Python prints it before every scraper run. It was documented nowhere, and it is the most likely thing to make a non-technical buyer think they broke the kit on minute one. Now covered in three places with the general rule attached: a warning keeps printing, an error stops. make_qr.pyneeded a package no requirements file declared. Addedfulfillment/review-engine/requirements.txt(segno>=1.5, with the venv note), rewrote the import failure into a plain-English message, and gave it a no-install fallback so a missing package can never block a client's review cards. Both paths run-tested.- No worked example and no design preview. At $897 the buyer was told to
open two
{{token}}-filled files and "ignore the tokens" to choose a design. Addedfulfillment/website-template/EXAMPLE/(all 45 tokens of the day filled for a made-up spa, Auravel Aesthetics — banner and README on every screen saying it is a demo and not a client of ours) andagency-site/EXAMPLE/daylight/+EXAMPLE/eventide/(both designs built by the kit's ownfill.py), plusdocs/see-it-first.mdas the 20-minute browser tour.config/business.yamlwas restored to pristine afterwards and verified. Follow-on defect caught while doing it: the delivery checklist'scp -r fulfillment/website-template …would have copied the newEXAMPLE/folder into a paying client's site folder and deployed it. The copy sequence in the checklist, the template README,fulfillment/README.mdand prompt 03 now delete it explicitly; re-tested. - No keyless fallback lead source. When OSM ran thin, the only documented
alternative was Google Places, which needs a Google Cloud billing account —
in a kit whose day-one promise is "no API key". The
--from-csvpath already existed in the code and appeared nowhere inscraper/README.md, not even in the options table. Now documented as a first-class path ("When the map is thin"), with newscraper/build-list-with-claude.md— a ready-made prompt that has the buyer's own Claude build the list from web searches, with its limits stated plainly (15–30 names in ~10 minutes, no maps database, stale results possible, every unverified field left blank rather than guessed).CLAUDE.mdnow requires offering this path before ever mentioning Google Places.
Still open (honest, unchanged)
- Phone coverage in this niche is brutal. 4 of 47 in Scottsdale, 2 of 33 in Tempe+Mesa. The lookup list and the ~30-second-per-number budget are the answer, and they are manual work between "scrape" and "dial".
- Booking detection is homepage-only by design (one polite fetch per lead).
It will read
nofor booking links a page deep or loaded by JavaScript. Documented; prompt 02 is the real check. - Owner names cannot be scraped. The no-name openers are the fix, not a workaround — but it does mean the warmest version of the script depends on research or on call one.
- Google Places path still untested against a live key (no billed key ships with the kit). The keyless paths are what we field-test.
- The Claude Code first-run screens are described, not screenshotted. Claude
Code updates often;
docs/connect-claude.mdsays so explicitly and describes the shape and the correct answer for each prompt rather than pretending to quote a version.
v3 — parity levelling, 2026-08-03
A parity audit compared all three kits side by side and reached an uncomfortable conclusion: at $897 this kit was not demonstrably the best of the three, and a buyer comparing the boxes would have seen it. It led on five real things and trailed the $597 plumber kit on eight that a buyer touches in week one. It also carried the only live house-rule violations on the shelf.
That is the whole reason for this entry. The premium has to be visible in the box, not just in the price.
The violations, and that they were real
Five statements were shipping that break house rules the other kits already enforce. All are gone:
fulfillment/care-plan-sop.md— "this is why nobody cancels"fulfillment/review-engine/monthly-cadence.md— same phraseplaybook/growth.md— "the monthly report is why nobody cancels"fulfillment/reactivation-campaigns/campaign-sop.md— "the richest pool because nobody has ever nudged them", a stated fact about a client's own customersplaybook/growth.md— "eight times the tier range is $1,600–$4,000/mo of recurring revenue", the multiplication that turns a price into a revenue projection
We have never signed a client, so we have no retention evidence and no revenue evidence. Where the reasoning behind a design was worth keeping, it is now labelled as reasoning.
The other thing that has to be said plainly
An earlier version of this file stated that both test artifacts
(scraper/leads.csv, scraper/lookup-list.md) "were deleted after the test;
the kit ships clean." They were still in the kit. Real Scottsdale
businesses, in the buyer's day-one folder, in the one document whose entire job
is to be the place we do not spin. The artifacts are now actually gone, and the
sentence is now actually true. A false sentence here is the same class of defect
as an income claim, and it is recorded rather than quietly corrected.
What was added to earn the price
| Added | Why |
|---|---|
fulfillment/website-template-b/ — design B, "Nocturne" |
The $597 kit shipped two client designs and this one shipped one, in a niche whose whole stated angle is "every spa looks the same online". B is charcoal-blue + antique gold with an editorial numbered treatment list. Token parity verified: 49 tokens each, identical names; 3 CLAIM-BLOCKs each |
README.md (root) |
The only complete map of the box lived inside CLAUDE.md, written for the AI |
office/intake-form.html |
Section 6 is the claims gate in tick-box form. In this niche that is the difference between "the owner said so on the phone" and evidence — and this is now the best version of the intake form on the shelf |
office/quoting-guide.md |
$1,000–$2,500 is the widest band on the shelf, and "quote inside the range" is useless advice on a live call. Base plus named add-ons, worked $1,000 / $1,750 / $2,500 examples, and the rule that the consent and claims checks are never billable extras |
office/EXAMPLE-call-list.md |
The fifth filled EXAMPLE. The buyer now sees a real short list — 16 numbers, not 25 padded ones — before their first call block |
fulfillment/care-plan-sop.md rebuilt, 87 → 262 lines |
It was the thinnest recurring SOP of the three attached to the highest recurring ticket in the catalog. Now week by week, plus month one, the churn playbook, the quarterly claims re-check, and scale-in-hours |
docs/see-it-first.md rewritten |
It sent the buyer to raw {{TOKEN}} masters while START_HERE sold the tour as "five finished examples". Now nine genuinely finished files |
The claims re-check that only this kit has
care-plan-sop.md week 1 now includes a step the other kits don't need: a
claim that was true at launch can stop being true. If a licensed injector
leaves and an aesthetician covers, the "licensed providers" tile has to come
down that day. The gate does not expire at go-live.
Verified
fulfillment/website-template-b/index.htmlvsfulfillment/website-template/index.html: identical token set (49), identical CLAIM-BLOCK count (4 since the 2026-08-03 QA pass fenced the booking-band reminder sentence; it was 3, and the fourth claim was hardcoded outside every fence), all fences balanced.- Grep sweep across all three kits for invented social proof, income claims and revenue multiplication: every remaining hit is a prohibition, not a violation.
scraper/contains onlyleads.py,requirements.txt,README.md,build-list-with-claude.mdandmanual-list-template.csv. Checked, not assumed.
Still not tested live (unchanged)
Netlify deploys, a real Cal.com account, Formspree with a real box, Google Places with a billed key, real calls and sends.
Design B has now been rendered (2026-08-03): website-template-b/EXAMPLE/
was built from the same pretend intake as design A's example, all 49 tokens
filled, and both the filled example and the raw master were rendered in WebKit
and looked at. That closes the open item this section used to carry.
QA re-verification — 2026-08-03
What this is: an independent QA pass run behind the swarm that last touched this kit, on the assumption that its work was unverified. Everything below was executed, not read. Where a sentence above this line disagrees with one below it, this section is the current one.
Headline: the machine ran, but three of the kit's own promises were being broken by the code, and two of them were the promises a buyer relies on most.
The three that mattered
1. A monthly rescrape ate the buyer's hand-verified phone numbers
The kit tells the CEO most leads ship without a phone, sends them to
lookup-list.md, budgets 20–40 minutes for looking numbers up by hand,
tells them to type each one into leads.csv, and then says "rescrape
monthly." The rescrape opened both files with "w".
Reproduced: 7 verified numbers written in (4 into leads.csv, 3 onto
Phone found: lines), the documented command re-run, all 7 gone.
write_lookup_list also called os.remove() on the lookup file outright.
Fixed. A rescrape now reads both files first, carries every hand-added phone and email onto the matching new row (matched on OSM id, then name + city + street address), prints how many it kept, and leaves a dated backup of both files. Ambiguous matches are dropped rather than guessed — "Glow Med Spa" and "The Glow Med Spa LLC" normalise to the same name, and a number carried onto the wrong shop is worse than a number lost. Nothing is ever deleted.
Re-run after the fix, same 7 numbers: Carried over 7 phone number(s),
DIALABLE NOW: 7, every number on its correct row.
2. The keyless rescue path was hitting the map servers
scraper/README.md promised --from-csv makes no map query and prints
map servers not used. The code printed that string and then queried three
Overpass mirrors anyway and merged the results in. This is the path the kit
sends a buyer to because the map is broken, so it was re-running the exact
failure they came there to escape.
The 2026-08-02 test recorded this command as verified. What it verified was the printed string, not the behaviour.
Fixed in the code, not the doc — the doc was describing the better
product. --from-csv now skips the map entirely. Re-run of the documented
command: 0.1 seconds, no network, and it says out loud what --city is
for on that path.
3. The anti-panic document omitted the panic screen
docs/what-you-will-see.md exists so a nervous, non-technical buyer can check
their screen against ours. It quoted a summary block the scraper no longer
printed (Wrote 47 leads, 4 have a phone number ready to dial), and it
reintroduced the "43 have no website" overclaim that defect #2 above says was
fixed. Meanwhile the two loudest things on the real screen — a banner-boxed
DIALABLE NOW and a fifteen-line VERDICT block with a four-step
recovery plan — appeared in zero markdown files in the entire kit.
Fixed by re-running, not by editing. §4b now carries a live 2026-08-03
capture; §5 carries a full live summary; §5c is new and explains both loud
screens. Every line inside those code fences was diffed against the raw logs
of the runs that produced them. START_HERE.md gained the same explanation.
Everything else fixed, with the check that proved it
| Defect | Fix | Executed check |
|---|---|---|
| Rows vanished between "Got 41" and "Wrote 36" — chains collapsed on name+city, losing the second address | Dedupe and merge keys now carry the street address, then lat/lon; every merge is printed | Hunter's 5-row repro: was 5 → 4 → 2, now 5 → 5 → 5, with genuine node/way duplicates still collapsing to 1 and the OSM+Google merge intact |
--out ../office/leads/leads.csv tracebacked after the whole scrape |
Path validated and folders created at argument-parse time | Ran the exact command: folder created, 7 rows written, no traceback |
| A list saved by Excel-for-Windows crashed on an accented name | Read as bytes, utf-8-sig then cp1252, plain-English exit on binary or a folder |
cp1252 file containing "Café Rosé Med Spa" / "Lé Nour Medspa": both round-trip to UTF-8 output correctly. Folder and binary inputs now exit 1 with instructions |
| Parks, a playground, a garden and a theatre in the "call these first" band | ~20 map categories and 5 name patterns added to the med-spa filter | Real names from the live runs: all 6 off-niche rows dropped, all 8 real spas kept — including "Park Avenue Med Spa" and two category=alternative spas |
| A genuinely empty town reported as "the servers were busy", forever | The map-rectangle pass is now trusted on its own; probe failure is distinguished from a real empty area | Deterministic: area unknown + rectangle empty now returns empty (was failed); both unknown still returns failed |
| "One polite map query per city" — it was up to 10 | Hard ceiling of 8 requests per city, area probe cached, no follow-up question to a mirror that just errored, dead mirrors skipped in the rotation, and no sleeping on a budget already spent | Live Scottsdale run reports 7 map request(s) used; a failing run stops at the ceiling and says so. Both docs now state the real numbers |
| A bad Google Places key discarded a finished OSM scrape | The key is checked with one cheap request before the city loop; a mid-run failure returns instead of exiting, and the CSV is still written | Code path returns (leads, fatal); sys.exit removed from scrape_google_city |
Business names starting with = + - @ written to CSV unescaped |
Leading apostrophe on anything formula-shaped; phone numbers and plain numbers untouched | =cmd|' /C calc'!A0 Med Spa and +1 Aesthetics both land quoted; +1 480-555-0117 is left alone |
make_qr.py's own documented example produced an orphan qr.png and a card with 8 broken images |
The script copies qr-card.html next to the generated PNG (never overwriting an existing one) and says so |
Ran the docstring's exact command, then rendered the result: 8 working QR cards. Found and fixed a second bug doing it — qr-card.html had no <meta charset>, so every em-dash printed as â€" |
parse_simple_yaml truncated any config value containing # |
# is a comment only at line start or after two or more spaces, and never inside a quoted value |
Both fill.py files: owner_name: Sam #1 Carter, Studio #1, Suite #4 all survive; trailing # comment still stripped. Built both agency designs from a #-containing config — 0 unfilled tokens, name intact in the output |
| The no-network message pasted a three-deep urllib3 exception into the plain-English line | Classified into timeout / no-connection / HTTP, one sentence each, raw text on a (technical detail: …) line |
explain_network_error() covers all four cases |
| "every med spas-type business" | label.rstrip("s") |
Live run prints every med spa-type business |
scraper/README.md: "retried 4 times across 3 mirrors" |
Now states 5 attempts × 3 mirrors × two search shapes, inside the ceiling of 8 | Matches run_overpass(max_attempts=5) called twice |
website-template/README.md said 45 placeholders; the template has 49 |
Count corrected and the four {{CLAIM_*}} tokens documented, cross-referenced to CLAIMS.md |
Two-way extraction re-run: 49 = 49, nothing missing in either direction |
office/README.md claimed to map "every document" but listed 17 of 22 |
The four missing EXAMPLE-* rows added, each tagged "reference only — never send to a client"; CLAUDE.md's glob widened to EXAMPLE-* so it covers the .csv |
EXAMPLE-client-tracker.csv was the one file in the kit referenced nowhere; it now is |
Integrity findings — the ones that mattered most
A promise the client can't keep was hardcoded outside every fence. Both
client-site designs carried "You'll get a confirmation right away and a
reminder before your visit" in the booking band, containing no token and
inside no CLAIM-BLOCK — while CLAIMS.md asserted "None of the claims is
hardcoded" and documented claim 4 as living only in the standards strip. A
buyer who followed the procedure exactly, deleted the tile because the
client's reminder layer wasn't running, and passed both pre-launch greps
still shipped the promise.
Fixed in the templates, not by softening the sentence in CLAIMS.md: the
sentence is now its own fenced CLAIM-BLOCK in both designs (4 blocks each, not
3), CLAIMS.md lists both locations for claim 4, and every "3 CLAIM-BLOCKs"
in the kit was corrected. Verified: deleting claim 4's blocks removes
"confirmation right away" and "reminder before your visit" from the page
entirely; the two greps still fail correctly on a pristine template.
Payback claims and unsourced competitor prices were being put in the
buyer's mouth. growth.md said "one reactivated filler client can cover a
month of Pro on her own. Say exactly that when you pitch"; objections.md
told the buyer to say "the site only has to win you a couple of the people
already comparing you"; the call script and three outreach templates asserted
"agencies quote med spas three to eight grand" — a number nobody here has
measured, said to a stranger. All replaced with the mechanism plus the
client's own numbers, and prompts/06-upsell.md now bans the payback framing
in both directions instead of hedging it to "can pay for itself".
The care-plan milestone paragraph sitting under the tier price table was rewritten to the same shape the pet-groomer kit already uses ("not because of a number it produces").
Parity and consistency
- Design B now ships a finished
EXAMPLE/— Auravel Aesthetics, same pretend intake as design A, all 49 tokens filled, 0 leftover braces, 0 fence comments. Both it and the raw master were rendered and looked at, which retires the "design B has not been rendered in a browser by us" caveat honestly rather than by disclosure.see-it-first.mdnow points at two finished pages instead of one finished page and one file of{{TOKENS}}. - "Auravel" was both the fictional agency and the fictional client. The
office
EXAMPLE-*paperwork now uses Lumiere Digital / Sam Carter as the agency — matching theagency-site/EXAMPLE/demos — leaving Auravel Aesthetics as the one fictional client throughout. - "Porcelain" named two different designs. The agency-site design A is now Daylight; the client-site design A keeps Porcelain. Four design names, no collisions.
- The dial ramp contradicted itself.
your-phone-setup.mdsaid start at 10–15 and ramp to 30;week-1.mdsaid "Goal: 25 dials" on day one andCLAUDE.mdsaid "25 leads a day" — i.e. the kit told the buyer to do on day one exactly what its own phone doc says gets the number flagged. Everything now follows the phone doc's ramp. fulfillment/publish-a-demo.mdadded. The kit told the buyer to promise a homepage mockup on a cold call and then handed them a local file. A demo is a link: free Netlify drop in 60 seconds, plus the two checks a med spa preview needs (claims gate — on a mockup every claim is a NO — and no invented facts). Wired intoCLAUDE.mdandprompts/03-fulfill.md.
Run log (2026-08-03, live, keyless, macOS Python 3.9.6)
| Command | Result |
|---|---|
leads.py --city "Scottsdale, Arizona" |
✅ 46 leads, 67 raw entries, 21 dropped off-niche, 4 dialable, 7 map requests used. The boundary pass failed on busy mirrors and the rectangle fallback recovered it — the v2.2 design working. One off-niche row survived it (Med Plaza IV Visitor Parking, matched by the bare iv keyword); parking and a dozen other street-furniture tags were added to the filter afterwards and verified against that row deterministically, not in a second live scrape — the mirrors would not give us one |
leads.py --city "Scottsdale, Arizona" (earlier attempt) |
❌ every mirror busy; stopped at the 8-request ceiling in 4m57s with the honest "this is NOT a result" message. Transcript is now §4b of what-you-will-see.md |
leads.py --city "Chicago, Illinois" |
❌ same — mirrors refusing. Exit 1, plain English |
leads.py --city "Ely, Nevada" |
❌ mirrors refusing; correctly said "only one mirror answered with a zero — not enough to call it empty" rather than declaring the town empty |
leads.py --from-csv my-list.csv --city "…" |
✅ 0.1s, no network, 7 in → 7 out |
leads.py --from-csv … --out ../office/leads/leads.csv |
✅ folders created, no traceback |
leads.py --from-csv <cp1252 file with accents> |
✅ accents preserved |
leads.py --from-csv <folder> / <binary> |
✅ exit 1, plain English |
Rescrape over a hand-edited leads.csv + lookup-list.md |
✅ 7/7 numbers carried, both files backed up |
fill.py in agency-site/ and variant-b/ |
✅ both built, 0 unfilled tokens, Studio #1 Digital intact; config/business.yaml restored |
make_qr.py (its own docstring example) |
✅ PNG + card copied together; card rendered with 8 working QR codes |
| Delivery-checklist copy sequence, both designs | ✅ 49 tokens each, README and EXAMPLE correctly absent, both pre-launch greps clean after filling |
Honest limits of this pass. Overpass mirrors were rate-limiting this IP for
most of the session (overpass-api.de refused TCP outright), so only one
successful live city scrape was captured. Netlify, Cal.com, Formspree and
Google Places with a billed key remain untested live, unchanged from above.
Pages were rendered with the system WebKit engine, not clicked through by a
human on a phone.
2026-08-04 — repair pass: two fresh metros, run cold
What this section is. A repair pass working docs/SHELF_GATE.md §2C and an
independent cold-Claude report. Everything below was executed on 2026-08-04 and
the output pasted from the terminal, not recalled.
The two new scraper runs — both in metros nobody in this repo had used
Sarasota, Florida — single city, then the whole metro.
[2/4] Asking OpenStreetMap for every med spa-type business there...
Dropped 3 business(es) outside the med spa niche (...)
The map servers answered for "Sarasota, Florida" and returned 3 named
business(es), but every one of them was outside the med spa niche
Then all four metro cities merged in one run — Sarasota + Bradenton + Venice + Lakewood Ranch, ~830,000 people, a wealthy market with a great many real med spas in it:
Merged total across 4 cities: 1 unique leads.
[4/4] Done. Wrote 1 rows to .../sarasota-metro.csv
DIALABLE NOW: 0
One row. Zero dialable. 3m33s. This is the worst measured result for this
kit and it is now printed in START_HERE.md where a buyer sees it before they
judge their own market.
Naples + Fort Myers + Cape Coral + Bonita Springs, Florida — same command shape, ~800,000 people:
Merged total across 4 cities: 34 unique leads.
[4/4] Done. Wrote 34 rows to .../swfl.csv
DIALABLE NOW: 5
- 30 have no website IN THE MAP DATA (score 100)
- 3 have a working site with NO online booking found on the homepage
- 29 leads missing a phone number
34 rows, 5 dialable, 29 needing a phone lookup. Note the 3 rows with
has_online_booking = no — the cold-Claude report found zero of those in
Boise and concluded the booking-funnel pitch had no qualifying leads in a real
American market. This run says the count is small but not always zero. Both
facts are now in the docs; neither is presented as the typical case.
Defects found by running it, and fixed in this pass
| Defect | Evidence | Fix, and how it was verified |
|---|---|---|
| A city where the niche filter drops every row reported itself as "the free map servers were busy… run the exact same command again" | Sarasota: servers answered, returned 3 named businesses, filter dropped all 3, and the run told the CEO to retry — a loop that can never succeed | New branch in leads.py: when raw is non-empty and city_leads is empty, it says the servers worked, says how many rows the filter dropped, and says "re-running this exact command will produce the exact same result." Re-ran Sarasota; new message printed, pasted above |
category column emitted the literal string yes for OSM nodes tagged healthcare=yes |
2 of 14 Boise rows in the cold report; prompts/01 step 4 uses "a specific category" as a ranking input, so yes quietly demoted real leads |
tag_meaningful() skips {yes,true,1} and falls through to the next key, keeping the placeholder key name as a last resort. Verified on the SWFL run: Counter({'doctors': 11, 'beauty': 7, 'beauty:skin_care': 5, 'beauty:spa': 4, 'clinic': 3, ...}) — no yes values |
--out was documented but every working prompt hard-coded scraper/leads.csv |
A run written to boise-leads.csv left prompt 01 reading a stale DuPage County, Illinois file — real businesses, real phone numbers, nothing visibly wrong |
leads.py now writes scraper/last-run.txt (always beside leads.py, never beside --out) recording leads_csv=, lookup_list=, source= and cities=. Prompts 01, 02, 03 and 05 read it first. Verified by running with --out into a different directory and confirming last-run.txt landed in scraper/ with the right absolute path |
| Redirecting output produced an empty log for the whole run | Block-buffered stdout; a buyer who runs ... > run.log concludes it crashed |
sys.stdout.reconfigure(line_buffering=True) at the top of main() |
| The lookup-list prompt could not distinguish a number from the business's own listing from one an aggregator matched to the wrong branch | Cold report: a number came back attached to a different street address than the CSV row, caught only because the tester happened to compare addresses | The generated lookup list now carries the address test as a mandatory step in both the manual and Claude-assisted flows, a Source (URL) + address matched? line per lead, and an explicit "this one needs TWO independent sources" note on any lead with no address on file. Regenerated the file and read it back |
What this pass did NOT test
Unchanged and still untested live: Netlify deploys, Formspree against a real inbox, Cal.com, Google Places with a billed key, and any actual phone call. The two website designs and both storefront designs were rendered in headless Chromium at 390 / 768 / 1280px and read; nobody clicked through them on a physical phone.
The keyless rescue path, proven with the map servers unreachable
SHELF_GATE.md M12 asked for this exact test: block the map hosts and run the
documented rescue command. Done on 2026-08-04 by making every Overpass and
Nominatim hostname fail DNS resolution inside the Python process.
Control — a map scrape must fail, and fail in plain English:
Looking up "Asheville, North Carolina" on OpenStreetMap...
Could not reach the OpenStreetMap city lookup service at all — no connection was made.
Fix: check your wifi, then run the exact same command again. If your wifi is fine,
the service is down for everyone; try again in ten minutes.
The rescue command, hosts still blocked — exit 0:
$ python3 leads.py --from-csv my-list.csv --city "Scottsdale, Arizona" --no-enrich
[1/4] Reading your hand-built list: my-list.csv
Loaded 7 businesses (map servers not used).
--city "Scottsdale, Arizona" is only a label for rows in your file that have no city of their own.
The map servers are NOT used on this path — that is the whole point of it.
[4/4] Done. Wrote 7 rows to ...
DIALABLE NOW: 3
Not one map request was attempted. --city on this path is a label for rows
with no city of their own and nothing more — the code sets the scrape list to
empty when --from-csv is present, so the documented invocations that carry
--city are safe as written.
The four pre-launch greps, run against a real mockup build
Built a mockup from fulfillment/website-template/index.html in mockup mode
(every CLAIM-BLOCK deleted, tokens filled) and ran the checks a buyer runs:
grep -c "{{" mockA.html -> 2 (the two CLAIM_* tokens whose
sentences a mockup must DELETE —
the gate correctly catching a
half-done job)
grep -c "CLAIM-BLOCK" mockA.html -> 0 (correct: all deleted)
grep -c "PREVIEW-BANNER" mockA.html -> 5 (correct: a mockup MUST carry it)
Rendered at 390px: the banner reads "PREVIEW — built by Lumiere Digital for Marchetti Aesthetics. Not a live site, not affiliated with Marchetti Aesthetics, and nothing on this page is authorised by them." — full-width, above the header, impossible to miss.
A responsive defect found by rendering, not by reading
Design A's header carried <div class="wrap nav"> — both classes on one
element — and .nav { padding: 16px 0 } is declared after .wrap { padding: 0 20px }, so it silently zeroed the horizontal padding. Measured in headless
Chromium at 320px: .logo left=0.0 — the client's business name flush against
the screen edge and visually clipped, on every viewport narrower than 1080px.
Fixed (padding: 16px 20px, plus a 460px breakpoint that shrinks the logo and
the Book Now button); re-measured at 320 / 390 / 768 / 1280 and re-screenshotted
both designs. Design B was already correct and got the breakpoint anyway.
This was in the paid deliverable, on the most common phone width, and no grep would ever have found it.
📄 scraper/README.md
Open file
Lead Scraper — find every med spa in your city
This is your lead machine. One command pulls every med spa and aesthetic clinic OpenStreetMap knows about in your city (or several cities at once), checks their websites, and ranks them by how badly they need what you sell. No API key. No account. Free data.
Run it
cd scraper
python3 -m pip install -r requirements.txt # first time only
python3 leads.py --city "Scottsdale, Arizona"
That's it. You get leads.csv in this folder, sorted hottest-first — plus
lookup-list.md listing every lead that came back without a phone number
(see below).
Always include the state in --city. "Scottsdale" alone could match
several places on Earth. "Scottsdale, Arizona" matches one.
Options
| Flag | What it does | Default |
|---|---|---|
--city "City, State" |
Where to scrape (required unless you use --from-csv). Repeat it to merge cities: --city "A" --city "B" |
— |
--from-csv my-list.csv |
Skip the map entirely and score a list you typed by hand (see "When the map is thin" below). No key, no card, no map servers | — |
--niche med_spa |
Business type to hunt | med_spa |
--source osm |
Lead sources: osm (free, keyless) or google = OSM + Google Places merged (OPTIONAL, needs an API key — see below) |
osm |
--api-key KEY |
Google Places API key (only with --source google; the GOOGLE_PLACES_API_KEY env var works too) |
— |
--out myfile.csv |
Where to save results (safe to use — see below) | leads.csv |
--no-enrich |
Skip visiting lead websites (faster run) | off |
--timeout 12 |
Seconds to wait per website check | 12 |
Examples:
python3 leads.py --city "Frisco, Texas"
python3 leads.py --city "Scottsdale, Arizona" --city "Phoenix, Arizona" # two cities, one list
python3 leads.py --city "Boca Raton, Florida" --out boca-leads.csv
python3 leads.py --city "Phoenix, Arizona" --no-enrich # quick pull, no site checks
The engine also knows --niche plumber and --niche pet_groomer — same
machine, different prey — but this kit is built around med spas, so leave
the default alone unless you're experimenting.
--out is safe now, and here is why that sentence exists
Every run writes scraper/last-run.txt — always beside leads.py, never
beside --out — recording the CSV it just wrote, the lookup list, the cities
and whether it was a map scrape or a hand-built list:
run_finished=2026-08-04 02:01
leads_csv=/Users/you/med-spa-agency/scraper/boca-leads.csv
lookup_list=/Users/you/med-spa-agency/scraper/boca-leads-lookup-list.md
source=map scrape
cities=Boca Raton, Florida
prompts/01, 02, 03 and 05 read that file before they read any CSV, so
they always work on the file you actually just made.
Until 2026-08-04 they didn't: --out was documented here while every prompt
hard-coded scraper/leads.csv. A CEO who ran --out boise-leads.csv exactly
as documented had their call list built from whatever stale file was still
named leads.csv — real businesses, real phone numbers, in the wrong state,
with nothing in the output looking wrong. If your Claude ever proposes reading
scraper/leads.csv by name, tell it to read last-run.txt instead.
How it finds med spas (and skips the nail salons)
There is no "med spa" tag on OpenStreetMap. Med spas hide under generic
tags — shop=beauty, leisure=spa, healthcare=clinic — that are mostly
ordinary salons, day spas, and doctor's offices. So the scraper hunts by
branding: any business under those tags whose NAME reads medical-
aesthetic (med spa, medspa, aesthetic, botox, laser, skin, rejuvenation,
hydrafacial, injectables, IV therapy...).
That name match drags in neighbors — some of them wild ("Baskin-Robbins" has "skin" in it, "Fuel Injection Services" has "inject", "Auto Aesthetics" is a car detailer) — so a three-layer filter cleans up:
- Category filter — the business's own map tag. Hair salons, barbers, nail salons, massage-only spas, saunas, tattoo/piercing shops, tanning salons, cosmetics stores, laser-tag arenas: dropped. So are the off-niche businesses the name match snags: dentists (cosmetic dentistry is tagged as an aesthetic speciality), auto shops, ice cream stores, accountants, landscapers, restaurants.
- Name filter — names that give the game away even when the map tag is generic: "Skin & Nails", "Hair Studio", "Laser Tag", LASIK eye centers, "... Dental", "Auto ...", "... CPAs".
- Keep filter — an unmistakable med spa name ("Palm Tree Med Spa", "Botox Bar") survives layer 2 anyway, so a real lead never dies to one stray word. Category still wins: a dentist is a dentist.
Expect dermatology practices in the list. In map data, "med spa" and
"cosmetic dermatology clinic" look nearly identical, and in the field test
about a third of the rows were dermatology groups. Keep them: they sell the
same high-ticket aesthetic services and they buy websites. Just adjust the
opener — a dermatology group is a medical practice with a front desk, not a
solo injector. The rows that are NOT worth your dial (a skincare product brand, an
injector-training company) still show up. The niche filter drops 60+ map
categories and a page of name patterns, and an August 2026 QA pass added the
ones it was missing — parks, playgrounds, gardens, theatres, sports centres
and engravers, which the deliberately loose skin keyword had been dragging
in ("Skinner Park", "Merle Reskin Theatre"). Expect a stray anyway. The
filter reads a volunteer-entered map; it cannot be perfect, and we would
rather it let one theatre through than drop a real spa called "Skin Bar".
The run prints how many rows it dropped. Spot-check your leads.csv names
before a call block anyway — map data is human-entered and a stray day spa
can slip through. One glance at their website settles it, and
prompts/02-research-lead.md does that for every lead you're about to call.
Multi-city runs — how you build a full call week
Med spas cluster in affluent suburbs, and OSM coverage of them is thinner
than for old-school trades — a single city might give you 10–25 rows (our
field test: Tempe 11, Mesa 22). The
fix is built in: pass --city more than once and you get ONE leads.csv,
merged and deduped:
python3 leads.py --city "Scottsdale, Arizona" --city "Phoenix, Arizona" --city "Tempe, Arizona"
- Each lead keeps its own
cityvalue, so you always know where it came from. - The same clinic appearing in two city pulls (same phone number) becomes one row, not two. Same-name clinics in different cities are kept separate on purpose — med spa brands franchise hard ("Restore", "The Joint"-style chains), and one location per town is the normal shape.
- Field-tested: Tempe + Mesa, Arizona in one run — 33 unique leads, 31 of
them with no website in the map data. Exact counts and the honest caveats:
docs/FIELD_TEST.md.
Rule of thumb: add neighbouring cities until adding another one stops adding
rows — then stop and go build the rest of the list by hand. Do not keep
scraping toward a lead count. Measured across five metros this tool returned
47 / 16 / 34 / 1 rows and 4 / 6 / 5 / 0 dialable numbers; there is no number of
extra cities that reliably turns free map data into a full call list, and a
CEO grinding through eight cities looking for 50 leads is spending an hour to
avoid an hour. Full table: START_HERE.md, "What this actually returns".
Budget a few minutes per city, and see "How many map requests is this, really?" below before you add ten of them.
No phone number? lookup-list.md
Map data often has the clinic but not the number — most score-100 leads ship
without a phone. So the scraper writes lookup-list.md next to your CSV
(with --out boca-leads.csv it's named boca-leads-lookup-list.md).
It lists every phoneless lead in the file — not just the hot ones. They're sorted into two blocks so you know what you're looking at, but nobody is dropped:
| Block | Who's in it | Your angle on the call |
|---|---|---|
| Hottest (score 85+) | No website in the map data, or a listed site that didn't load | The rebuild — "I couldn't find a website for you" |
| Also worth a number (score under 85) | A working site, but the run found problems with it (or no way to book from the homepage) | The booking funnel, not a rebuild — check has_online_booking before you dial |
That second block matters in this niche. A spa with a perfectly nice site and no booking funnel is a real lead here, and it is exactly the lead a "hot-leads-only" list would have thrown away. An earlier version of this scraper did throw them away silently; it doesn't now.
Each entry carries a pre-built Google Maps search link. Click it, read the number off the Maps listing, paste it in. About 30 seconds per lead by hand — a page of 25 is a real 20–40 minute sitting — or paste the file's built-in prompt to your Claude and let it do the lookups with web search.
One rule, and it's in the file too: never dial an unverified number. If no listing turns up, the clinic may be gone — skip it, don't guess.
What the columns mean
leads.csv, sorted so your best calls are at the top:
| Column | Meaning |
|---|---|
name |
Business name. |
phone |
Phone number — from the map data, or pulled off their website if the map didn't have it. |
email |
Email, same two sources. Often blank; the phone is your weapon anyway. |
website |
Their site, if they have one. Blank = they don't. Blank is good news for you. |
address |
Street address when mapped. Sometimes partial — OSM data varies. |
city |
The city that run pulled the lead from (multi-city runs land in one file). |
weak_web_score |
The money column. 0–100. How weak their web presence is = how much they need you. See below. |
weak_web_reasons |
Plain-English why: "no SSL", "not mobile-friendly", "copyright frozen at 2019"... Use these lines ON the call. |
category |
How the map tags them (beauty, clinic, spa...). Compound tags keep their subtag, e.g. beauty:spa. |
opening_hours |
When they're open, if known — call when they're in. |
lat, lon |
Map coordinates. Paste into Google Maps to see the storefront. |
osm_type, osm_id |
OpenStreetMap references, for your Claude's deeper research. Blank on Google-only rows. |
source |
Where the lead came from: osm, google, manual (your hand-built list), or a combination like osm+google. |
has_online_booking |
yes / no / unknown — whether a way to book online turned up on the homepage. Read the next section before you say anything about it on a call. |
The booking column — exactly what it can and cannot know
has_online_booking is the column behind this kit's second-best pitch, so be
precise about it. It has three values and they mean three different things:
| Value | What actually happened | What you may say on the phone |
|---|---|---|
yes |
Their homepage links a booking platform this industry uses (Vagaro, Boulevard, Mindbody, GlossGenius, Square Appointments, Acuity, Cal.com, Zenoti, Aesthetic Record and ~20 more), or carries a plainly labelled booking button | Don't pitch the funnel as missing. Pitch the site, or ask how well the booking flow converts on a phone |
no |
The homepage loaded, and neither a known platform nor a booking button was anywhere in it | "I couldn't find a way to book with you online — do you take bookings through Instagram?" A question. Never "you have no online booking." |
unknown |
We never saw a homepage: no website in the map data, the site didn't load, or you ran --no-enrich |
Say nothing about booking. You have no information, and guessing here is the fastest way to sound like you did no homework |
What it reads: the homepage only. One page, once, as a browser would fetch it.
What that means it can miss — all four of these come back no when the
answer is really yes:
- A booking link that only exists on
/contact,/servicesor a "Book" page one click deeper. The scraper never clicks. - A booking widget that JavaScript loads after the page arrives. The scraper reads the delivered HTML, not a rendered browser page.
- Booking that happens entirely in Instagram DMs (arguably the right answer is still "no online booking" — but say it as a question anyway).
- A phone-only site whose "Book now" button is really a phone link — that one
goes the other way and can read as
yes.
Expect a lot of unknown. In our Scottsdale field run, 47 leads came back
as 44 unknown, 2 no, 1 yes — because 43 of those 47 had no website in
the map data at all, and a lead with no site to fetch can't be checked. That
is not a broken column; it's an honest one. The fix is the same 30 seconds
that fills in a phone number: the Maps listing shows a "Book online" button if
there is one.
prompts/02-research-lead.md re-checks booking properly, on the actual site,
for any lead you're about to call. Between the column and that prompt, the
column is the shortlist and the prompt is the fact.
The weak-web score
| Score | Meaning | Your move |
|---|---|---|
| 100 | No website in the map data. | Call first. A med spa selling $600 laser packages with no website is losing clients to whoever ranks above them — and they know it. ⚠️ Verify before you make it your opener: OSM often just hasn't recorded the site (a Tempe spa in our field test scored 100 and had a live site). The Maps listing shows it in 10 seconds — "I couldn't find a website for you" is safe, "you have no website" can be flat wrong. |
| 85 | Website listed but dead/unreachable. | Almost as good. Their link is broken and they may not know. |
| 35–80 | Site exists but has real problems: no SSL padlock, not mobile-friendly, ancient markup, frozen copyright year. | The weak_web_reasons column is your pitch, word for word. Med spa clients book high-ticket treatments from their phones — "not mobile-friendly" is a heavy line here. |
| 0–30 | Site is basically fine. | Lower priority for a rebuild — lead with the booking-funnel and no-show angle: deposits, reminders, reactivation campaigns, review engine. |
The score and the booking column are two different questions. A spa can score
0 (fine site) and still be has_online_booking: no — a decent site with a
"call to book" button is still leaking the 9pm consultation, and every no-show
on an injector's calendar is a three-figure hole. Those rows sit at the BOTTOM
of leads.csv by score and they are still worth the dial; the booking column
is how you find them.
When the map is thin — the keyless fallback (no key, no card)
OpenStreetMap is free and it is also volunteer-drawn, which means some towns are simply not mapped yet. You never need a Google Cloud billing account to keep going. The kit ships a second path that uses no map servers, no API key and no card at all: you build the list, the scraper does everything else to it.
It is the same machine, minus the map: website check, booking check, weak-web
score, ranking, lookup-list.md, ready for prompts/01-prep-calls.md.
Step 1 — get 20 names into a CSV
Copy manual-list-template.csv to a new file and type in businesses. Only the
name column is required; fill in whatever else you can see:
name,phone,website,address,city
Velvet & Vine Aesthetics,(480) 555-0142,https://velvetandvine.example,7014 E Camelback Rd,"Scottsdale, Arizona"
Sonoran Glow Med Spa,,,,"Scottsdale, Arizona"
Two honest ways to fill it:
By hand (~15–20 minutes for 20 leads). Search Google Maps for "med spa near [your city]", "botox [your city]", "laser hair removal [your city]" and "aesthetic clinic [your city]" — four searches, because each surfaces a different slice. Type the name, and the phone and website if the listing shows them. Twenty is enough to start calling.
With your Claude (~5 minutes of your time). Your Claude subscription is
the keyless data source you already own. Open Claude Code in this kit's folder
and use the ready-made prompt in build-list-with-claude.md (same
folder). Its limits, stated plainly: your Claude searches the web, it does not
have a Maps database, so it will find fewer businesses than a good scrape and
some of what it finds will be stale. The prompt makes it mark every unverified
field unknown instead of guessing — check its work before you dial, same as
always.
Step 2 — run the scraper on your list
python3 leads.py --from-csv my-list.csv --city "Your City, Your State"
The --city here is only used to label rows that didn't carry their own city.
No map query is made — the run prints map servers not used, and the
OpenStreetMap step is skipped entirely. That is the point of this path: it is
what you run when the map is the problem, so it must never re-run the thing
that just failed. (Want the map as well? That's a second, separate command
without --from-csv.)
What this path can't do
- It won't find businesses you didn't find. Coverage is your searching, not a database.
- No
lat/lon, noosm_id, no map category, so the niche filters (the ones that drop nail salons and dentists) can't help you — you are the filter. Don't type in a hair salon. - It's slower per lead than a scrape that works. Use it when the map is thin, not instead of the map.
What it does do: it gets you a scored, ranked, callable list today, with a card in your wallet and nothing else.
OPTIONAL: add Google Places as a second source
You do not need this. The keyless OpenStreetMap path is the default and it's what we field-test every kit on. But med spas are exactly the kind of business OSM under-maps — young, suburban, strip-mall storefronts — and if your rows stay thin even across 3 cities, Google Places knows almost every business in America. Google gives new billing accounts a monthly free credit ($200/mo of usage at the time of writing; check Google's current pricing page, terms move) that comfortably covers a few scraper runs. It DOES require a card on file and an API key. Setup, once, ~10 minutes:
- Go to https://console.cloud.google.com/ and create a project (any name).
- Enable billing on the project (card required — the free monthly credit applies automatically; a few runs of this scraper stay comfortably inside it).
- In "APIs & Services", enable Places API (New) — note the "(New)": it's a separate switch from the old Places API, and this scraper uses the new one.
- In "Credentials", create an API key. Optionally restrict it to Places API (New) — good hygiene.
- Run:
python3 leads.py --city "Frisco, Texas" --source google --api-key YOUR_KEY
# or, so you never paste the key again:
export GOOGLE_PLACES_API_KEY=YOUR_KEY
python3 leads.py --city "Frisco, Texas" --source google
--source google means OSM + Google merged: the free OSM pull still
runs, Google results (up to 60 per city) are added on top, and duplicates
collapse into single rows tagged osm+google. Google rows usually arrive
with phone numbers and full addresses — the two things OSM is weakest on.
The same exclude filters run on Google rows too.
Honesty note: we field-test what we can run. The keyless OSM path and
the merge/dedupe engine are tested end-to-end on real cities (see
docs/FIELD_TEST.md); the Google request path is built against the
documented API format but not tested against a live key — we don't ship
your kit with our billing account. If Google rejects your first request, the
error message the scraper prints tells you exactly what to check, and the
keyless default keeps working regardless.
Never commit or share your API key. It's tied to your card.
How it works (60 seconds)
- City lookup — each city name goes to OpenStreetMap's free geocoder to find the official city boundary.
- One Overpass query per city — asks the OpenStreetMap Overpass API (free, keyless) for every business inside that boundary that's either explicitly tagged medical-aesthetic, or carries a beauty/spa/clinic-type tag AND a medical-aesthetic name. Salon/barber/massage rows the name match drags in are dropped by the two-layer exclude filter.
- Optional Google pull — with
--source google, up to 60 Places results per city join the pot. - Merge + dedupe — same phone = same business; same name in the same city = same business (unless the phones disagree). Blanks get filled from whichever source knows more.
- Polite website checks — for each lead with a website, it fetches the homepage once (browser identity, 12s timeout, ~1 request/second) to grab missing phones/emails and score the site.
- CSV out, weakest web presence first — plus
lookup-list.mdfor every lead still missing a phone.
(With --from-csv, steps 1–4 are skipped entirely and your typed list goes
straight into step 5.)
How many map requests is this, really?
One city is not one request, and the kit used to say it was. Here is the honest shape, and the ceiling that now enforces it:
| What happens | Requests |
|---|---|
| City-boundary search, retried across mirrors | up to 5 |
| "Can this mirror even see this city?" probe, when a mirror answers zero | up to 1 per mirror, cached, and never sent to a mirror that just errored |
| Map-rectangle search, if the boundary search found nothing | up to 5 |
| Hard ceiling per city, enforced in code | 8 |
A city that answers on the first try costs 1 request and about 20–60
seconds. A city where the free mirrors are struggling can cost all 8 and
several minutes, and then the run stops and says so rather than grinding on.
Timings we measured: a healthy small city ~30 seconds; a busy-server day, 4–7
minutes to a dead end. The website-checking step ([3/4]) is separate and
prints its own estimate — roughly a second or two per lead that has a website,
so a 40-lead city is genuinely several minutes. It is not stuck; --no-enrich
skips it.
Beyond the ceiling, the scraper is polite by design: automatic backoff and mirror-switching when servers are busy, no repeat questions to a mirror that just refused us, and a slow crawl over lead websites. Don't run it in a loop.
Troubleshooting
"Missing dependency: requests" — run
python3 -m pip install -r requirements.txt. If pip is blocked on your
machine, make a virtual env first:
python3 -m venv .venv && source .venv/bin/activate, then install.
"City not found" — spelling, or you gave just the city. Use
--city "City, State" (or "City, State, Country" outside the US).
"Found ... but only as a map point" — that place name exists but not as a proper boundary. Add the state, or use the nearest larger city.
"Overpass API did not respond" — the free servers are busy. The scraper already tried hard before telling you: up to 5 attempts across 3 mirrors, twice — once against the city boundary and once against a map rectangle — inside a hard ceiling of 8 map requests for that city. Wait 5 minutes and run it again, once. If a second run fails the same way, the mirrors are having a bad day; go to the keyless path below rather than re-running a third time.
Same city, slightly different count on a re-run? Normal. The three Overpass mirrors carry snapshots taken at different times, so a rerun that lands on another mirror can differ by a lead or two (our field test saw Tempe come back 12 one run and 11 the next). Nothing is broken — merge runs if you want the union.
"OpenStreetMap has no med spas mapped inside ..." (or only a handful) — normal. Med spas are young businesses in strip malls; volunteer mappers get to them last, so OSM coverage runs thinner here than for plumbers or restaurants. Your fixes, in order:
- Add neighboring cities to the same run:
--city "A" --city "B" --city "C"— one merged, dedupedleads.csv. Aim at the affluent suburbs; that's where med spas cluster. - Scrape the county:
--city "Maricopa County, Arizona". - Build the list yourself and let the scraper score it — no key, no card:
python3 leads.py --from-csv my-list.csv --city "Your City, Your State". Full steps in "When the map is thin" above, including thebuild-list-with-claude.mdprompt. This is the day-one answer; you are never blocked on a billing account. - OPTIONAL, and only if you want it: add Google Places as a second source (section above — it needs a card on file). Thin map data means competitors aren't finding these leads either; it doesn't mean the med spas aren't there.
A day spa or salon slipped through? The filters catch what the map data
lets them catch. Delete the row and move on — and if it happens a lot in
your area, tell your Claude which names were wrong; it can tighten the
exclude regex in leads.py for your market.
"Google Places rejected the request" — the scraper prints the specific
checks: right key, Places API (New) enabled (not the old one), billing
enabled on the project. Fix, or just drop --source google — the keyless
default keeps working.
Few phone numbers? Normal — most no-website leads have no mapped phone
either. That's exactly what lookup-list.md is for: pre-built Maps links,
30 seconds per number, or hand it to your Claude (the prompt is inside the
file). It lists every phoneless lead, hot ones first, then the
working-site leads whose angle is the booking funnel — work both blocks.
A Python warning about NotOpenSSLWarning / LibreSSL before the run
starts? Ignore it. Stock macOS ships an older Python and one of the
libraries mumbles about it every single time. It is a warning, not an error —
the scrape underneath it runs normally. docs/what-you-will-see.md, section
2, shows it printed in full so you recognise it.
Is this legal? You're reading a public map and public homepages, politely, to make one-to-one sales calls. That's classic prospecting. The rules that DO bind you: no spam blasts, honor every do-not-call request instantly. It's in your operator brain (CLAUDE.md) and it's non-negotiable.
After the scrape
Open your Claude in this kit's folder and run prompts/01-prep-calls.md —
it turns leads.csv into today's prioritized call list with openers built
from the weak_web_reasons column (and fills phones from lookup-list.md
if you've worked it). Then pick up the phone.
📄 playbook/week-1.md
Open file
Week 1 — From Zip File to First Close
You're the CEO now. This week has one goal: get your first paying client. Everything below assumes you've never run a business and never made a cold call. That's fine. Follow the days in order. Your Claude does the heavy lifting; you bring the hustle.
Prices below are market rates for this work, not promises of what you'll earn. Your results depend on your calls, your market, and your follow-through.
One niche note before day one: med spas are premium businesses run by image-conscious owners. Your edge isn't being cheap — it's being sharp, specific, and honest. Every call, text, and mockup should feel like it came from a boutique agency. The kit is built to make that easy.
Day 1 — Setup + Scrape (about 2 hours)
Goal: kit running, your area scraped, a lead list on your screen.
See what you sell (10 min, no setup). Work through
docs/see-it-first.md— five finished files you just double-click. The filled client website (fulfillment/website-template/EXAMPLE/index.html), both designs of your own storefront already built, the proposal, the agreement. You cannot pitch something you've never seen rendered, and this is the cheapest ten minutes in the week.Connect your Claude. Follow
docs/connect-claude.mdstart to finish — including Step 2.5 (Python) and the section on what Claude Code asks the very first time you run it. Nervous about the terminal? Readdocs/what-you-will-see.mdfirst; it's every screen of today printed in advance with "good news / bad news / just noise" beside each one. You're done when you open this folder in Claude Code, ask "what business is this?", and Claude answers like it runs the place.Fill in your details. Open
config/business.yamland edit: your name, your agency name, your city/region, your contact info. Keep the pre-filled prices unless you have a reason to change them — they're market-rate defaults ($1,000–$2,500 builds, $199–$499/mo care plans).Run the scraper. In Claude Code, just say:
Run the lead scraper for my city.
Claude runs
scraper/leads.pyand producesleads.csv— every med spa and aesthetic clinic it can find in your area, with phones, websites (or the telling absence of one), and a "weak-web" score. Med spas are a thin niche per city, so the standard move is scraping 2–4 neighboring cities in one merged run — Claude knows how. No API key needed; day one is free.What it actually returns, measured, every run we have. Read this now rather than at 9am tomorrow:
Metro (all cities in ONE command) Rows Dialable Scottsdale + Tempe + Mesa, AZ 47 4 Boise metro, ID (4 cities, ~800k people) 16 6 Charlotte, NC (single city) not recorded 5 Naples + Fort Myers + Cape Coral + Bonita Springs, FL 34 5 Sarasota + Bradenton + Venice + Lakewood Ranch, FL 1 0 That bottom row is a wealthy 800,000-person metro with plenty of real med spas in it. Free map data simply doesn't have them. So the list you dial this week is usually one you and Claude built, not one the scraper handed you — and that is the normal path, not the rescue path. Run the scraper anyway: five minutes, sometimes thirty rows, and it always produces the lookup list that makes the building fast. Then budget an evening for
scraper/lookup-list.md(~30 seconds a row) andscraper/build-list-with-claude.md(Claude does the searching).Look at your list. Ask Claude:
Show me the top 20 leads by weak-web score.
These are your first calls. Notice the pattern: spas with clearly premium work and websites that don't match it — or no online booking at all. That gap is the whole business, right there.
Two columns come with a warning label, and both are about not saying something wrong on the phone. Score 100 means the map has no website on file for them — not that they have none.
has_online_booking: unknownmeans the scraper never got to see a homepage — not that they have no booking. Say nothing you haven't checked; Claude verifies per lead before you dial. (scraper/README.md, "The booking column".)Came back thin? That's the map, not your market, and it doesn't need a credit card to fix. Ask Claude to add 2–3 neighbouring cities to the run — and if it's still thin,
scraper/build-list-with-claude.mdhas Claude build you a list from web searches instead, which the scraper then scores exactly the same way. About ten minutes.Optional but smart: deploy your own agency site tonight.
agency-site/has the instructions — drag-and-drop on Netlify, free, ~15 minutes. In this niche especially, "check us out at..." matters: owners will look you up before calling back.
Done when: leads.csv exists, config/business.yaml has your info in it.
Day 2 — Your First Call Block
Goal: 10–15 dials. Not 10–15 sales. 10–15 dials.
That number is not timidity, it's your phone number's survival. A brand-new
business line making 30 calls on its first day is the exact pattern the carrier
spam filters watch for, and once you're labelled "Spam Likely" nothing in your
experience explains why nobody picks up. The ramp — 10–15 on days 1–3, 15–20 on
days 4–7, 25–30 from week two — is in docs/your-phone-setup.md, and this
playbook follows it exactly.
Nerves are normal. The script carries you.
Morning prep (20 min). In Claude Code, use the prompt in
prompts/01-prep-calls.md. Paste it (or just ask Claude to run it). It turnsleads.csvinto today's prioritized call list with a custom opener for each lead. Numbers first, though: map data usually has the clinic but not its phone — in our field tests only 2 of 33 leads (Tempe + Mesa) and 4 of 47 (Scottsdale) shipped with a number. The scraper wrotescraper/lookup-list.mdwith a pre-built Google Maps link for every phoneless lead — the hottest first, then the ones with a working site whose angle is the booking funnel. Click, read the number off the listing, paste it in (~30 seconds each), or hand the file to Claude. Budget a real 20–40 minutes for the first 20–25 numbers, ideally the evening before. That's more than day 2 needs on purpose — it gives you a buffer for day 3 and day 4 too. Never dial a number you didn't verify.And don't wait on a first name.
leads.csvhas business names, not owners' names, and nothing in this kit produces one. Claude looks names up where it can during prep (1–2 minutes each, roughly half succeed) and marks the restunknown— which is your cue to use the no-name opener insales/call-script.md. You get the name from the front desk on call one and use it on call two. A guessed name is worse than no name.Print or open
sales/call-script.md. Short on purpose: opener, 3 discovery questions, pitch, price anchor, close. Read it out loud twice before your first dial. Then read objection #3 insales/objections.md— the HIPAA/medical-claims answer — until you can say it cold. It's the one question in this niche you must never fumble.Open
sales/objections.mdin a second window. You will hear "we already have a website" and "we get everything from Instagram" today. Both are covered, word for word.Call Tue–Fri, 10am–12pm or 2–4pm local. Many spas are closed Mondays and slammed Saturdays. If day 2 lands on a Monday, swap in mockup/admin work and dial Tuesday.
After every call, tell Claude what happened. One line is enough:
Called Glow Aesthetics — front desk, owner is Dana, call back Thursday 2pm. Called Lumière Med Spa — talked to the owner, sending mockup.
Claude keeps your pipeline. This is how nothing falls through the cracks.
No-answers get the templates in
sales/outreach.md— voicemail + day-1 SMS/email right after the missed call. The follow-up cadence is day 1/3/7.
What a 10–15 call block realistically looks like: many go to the front desk — logging the owner's name and best time IS a win. A handful of owners will talk. One or two will be genuinely interested. That's a good day. Log everything.
Done when: 10–15 dials logged with Claude, follow-up texts sent to no-answers.
Day 3 — Another 10–15 Calls + Research the Warm Ones
- Before calling anyone who showed interest, run
prompts/02-research-lead.mdon them. Claude deep-dives one lead: their site, their booking flow (or lack of one), the Instagram-to-website gap, what to say. Walking into a call knowing their business beats any script. - 10–15 fresh dials from the next batch on your list. Same routine as Day 2 — still days 1–3 of your new number, so hold the ramp.
- Day-3 follow-ups go out to Day 2's no-answers (templates in
sales/outreach.md). - End of day, ask Claude:
Pipeline status — who's warm, who do I call back, what did I learn today?
Done when: 20–30 total dials, every warm lead researched.
Day 4 — Calls + Your First Real Pitch
By now someone has said "tell me more." Today you pitch properly.
- The pitch, in one breath: "I build med spas a website that finally matches their work — premium, mobile-first, with a booking funnel that captures a new client's name and number the moment she's interested, even at 9pm. Live within the week. Builds like this run $1,000–$2,500 one-time, I handle everything, and all treatment copy stays in your words."
- Show, don't tell. Before a pitch call, ask Claude to spin up a mockup
from
fulfillment/with the lead's branding on it (prompts/03-fulfill.md, mockup mode). "I already designed yours" closes more than any argument — and in this niche, a beautiful mockup IS the proof of competence. - Keep dialing. 15–20 more calls around your pitch appointments — day 4 is where the ramp lets you step up.
- The close is in
sales/call-script.md. Ask for a yes, take a 50% deposit (Stripe or PayPal link —prompts/04-invoice.mdshows Claude how to set this up), and book the delivery window: "Live within the week once your answers are in, preview a couple of days before that."
Done when: at least one full pitch delivered. Closed or not — you pitched.
Day 5 — Close + Book the Delivery Slot
- Call back everyone who said "let me think about it." The callback line: "Wanted to catch you before I fill this month's build slots — should I hold one for you?" Scarcity is honest here: you genuinely can only build so many at once.
- When someone says yes:
- Take the deposit (half up front is standard;
prompts/04-invoice.mdhandles the invoice). - Send the intake questions (
prompts/03-fulfill.mdprints them — branding, services, providers, and the treatment copy THEY supply; never write medical copy for them). - Book the delivery window — a real date. Same-week delivery is your edge. The AI makes it possible.
- Take the deposit (half up front is standard;
- Day-7 follow-ups go out to Day 2's silent leads.
Day 6 — Build (or Keep Calling)
If you closed: open prompts/03-fulfill.md in full-build mode. It drives
Claude through the fulfillment/ template step by step — the client's branding,
services menu, booking funnel, reviews section. A first build takes an
afternoon or two. Deliver what you sold, exactly — and remember the client
approves every page (their treatment copy especially) before anything goes live.
If you haven't closed yet: that is the expected state at day 6, not a
warning sign. Another 15–20 dials if you have the numbers; list-building if you
don't. Nobody has run this kit and closed anything, so we can't tell you what
week a first close typically lands in — what we can tell you is that a
$1,000–$2,500 decision made by a business owner who has never heard of you is
not usually made on the first call, and the day 1/3/7 cadence in
sales/outreach.md exists for exactly that reason. The planning arithmetic is
in playbook/month-1.md, clearly labelled as invented placeholders.
Day 7 — Review + Reset
Run prompts/05-weekly-review.md with Claude. It walks the week: calls made,
conversations had, pipeline state, follow-ups due, and next week's plan.
Then rest. Real businesses take a day off.
Week 1 Scoreboard
Read the first row before the others, because it is the one that decides your week and it is the one you don't fully control.
| Metric | Target | Who controls it |
|---|---|---|
| Verified phone numbers in hand | 40+ by day 7 | You and your metro |
| Dials made | 55–70 | You |
| Owner conversations | — log it, don't target it | Neither |
| Pitches | — log it, don't target it | Neither |
| Closes | — log it, don't target it | Neither |
Why 55–70 dials and not 100. That is what this kit's own phone-safety ramp
allows in one week: 10–15 on days 2 and 3, 15–20 on days 4 and 6
(docs/your-phone-setup.md). A week-one target of 100 dials would require
either breaking the ramp — which is how a new business number gets flagged
"Spam Likely" and stops being answered — or having 100 verified numbers on day
two, which no run of our scraper has ever produced. An earlier version of this
page printed "100+" as the target. It was unreachable by the kit's own rules
and it is gone.
If your metro gives you fewer than 40 verified numbers by day 7, week one's
job was list-building, and you are on schedule. That is not a consolation
prize and it is not a failure — it is the most common shape of week one, and
the measured yields in docs/FIELD_TEST.md say so plainly. Spend day 6 on
scraper/build-list-with-claude.md instead of on a build, and dial in week
two with a list that can actually carry it.
Why conversations, pitches and closes have no targets here. Nobody has ever
run this kit and counted them, so any number we printed would be invented — and
a target you can miss without doing anything wrong is how a first-timer
concludes they failed in week one and quits. Log all three from day two. By the
end of week two they are your numbers, and prompts/05-weekly-review.md will
tell you which stage is actually leaking.
Fewer conversations than a trades niche would give you? Expect that — more front-desk screening, thinner lists. The only thing you can fail at in week 1 is not making the calls you had numbers for.
The rest of the box
The 🔒 files — CLAUDE.md (the operator brain), the prompt pack, the sales scripts, the fulfillment templates and the agency site — unlock when you become CEO. Market rates, not promises: the open files above tell you exactly what the machine does; the locked ones are the machine.