AI Business Marketplace

We build the businesses. You buy the code and run them up*

Med Spa Agency

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 domainconfig/business.yaml feeds them into every document automatically.
  • Your prices — the defaults are market rates, not a price list you're bound to. office/quoting-guide.md shows you how to move them.
  • Your storefront copyagency-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 (see agency-site/README.md, "Make it yours"). This niche judges you on polish and sameness reads as a template.
  • Your call openersales/call-script.md is 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 --out to keep per-city files, that's fine. The run records the path in scraper/last-run.txt and 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 to boise.csv left the prompts quietly reading whatever stale file was still called leads.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:

  1. 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.
  2. scraper/build-list-with-claude.md — a ready-made prompt where your Claude searches the web and builds the list from scratch into my-list.csv, which --from-csv then 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, type Terminal, 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 python3 stub Windows ships. It is not Python. Install it below.
  • Python 2.7.x → too old. Install it below.

Install it — Mac

  1. Go to https://www.python.org/downloads/macos/
  2. Click the "Download Python 3.x.x" button.
  3. Open the downloaded .pkg and click through the installer.
  4. Close your Terminal window and open a fresh one (it only notices new programs on startup).
  5. 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, not python3. Everywhere this kit says python3 something, you type py -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).

  1. Go to https://console.anthropic.com and create a Console account.
  2. Add a small amount of prepaid credit ($5–$10 is plenty to start).
  3. 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_KEY environment 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:

  1. 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.
  2. 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.
  3. 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:

  1. Sign in at https://claude.ai (any paid plan).
  2. 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).
  3. Upload or paste CLAUDE.md from this kit as your first message, with: "You are the operator brain for this business. Read this and act accordingly."
  4. Verify the same way: ask "what business is this?" — you should get the med-spa-agency answer.
  5. Upload the other files as you need them: sales/call-script.md before calling, prompts/01-prep-calls.md plus your leads.csv for 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-csv path 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 to lookup-list.md with 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 loose skin keyword. 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.md so 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 nested pricing: 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.yaml byte-identical to its shipped state (verified with diff).

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 -r the template, rm README.md as 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, including photos/README.md (no tokens in it). The page still parses as valid HTML.
  • fulfillment/review-engine/make_qr.py was run (with segno, 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.md all 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.md matches the generated CSV exactly (15 columns, source last). (Superseded 2026-08-03: the CSV now has 16 columns, has_online_booking last — 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 in config/business.yaml.
  • Tracker stages and offer values used by prompts 03/04/05/06 all match office/tracker-guide.md.
  • office/invoice.html tokens are all documented in office/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

  1. 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_categories widened to cover dentists, auto shops, food, and professional-services map tags — the categories that actually showed up; (b) exclude_name_regex extended (\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.
  2. 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.md and prompts/01-prep-calls.md now require "I couldn't find a website for you" instead of "you have no website", and lookup-list.md tells the CEO to check the Maps listing for a website while they're grabbing the phone number.
  3. Care-plan Standard tier was priced two ways. $299 in config/business.yaml and fulfillment/care-plan-sop.md, $349 in office/proposal-onepager.html, playbook/growth.md and office/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.
  4. The delivery promise contradicted itself. Storefront, START_HERE.md, CLAUDE.md and 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.
  5. Calling windows contradicted themselves. CLAUDE.md said 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 in CLAUDE.md and START_HERE.md.
  6. CLAUDE.md's kit map was missing two fulfillment filescalcom-setup.md (the booking page) and reminder-system.md (the no-show killer). The operator brain navigates by that table, so both are now rows in it.
  7. START_HERE.md taught 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 to playbook/week-1.md (Day 1 counts, Day 2 lookup budget) and CLAUDE.md's day-one sequence.
  8. scraper/README.md cited a field test that had never happened ("Scottsdale + Phoenix… see docs/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-runmaps.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

  1. has_online_booking was undocumented and over-promised. START_HERE.md told 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 are unknown, because 43 leads have no website to check. Fixed: the column and its three values are documented in scraper/README.md ("The booking column"), including the four things a homepage-only check misses; START_HERE.md now carries the three-value table; prompt 01 forbids mentioning booking at all on an unknown lead; the scraper prints the unknown count and the rule at the end of every run.
  2. Every call opener and outreach template required [FIRST NAME] and nothing in the kit produces one. leads.csv comes from a map — business names, never owners' names — and there was no fallback line anywhere. Fixed on both sides: sales/call-script.md opens 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 in sales/outreach.md carries 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, unknown otherwise, 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.
  3. 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.md and playbook/week-1.md all 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.
  4. docs/connect-claude.md never 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 to pip install with no install step). Added: Step 2.5 (Install Python, all three platforms, the externally-managed-environment venv 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. New docs/what-you-will-see.md matches the plumber kit's standard with real transcripts from this session.
  5. The NotOpenSSLWarning / LibreSSL block. 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.
  6. make_qr.py needed a package no requirements file declared. Added fulfillment/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.
  7. 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. Added fulfillment/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) and agency-site/EXAMPLE/daylight/ + EXAMPLE/eventide/ (both designs built by the kit's own fill.py), plus docs/see-it-first.md as the 20-minute browser tour. config/business.yaml was restored to pristine afterwards and verified. Follow-on defect caught while doing it: the delivery checklist's cp -r fulfillment/website-template … would have copied the new EXAMPLE/ folder into a paying client's site folder and deployed it. The copy sequence in the checklist, the template README, fulfillment/README.md and prompt 03 now delete it explicitly; re-tested.
  8. 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-csv path already existed in the code and appeared nowhere in scraper/README.md, not even in the options table. Now documented as a first-class path ("When the map is thin"), with new scraper/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.md now 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 no for 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.md says 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:

  1. fulfillment/care-plan-sop.md — "this is why nobody cancels"
  2. fulfillment/review-engine/monthly-cadence.md — same phrase
  3. playbook/growth.md — "the monthly report is why nobody cancels"
  4. fulfillment/reactivation-campaigns/campaign-sop.md — "the richest pool because nobody has ever nudged them", a stated fact about a client's own customers
  5. playbook/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.html vs fulfillment/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 only leads.py, requirements.txt, README.md, build-list-with-claude.md and manual-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.md now 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 the agency-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.md said start at 10–15 and ramp to 30; week-1.md said "Goal: 25 dials" on day one and CLAUDE.md said "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.md added. 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 into CLAUDE.md and prompts/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:

  1. 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.
  2. 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".
  3. 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 city value, 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, /services or 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, no osm_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:

  1. Go to https://console.cloud.google.com/ and create a project (any name).
  2. Enable billing on the project (card required — the free monthly credit applies automatically; a few runs of this scraper stay comfortably inside it).
  3. 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.
  4. In "Credentials", create an API key. Optionally restrict it to Places API (New) — good hygiene.
  5. 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)

  1. City lookup — each city name goes to OpenStreetMap's free geocoder to find the official city boundary.
  2. 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.
  3. Optional Google pull — with --source google, up to 60 Places results per city join the pot.
  4. 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.
  5. 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.
  6. CSV out, weakest web presence first — plus lookup-list.md for 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:

  1. Add neighboring cities to the same run: --city "A" --city "B" --city "C" — one merged, deduped leads.csv. Aim at the affluent suburbs; that's where med spas cluster.
  2. Scrape the county: --city "Maricopa County, Arizona".
  3. 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 the build-list-with-claude.md prompt. This is the day-one answer; you are never blocked on a billing account.
  4. 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.

  1. 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.

  2. Connect your Claude. Follow docs/connect-claude.md start 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? Read docs/what-you-will-see.md first; 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.

  3. Fill in your details. Open config/business.yaml and 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).

  4. Run the scraper. In Claude Code, just say:

    Run the lead scraper for my city.

    Claude runs scraper/leads.py and produces leads.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) and scraper/build-list-with-claude.md (Claude does the searching).

  5. 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: unknown means 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.md has Claude build you a list from web searches instead, which the scraper then scores exactly the same way. About ten minutes.

  6. 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.

  1. 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 turns leads.csv into 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 wrote scraper/lookup-list.md with 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.csv has 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 rest unknown — which is your cue to use the no-name opener in sales/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.

  2. 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 in sales/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.

  3. Open sales/objections.md in a second window. You will hear "we already have a website" and "we get everything from Instagram" today. Both are covered, word for word.

  4. 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.

  5. 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.

  6. 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

  1. Before calling anyone who showed interest, run prompts/02-research-lead.md on 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.
  2. 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.
  3. Day-3 follow-ups go out to Day 2's no-answers (templates in sales/outreach.md).
  4. 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.

  1. 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."
  2. 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.
  3. Keep dialing. 15–20 more calls around your pitch appointments — day 4 is where the ramp lets you step up.
  4. The close is in sales/call-script.md. Ask for a yes, take a 50% deposit (Stripe or PayPal link — prompts/04-invoice.md shows 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

  1. 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.
  2. When someone says yes:
    • Take the deposit (half up front is standard; prompts/04-invoice.md handles the invoice).
    • Send the intake questions (prompts/03-fulfill.md prints 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.
  3. 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.