AI Business Marketplace

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

Pet Groomer Agency

Pet Groomer 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.

pet-groomer-agency/
├── START_HERE.md 📄 preview available
├── CLAUDE.md 🔒 Unlocks with purchase
├── config/
└── business.yaml 🔒 Unlocks with purchase
├── scraper/
├── hand-list-template.csv 🔒 Unlocks with purchase
├── leads.py 🔒 Unlocks with purchase
├── README.md 📄 preview available
├── requirements.txt 🔒 Unlocks with purchase
└── self-test.py 🔒 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/
│   │   ├── a/
│   │   │   ├── index.html 🔒 Unlocks with purchase
│   │   │   ├── script.js 🔒 Unlocks with purchase
│   │   │   └── style.css 🔒 Unlocks with purchase
│   │   └── b/
│   │       ├── 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-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
├── rebooking-campaigns/
│   │   ├── campaign-sequences.md 🔒 Unlocks with purchase
│   │   ├── pricing.md 🔒 Unlocks with purchase
│   │   ├── README.md 🔒 Unlocks with purchase
│   │   ├── run-a-campaign.md 🔒 Unlocks with purchase
│   │   ├── seasonal-calendar.md 🔒 Unlocks with purchase
│   │   └── sell-in-one-pager.md 🔒 Unlocks with purchase
├── review-engine/
│   │   ├── make_qr.py 🔒 Unlocks with purchase
│   │   ├── monthly-cadence.md 🔒 Unlocks with purchase
│   │   ├── pricing.md 🔒 Unlocks with purchase
│   │   ├── qr-card.html 🔒 Unlocks with purchase
│   │   ├── README.md 🔒 Unlocks with purchase
│   │   ├── requirements.txt 🔒 Unlocks with purchase
│   │   ├── review-flow.md 🔒 Unlocks with purchase
│   │   └── sell-in-one-pager.md 🔒 Unlocks with purchase
└── site-template/
├── index.html 🔒 Unlocks with purchase
├── PLACEHOLDERS.md 🔒 Unlocks with purchase
├── EXAMPLE/
│       │   ├── index.html 🔒 Unlocks with purchase
│       │   ├── README.md 🔒 Unlocks with purchase
│       │   └── photos/
│       │       ├── gallery-1.jpg 🔒 Unlocks with purchase
│       │       ├── gallery-2.jpg 🔒 Unlocks with purchase
│       │       ├── gallery-3.jpg 🔒 Unlocks with purchase
│       │       ├── gallery-4.jpg 🔒 Unlocks with purchase
│       │       ├── gallery-5.jpg 🔒 Unlocks with purchase
│       │       └── gallery-6.jpg 🔒 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
├── 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 — Pet Groomer Agency Kit

You just bought a starter system — not a business with customers, and not an idea either. It's the whole machine: the lead scraper, the sales scripts, the product you deliver, the paperwork, the prompts that make your Claude run it, and the playbook that tells you what to do every day. There's no revenue in the box. You bring the calls; everything else is here.

You're the CEO. Your Claude is the operator.

Day 1 is setup and building your call list. Day 2 is calling it. Today is roughly two hours: install Claude Code and Python, fill in one config file, sort out a phone number, run the scraper. Nobody gets dialled today, and that is the plan working, not a delay.

What you actually own — 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 the first session.

What this business does: local pet grooming studios are booked solid but bleed money on no-shows. You sell them a booking page plus a reminder system — $300–$800 one-time setup at market rates, with a $79–$199/mo care plan behind it. (Market rates, not promises — your calls and your follow-through set your numbers.)

Be precise about what "reminder system" means, because you're the one selling it:

Layer Who does it
Booking confirmation email, sent the second someone books Automatic
24-hour email reminder before the appointment Automatic — a free Cal.com feature; check it's available on the account (fulfillment/reminder-system.md)
Morning-of text from the studio's own phone A 2-minute manual routine you set up and teach the owner at handoff

That third layer is a text from the studio's own number, sent by a human each morning. It is not automatic software. Nobody has measured which of the three layers stops more no-shows than the others — this kit has never delivered a system to a groomer — so describe the mechanism and never rank it. Say it that way on calls. Selling "set it and forget it" and then delivering a morning routine is the fastest way to get a refund request.

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. And LICENSE.md is the two-minute read on what you actually own.


1. Connect your Claude and install Python (~25 min, one time)

Open docs/connect-claude.md and follow it top to bottom. Zero experience needed — it walks you from "never opened a terminal" to Claude Code running in this folder.

You're done when you type "what business is this?" and your Claude answers like it runs the place — offer, prices, and "want to start day one?" From that moment you have an operator: it knows every file in here and what it's for.

⚠️ 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 the guide walks it through for Mac, Windows and Linux — including the two messages that look like errors and aren't. Skip it now and you'll meet it in ten minutes anyway, without the instructions.

2. Fill in your one config file (~5 min)

Open config/business.yaml in any text editor (or just tell Claude your details and let it edit). Your name, your agency name, your city ("City, State" — the scraper needs the state), phone, email. Everything in the top section ships as a placeholder and every line of it has to be replaced. Leave the pre-filled prices alone; those are market-rate defaults.

You're naming a business here. Before money changes hands, skim docs/setting-up-shop.md — one page on sole proprietor vs LLC, whether your invented agency name needs a DBA, a separate bank account, and setting money aside for tax. Not legal or tax advice, and it takes five minutes.

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 the config file is where the rebrand starts, not where it ends. Over your first week, replace:

  • Your agency name and domain — the config carries them into every document automatically.
  • Your prices — the defaults are market rates, not a price list you're bound to.
  • 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").
  • Your call openersales/call-script.md is a script to learn, then loosen. It should sound like you by call fifty.
  • The demo names and the six example photos — everything in the EXAMPLE/ folders is an invented studio for you to look at. Never deploy one, never send one to a prospect, and never put those photos on a paying client's site.

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 (~15 min, free) — 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 20+ 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 bites here specifically: the groomer you want is alone in the shop with a dog on the table and both hands full, already deciding in half a second whether a ringing phone is worth putting the clippers down for.

Google Voice is free and takes fifteen minutes. The doc covers the setup, the free caller-ID registration that puts your agency name on the screen, how to spread week one's dials — 12/15/18/20/20 across days 2–6, 85 in all — so a brand-new number doesn't get flagged on Tuesday, and what to do if it happens anyway.

3. First scrape (about 4 min for one city, 10–15 for several)

🧭 Read this paragraph before you run anything, because it reframes day one.

In a thin metro, the scraper's job is to prove the niche is real and hand you the map's names. Most of your first 25 phone numbers you will build by hand — and that is the plan working, not the tool failing.

Measured cold in Knoxville, Tennessee (900k metro), keyless, 2026-08-04: one city gave 5 rows / 3 dialable in 2m 25s. Adding six more metro towns gave 7 rows / 3 dialable in 12m 50s — six extra towns, thirteen extra minutes, zero extra phone numbers. What moved the number was five rows typed by hand: 3 dialable → 8.

So the order that actually works on day one is: run the scrape once to see what the map holds, then go straight to the hand-built list (step 3c) — ahead of adding cities, not after it. Your Claude does the searching. Plan 60–90 minutes for 20–25 numbers, whether you do it or Claude does: the big directory sites (yellowpages, superpages) answer automated reads with an HTTP 403, so there is no page to harvest in bulk. It is one search per business, and a few come back name-only.

Nobody gets dialled on day one. Day one is setup and list-building. Say that to yourself now so day two doesn't feel like a failure.

Ask your Claude:

Run the lead scraper for my city.

Or run it yourself:

cd scraper
python3 -m pip install -r requirements.txt
python3 leads.py --city "Mesa, Arizona"     # your city here

📄 The scraper/ folder ships with no leads in it. leads.csv and lookup-list.md don't exist until you run the command above — that way the only names you ever see are from your own city. If you want to know what the output looks like first, docs/what-you-will-see.md shows a whole real run, line by line. If you want to see the finished product you'll be selling, docs/see-it-first.md is the browser tour.

⚠️ Read docs/what-you-will-see.md first — five minutes, and it's the page that stops you quitting on day one. A perfectly healthy run prints two lines that look like errors and aren't (a macOS NotOpenSSLWarning and a pip "version 26.0.1 is available" warning), plus Overpass is busy — waiting 8s, then retrying. That page shows the whole run as it really prints, line by line, with a verdict on each one.

Free, no API key, no account. Out comes scraper/leads.csv — every grooming studio OpenStreetMap knows in your city, ranked by how badly they need you (no website in the map data = score 100 = call first — but confirm it before you say it on a call; the map is sometimes just missing the link).

The number that matters is the one in the box. The run ends by printing:

      ===============================================
      DIALABLE NOW: 6
      ===============================================

That's rows with a phone number attached to an actual independent studio. Row count is not a call list — you can't ring a row. If DIALABLE is under 25, the scraper prints a numbered list of fixes with the exact command to type next, free ones first and the one that needs a credit card last and clearly labelled. Nothing on day one requires a card.

How long it really takes. Timed runs (2026-08-03, Phoenix metro): one city 1m 28s, four cities 3m 17s, all seven 6m 48s. A different metro on a different day (Knoxville, 2026-08-04) took 2m 25s for one city and 12m 50s for seven — roughly double, same commands. And on 2026-08-04 the free servers were busy enough that a Lexington, Kentucky run could not get an answer at all across seven retries on three mirrors, and exited with the "servers are busy, try again in five minutes" message. That is the real range: 90 seconds to 'come back later'. Treat the timed numbers as the floor, not the number — "Overpass is busy — waiting 8s/20s/45s" happened repeatedly on every run, and on a busy afternoon the same command can take two or three times as long. That message is the free map servers being busy, it's normal, and each retry adds 8–45 seconds. It is never instant and it is never broken just because it's slow. Start it and go make coffee.

Grooming lists run thin, and that is the niche, not your city. Two metros, two days, same machine, all keyless and no card:

Command Leads Dialable Lookups needed Run time
Mesa, Arizona 15 2 13 1m 28s
Mesa + Gilbert + Chandler + Tempe 35 2 33 3m 17s
+ Scottsdale + Glendale + Phoenix (7) 60 8 52 6m 48s
Knoxville, Tennessee 5 3 2 2m 25s
Knoxville + 6 metro towns 7 3 2 12m 50s
Knoxville + 5 rows typed by hand 12 8 2

Read the dialable column, not the leads column, and read the last three rows twice. In Phoenix, three extra cities bought twenty extra leads and not one extra phone number. In Knoxville, six extra towns bought two extra rows and zero extra numbers, for thirteen minutes. The thing that moved the number in both metros was work done by hand. Run the scrape, then go to step 3a. (This table is copied from docs/FIELD_TEST.md; if the two ever disagree, that file wins.)

3a. Build the rest of the list by hand — this is day one's real job

This used to be filed under "if your map is genuinely empty". It has been moved up, because in every thin metro we have measured it is where nearly the whole call list comes from.

Copy scraper/hand-list-template.csv, search "pet groomer near [your city]" on Google Maps, and fill in what you find. Only the name column is required — the scraper checks the websites, scores them, spots who already takes online bookings and ranks them alongside the map rows. Then:

python3 leads.py --city "Your City, State" --merge-csv my-list.csv

Free, no key, no card, works in every city on Earth. Ask your Claude to do the searching — it is exactly the kind of chore it is for, and the prompt is already written inside scraper/lookup-list.md.

The honest timing: 60–90 minutes for 20–25 numbers. Not "about an hour for 20", which is what this file used to say. Measured: 5 verified numbers in ~15 minutes. The reason it is not faster is that yellowpages.com and superpages.com both return HTTP 403 to an automated read, so nothing can be harvested in bulk — it is one search per business, and several come back with a name and no number.

Two rules while you do it: never invent a number, and spot-check a couple against the business's own listing before you dial.

3b. Most leads won't have a phone number — that's normal (this is the long part)

Read this before you decide the scraper is broken. It isn't.

Map data usually has the studio but not the number. In our own field test, a Mesa run returned 48 real studios and 6 of them had a phone number. The other 42 went into scraper/lookup-list.md — a file the scraper writes for you automatically, right next to leads.csv.

Every row in that file already has a Google Maps search link built for it. Click it, read the number off the listing, paste it into leads.csv. Budget about a minute each — 30 seconds is the best case, and the average includes reading the listing, checking it's actually the right business, and pasting it back. Or paste the prompt that's already inside lookup-list.md into your Claude and let it do the lookups for you, which is faster and the reason this step doesn't have to eat your evening.

Real counts from our timed runs — this is the size of the job, not a guess:

Your scrape Leads with no phone → lookups
One city (Mesa, 48 leads) 42
Four cities (56 leads) 48
  • One city (~48 leads): budget 40–50 minutes by hand.
  • A 50+ lead pot: budget 45–60 minutes by hand, and expect 50-odd lookups, not 40. You do it once per lead; you never redo the same one.
  • Handing the file to your Claude cuts that materially — do that first and spot check its work rather than typing 52 searches yourself.
  • Never dial an unverified number. If no listing turns up, the studio may have closed — skip the row, don't guess.

That's setup: about 2–3 hours end to end, most of it lookups — the same number playbook/week-1.md Day 1 gives you. You now have a real call list with real numbers on it.

4. First calls (tomorrow morning)

  1. Ask Claude to run prompts/01-prep-calls.md — it turns leads.csv into today's calls, each with a custom opener. It builds a list of 25; you are dialling the first 12 of them tomorrow (see below) and the rest across the week.
  2. Read sales/call-script.md out loud twice. It's short on purpose — opener, 3 questions, pitch, price, close. Keep sales/objections.md open in a second window.
  3. Dial between 10am and 2pm. 12 dials is tomorrow's goal — not 12 sales, 12 dials. That's day 2 of the 12/15/18/20/20 ramp in docs/your-phone-setup.md; a brand-new number that opens with 25 in a morning is how you get labelled "Spam Likely" in week one. Tell Claude what happened after each call, one line each. It keeps the pipeline.

Your day-by-day map for the whole first week — through mock-ups, pitches, and your first close — is playbook/week-1.md. Your Claude knows it cold; when in doubt, just ask "what's next?"

5. Two files to read before anyone pays you

They save you the two arguments that ruin a first client:

  • office/service-agreement.md — the one-page plain-English agreement every client signs before you build. What you deliver, what you don't, how many revision rounds, who owns the accounts, deposit and refund terms. It's written; you fill in a handful of brackets. office/service-agreement.html is the same thing as a printable page.
  • docs/rules-of-the-road.md — the legal side in plain English: calling and do-not-call, texting, email, and the campaigns you'll later run on a client's behalf. Fifteen minutes, not legal advice, and it keeps you out of trouble. Read it before your first call block, not after.

Bonus tonight (~15 min): deploy your own agency site free — agency-site/README.md, drag-and-drop, no hosting bill. "Check us out at..." makes tomorrow's calls easier.

One catch, and the script enforces it: fill.py refuses to build while config/business.yaml still holds example values. If it stops and lists which fields are still placeholders, fill those in and run it again. It's doing that on purpose — a public agency site with a fake phone number on it is worse than no site at all.

Want to see what you're selling before you sell it? Open fulfillment/site-template/EXAMPLE/index.html in your browser — a finished client booking page for a made-up studio, every field filled in. Then office/EXAMPLE-invoice.html, office/EXAMPLE-proposal.html, and office/EXAMPLE-service-agreement.html. Five minutes, and you'll pitch better for it. The full "open these in a browser" tour is docs/see-it-first.md.


The only way to fail week 1 is to not make the calls. Everything else, your operator handles. Go.

📄 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 installing/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, verify Proves the operator brain loaded 5 min

Nothing here costs money except your Claude plan. Python and Claude Code are free downloads.


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 booking pages.

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, and it is the first thing you run on day one. Python is a free programming-language runtime. Nothing else in this kit needs it — but the scraper is the whole lead machine, so five minutes now saves you an hour tomorrow.

This is the single most common place people get stuck. It is also the hardest technical thing in the entire kit, and once it's done it's done forever.

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.

docs/what-you-will-see.md shows a whole real run, line by line, with a verdict on every line. Read it once before your first scrape.

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/pet-groomer-agency
claude

Replace path/to/pet-groomer-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.

The first time you run claude, it prompts you to log in. Follow the prompts — a browser opens, you sign in with your subscription account (Road A) or Console account (Road B), and you're back in the terminal. You only do this once; to switch accounts later, type /login inside Claude Code.

Step 5 — Verify the Brain Loaded

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 pet-groomer agency — you sell booking pages with reminder systems ($300–$800 setup) to local grooming studios so no-shows stop eating their slots, with review engine, rebooking campaigns, and a $79–$199/mo care plan as 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.


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.
Claude doesn't know the business Wrong folder — cd into the folder containing CLAUDE.md, run claude again.
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".
Overpass is busy — waiting 8s, then retrying Not an error either. Free shared map servers. It waits and carries on. scraper/README.md troubleshooting.
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 pet-groomer-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, the fulfillment/ booking-page template and setup guide when you close a client.

For the lead list without the scraper: the OpenStreetMap scraper needs the terminal, so in the browser either ask Claude to help you build a list by hand from Google Maps searches ("pet groomer near [your city]" — name, phone, website, and whether they have online booking, 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 — grooming lists run thin anyway, and a "call to book" button on their site is your opener.

Honest recommendation: do the 15-minute terminal setup. The whole point of this kit is that your Claude runs the machine — scraper, files, pipeline, mock-ups, deliveries. The browser fallback keeps you moving today; Claude Code is the business.

📄 docs/FIELD_TEST.md

Open file

FIELD TEST — Pet Groomer Agency Kit

Date: 2026-08-01 Tester: a fresh Claude playing the buyer's operator — no prior knowledge of this kit, working only from START_HERE.md and CLAUDE.md, exactly as a new CEO's Claude would. This is The Rule (Shelf 1): we ran it before we listed it.

What we ran

1. Day one, literally

Followed START_HERE.md step 3 as written: installed scraper/requirements.txt (clean Python 3.9 virtual env, one pip install, no errors), then ran the scraper on two real US cities:

City Command Leads No website (score 100) Phones ready
Boise, Idaho python3 leads.py --city "Boise, Idaho" 4 3 2
Mesa, Arizona python3 leads.py --city "Mesa, Arizona" --out ... 16 15 2
  • Real businesses with real addresses landed in leads.csv (e.g. a groomer on South Broadway Ave, Boise, with mapped weekday hours; a Main St studio in Mesa).
  • No API key, no account — Overpass ran keyless as promised. One run hit a busy free server; the built-in retry/backoff handled it without our help.
  • Lead counts match the kit's own honesty: grooming lists run thin (5–25 per mid-size city), which is why START_HERE.md, CLAUDE.md, and scraper/README.md all say to scrape 2–3 neighboring cities and combine. The --out flag for multi-city runs works.
  • CSV columns match scraper/README.md's column table exactly: name, phone, email, website, address, city, weak_web_score, weak_web_reasons, category, opening_hours, lat, lon, osm_type, osm_id.

2. Agency storefront build

Ran agency-site/fill.py. Against the shipped config/business.yaml it now refuses to build and names every line still holding a placeholder — that guard was added after the first field test, because the earlier version happily green-lit a Netlify deploy of a site reading "Your Name" with a fake 555 number on it. Re-run with real values filled in: built deploy/, filled all 12 tokens, zero {{TOKEN}}s left and zero placeholder strings. The output is a complete, professional single-page agency site (hero, offer, pricing from yaml, FAQ, honest-about-AI section, contact) ready for the Netlify drag-and-drop.

3. Fulfillment delivery checklist walk

Walked fulfillment/delivery-checklist.md Stage 1–7 against the actual files:

  • Every referenced file exists: calcom-setup.md (its "step 6" reference is a real Step 6), reminder-system.md, care-plan-sop.md, site-template/PLACEHOLDERS.md, site-template/photos/, prompts/03-fulfill.md, prompts/04-invoice.md, prompts/05-weekly-review.md.
  • site-template/index.html is a complete, professional booking page (365 lines): sticky nav, hero, 6-service pricing grid, gallery with graceful photo fallback, hours/map/contact, embedded Cal.com booking with a no-JS fallback link, mobile floating CTA.
  • Token audit: the 32 distinct {{TOKEN}}s in the template match PLACEHOLDERS.md one-for-one — nothing in the checklist or placeholder file references a token that isn't in the page, and vice versa. The grep '{{' verification command in the checklist works as written.
  • Photo naming (gallery-1.jpggallery-6.jpg) is consistent across the checklist, PLACEHOLDERS.md, and the template's onerror fallback.

4. Prompt pack vs. real data

Dry-ran prompts/01-prep-calls.md against the real Boise/Mesa CSVs. The lead fields it asks for (name, phone, address, website, hours) all exist in the output. Prompts 02–05 reference only files that ship in the kit or work/ files the operator creates (documented as such in CLAUDE.md).

5. Path audit

Extracted every file path referenced in CLAUDE.md, START_HERE.md, all five prompts, sales/, fulfillment/, playbook/, and docs/ — every kit-shipped path resolves to a real file on disk. No dead references.

What we fixed during the test

  1. prompts/01-prep-calls.md — the priority criteria named a "weak-web score" loosely and leaned on "rating signals" as if leads.csv carried ratings (it doesn't — OpenStreetMap has no review data). Rewritten to name the real columns (weak_web_score, weak_web_reasons, opening_hours, blank website = no online booking) and to source review knowledge from work/research/ only, with an explicit "never invent review data" rule.
  2. fulfillment/delivery-checklist.md — Stage 2's example client folder (clients/sudsy-paws/site/) didn't match the work/clients/<studio>/ convention used by CLAUDE.md and prompts/03-fulfill.md. Aligned.
  3. Removed the test-run scraper/leads.csv so the kit ships clean and the buyer's first scrape is genuinely theirs.

Known limits (honest, by design — not defects)

  • Lead volume is thin per city. 4–16 rows per city in our runs. The kit says this out loud in three places and builds the multi-city merge into day one. This is the niche, not a bug.
  • No ratings in leads.csv. OSM carries no review data; the kit routes review intel through pre-call research (prompts/02-research-lead.md) instead of pretending the scraper has it.
  • Phone coverage varies. Score-100 leads often lack a mapped phone; scraper/README.md documents the lookup workaround.
  • Not tested live: actual Netlify deploys, a real Cal.com account, and real phone calls — those need human accounts and a human voice. Everything up to those steps (files, commands, embeds, instructions) checks out.

Verdict

PASS. A fresh Claude, given only this folder, gets from zip to a ranked list of real grooming studios in well under 30 minutes, and every document it's told to lean on exists and agrees with the data.

⚠️ Corrected 2026-08-03: the original wording said "real, callable studios". It isn't callable at that point — most rows arrive with no phone number, and turning the list into a call list takes another 45–60 minutes of lookups. See the 2026-08-03 timing addendum at the bottom of this file.


Addendum — scraper v2 (2026-08-01)

Scraper upgraded to v2 (same engine as the plumber kit, groomer defaults kept). What we ran this time:

  • Multi-city merge, live and keyless: python3 leads.py --city "Boise, Idaho" --city "Mesa, Arizona" → 4 + 16 rows, 20 unique leads in one CSV — the documented answer to this niche's thin per-city coverage, now one command instead of manual CSV stitching. Full enrichment ran; 18 leads scored 100 (no website), 4 phones ready to dial.
  • Phone-lookup helper: the same run wrote lookup-list.md with all 16 hot no-phone leads (e.g. real Mesa studios with street addresses), each with a pre-built Google Maps search link and the manual + Claude-assisted lookup flow. Links verified by hand.
  • CSV schema: backward-compatible — original 14 columns unchanged and in the same order; one new column, source (osm / google / osm+google), added at the end. prompts/01-prep-calls.md updated to match.
  • Merge/dedupe engine: 26 synthetic unit checks, all passing, including pet-kit-specific checks (default niche, "pet groomers" Google query, identical merge behavior).
  • Google Places source (OPTIONAL): NOT tested live. Needs a billed API key we don't ship. Request path unit-tested against the documented API format (mocked responses); a rejected key fails safe with specific checks and the keyless fallback. The README says this to the buyer plainly.
  • No-key guard: --source google without a key exits with exact instructions — verified live.
  • All test CSVs and lookup lists were generated outside the kit folder; the kit still ships clean.

v2 — 2026-08-01

Tester: a second fresh Claude, skeptical-buyer dry run after the upsell modules (fulfillment/review-engine/, fulfillment/rebooking-campaigns/), the office/ back office, agency-site/variant-b/, and prompts/06-upsell.md landed. Same bar as v1: run what the README says, believe nothing untested.

What we ran

  1. Keyless scraper, multi-city merge, live — clean Python 3.9 venv, one pip install, then the README's exact documented command: python3 leads.py --city "Mesa, Arizona" --city "Gilbert, Arizona" → 16 + 13 rows, 27 unique merged leads in one CSV, real studios with real street addresses (Main St Mesa, Gilbert Rd). 26 scored 100 (no website), 2 phones ready, lookup-list.md written with 25 pre-built Maps links. One Overpass 429 on a later run was absorbed by the built-in retry/backoff, as designed. CSV columns match scraper/README.md's table exactly (15 columns incl. source).
  2. Both agency-site variants builtfill.py run in agency-site/ AND agency-site/variant-b/, first against the shipped business.yaml (both hard-stop on placeholder values, exit code 1, nothing written — the intended behaviour) and then against a filled one: 12 tokens filled each, zero {{TOKEN}}s left and zero placeholder strings in either deploy/. Token sets of variant A and B are identical (10), so one business.yaml genuinely drives both; variant-b's fill.py resolves ../../config/business.yaml correctly.
  3. Every new module walked against CLAUDE.md + the prompt pack:
    • office/ — tracker CSV ships header-only (clean); tracker-guide.md stages/columns match what prompts 01/03/04/05/06 and onboarding-emails.md tell Claude to write. invoice.html and proposal-onepager.html are complete printable pages; every {{TOKEN}} in both is documented in the file's own header comment, and the grep '{{' verification command works as written.
    • review-engine/ — all 6 files present, SOP → cadence → pricing → one-pager cross-references resolve; integrity rules (no fake/gated/ incentivized reviews) stated in README, flow, pricing, AND the one-pager. qr-card.html self-warns on unreplaced tokens (verified in the code).
    • rebooking-campaigns/ — all 6 files present; hard limits (max 2 messages, 1 campaign/customer/month, stop-means-stop, real customers only) repeated in every file that sends anything.
    • Bundling honesty is consistent everywhere it's stated (CLAUDE.md, both pricing.mds, prompts/06-upsell.md, care-plan-sop.md): Standard/Full care-plan clients are never sold a standalone module.
  4. Prompts 01–06 vs. the real CSV — every column prompt 01 names (weak_web_score, weak_web_reasons, opening_hours, blank website) exists in the live output; prompts 02–06 reference only kit-shipped files or documented work/ runtime files. Full path audit across every .md in the kit: zero dead references to shipped paths.
  5. No income promises — swept the whole kit; all dollar figures carry market-rate framing.

What we fixed

  1. scraper/leads.py — human barbershops leaked into the lead list. The name-keyword fallback ("groom") pulled in shop=hairdresser businesses: the Mesa+Gilbert run shipped 3 rows like "Men's Ultimate Grooming" (category hairdresser) — dead dials a buyer would burn time on. Added a per-niche exclude_categories filter (hairdresser/barber/beauty + the Google Places equivalents) applied to both OSM and Google rows. Unit-checked synthetically, then verified live: re-run on Gilbert alone dropped 4 human salons, 13 → 9 rows, 0 hairdresser categories in the CSV. Plumber niche unaffected. README's "How it works" now says this out loud.
  2. fulfillment/review-engine/sell-in-one-pager.md claimed "the scraper's rating columns tell you exactly who" — leads.csv has NO rating columns (v1 finding). Rewired to the honest sources: an incognito search or the prompts/02-research-lead.md brief.
  3. fulfillment/rebooking-campaigns/campaign-sequences.md used {{PHONE_DISPLAY}} in the window-3 email without documenting it in the header token note. Documented (it's a site-template token from the client's live build, same as {{BUSINESS_NAME}}).

Open gaps (honest, none blocking)

  • Phone coverage in this niche is thin at the source. 2 of 27 merged leads had a mapped phone; the kit's answer (lookup-list.md + verified-only dialing) is documented and worked, but day one includes ~30 min of number lookups. Known, stated, by design.
  • Still not tested live: Netlify deploys, a real Cal.com account, Google Places with a billed key, real calls/sends — same human-account limits as v1; every file, command, and embed up to those steps checks out.
  • The --source google exclusion filter is unit-tested only (no live key), same honesty note as the v1 addendum.

Verdict

PASS. The documented keyless path produced 27 real, deduped, ranked leads in one command; both storefront variants build token-clean from one config; every new module's files exist, agree with CLAUDE.md and the prompts, and keep the integrity rules loud. Three small defects found, fixed, and re-verified (one live). Test artifacts removed — the kit ships clean.


Addendum — integrity + completeness pass (2026-08-03)

A skeptical-buyer audit scored the kit 6/10 and found the day-1 path real but three steps under-documented, plus a set of honesty and completeness gaps. All of it was fixed in the kit; this is what changed and what was re-verified.

Re-verified by running it:

  • agency-site/fill.py and agency-site/variant-b/fill.py — both now hard-stop (exit 1, nothing written) on the shipped placeholder business.yaml, and both build token-clean and placeholder-clean from a filled one. Confirmed by running both, twice each.
  • config/business.yaml now ships FILL IN ... in every identity field. The old defaults (Your Name, Happy Paws Digital, Mesa, Arizona, (480) 555-0143, you@example.com) are still caught by the guard for anyone upgrading, and --i-mean-it exists for the buyer who genuinely lives in Mesa.
  • Every {{TOKEN}} in the three new office/EXAMPLE-*.html files and in fulfillment/site-template/EXAMPLE/index.html resolves — checked programmatically, zero left.
  • Full cross-file reference sweep: no path mentioned anywhere in the kit points at a file that doesn't exist.

Copy corrected for honesty (the biggest fix): the marketing surfaces sold "automatic reminders" while fulfillment/reminder-system.md delivers a manual morning text routine as layer 3. Both agency-site variants, the proposal, the invoice line item, delivery email 3, the call script, the objections, the outreach templates, the week-1 pitch and CLAUDE.md now all describe the same three layers the same way: two automatic, one a taught 2-minute morning routine.

Income claims removed: playbook/month-1.md ("even the conservative line pays for this kit"), playbook/growth.md (the 10-care-plan monthly figure, the "say exactly that in the pitch" payback line, the months 4–6 profitability shape), sales/call-script.md ("it usually pays for itself"), CLAUDE.md ("this folder only makes money when…"), and START_HERE.md ("you just bought a business" → starter-system framing).

New assets, all complete and usable, none stubs: office/service-agreement.md + .html, office/intake-form.html, office/quoting-guide.md, fulfillment/publish-a-demo.md, fulfillment/site-template/EXAMPLE/ (filled reference build + 6 placeholder gallery images), docs/setting-up-shop.md, docs/rules-of-the-road.md, docs/see-it-first.md, and three filled office/EXAMPLE-*.html references.

Open gaps, updated:

  • The phone-lookup step is no longer buried. It is now step 3b of START_HERE.md with the real hit rate (15 leads → 2 phones), step 4 of week-1 Day 1, and a "say it before the run" instruction in CLAUDE.md. Day 1's time estimate moved from "about 2 hours" to "2–3 hours" to match reality.
  • Still not tested live (unchanged from v2): Netlify deploys, a real Cal.com account, Google Places with a billed key, real calls and sends.
  • Cal.com Workflows availability could not be confirmed from their pricing page (JavaScript-rendered). The kit now treats it as check, don't assume: a YES/NO branch in delivery-checklist Stages 3 and 5, a matching conditional in delivery email 3, and an explicit "don't buy an upgrade" instruction.

Addendum — timed run + honesty pass (2026-08-03, second round)

A fresh skeptic scored the kit 6.5/10. The single worst finding (score-100 documented as "no website at all") was fixed by hand earlier the same day. This addendum covers everything else, and the numbers below come from running the scraper, timed, on the day — not from memory.

The timed runs (all keyless OSM, one machine, one afternoon)

⚠️ These three rows are CITY-LIMITS ONLY — the metro widener was off (--no-widen). They are the floor: what the bare map holds inside a city boundary. The default command widens automatically when a city comes back thin on dialable leads, and it returns considerably more (see the re-run below). scraper/README.md prints both tables side by side, labelled the same way.

Command (--no-widen, city limits only) Overpass retries Leads With a phone Lookups needed Wall time
--city "Mesa, Arizona" 1 15 2 13 1m 28s
Mesa + Gilbert + Chandler + Tempe 3 35 2 33 3m 17s
+ Scottsdale + Glendale + Phoenix (7) 4 60 8 52 6m 48s

Per-city spread in that metro: Glendale 2, Tempe 3, Chandler 8, Scottsdale 8, Gilbert 9, Mesa 15, Phoenix 15.

What that killed:

  • "First scrape (~5 min)" in START_HERE.md — meaningless as a single number. One city ran in 88 seconds; the same command on a busy afternoon can take 5–10 minutes because each Overpass is busy retry adds 8–30 seconds. Replaced with the real range plus an explanation of what makes it vary.
  • "50+ combined leads" from "2–3 neighboring cities" in playbook/week-1.md and START_HERE.md — wrong by a factor of two. Four cities gave 35. It took seven cities — the entire Phoenix metro — to reach 60 rows. Every place that said 2–3 cities now says 5–7, with the table above as the evidence.
  • "roughly 40 lookups, 30–45 minutes" — the 50+ pot left 52 leads with no phone, and "about 30 seconds each" is a best case, not an average. Rewritten as ~1 minute each, 45–60 minutes by hand, with the Claude-assisted path pushed as the default rather than the alternative.
  • Day-1 totalSTART_HERE.md said "about 1.5–2 hours end to end" while playbook/week-1.md Day 1 said "2–3 hours". They now both say 2–3 hours.
  • Week-1 Day 1's "Done when" was a row count (50+). Rows are not calls: the seven-city run produced 60 rows and 8 dialable numbers. The bar is now 25 verified phone numbers, with an explicit "if your area is thin, that's a real answer, not a failure" branch.

New: docs/what-you-will-see.md

A clean, successful run prints two things that look like errors to a non-technical buyer, and the kit did not mention either one — grepping the whole kit for OpenSSL / LibreSSL / urllib3 / pip / warning returned nothing:

NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl'
module is compiled with 'LibreSSL 2.8.3'.
WARNING: You are using pip version 21.2.4; however, version 26.0.1 is available.

Both were reproduced on this machine on every run. scraper/README.md's troubleshooting table covered hard failures only, so it opened with an "it worked but printed scary text" section, and a new docs/what-you-will-see.md walks the whole first hour with real captured output — the pip install, the healthy one-city run, the seven-city run, the City not found error, and the Overpass is busy retries — with a good / bad / noise verdict on every line. It's wired into START_HERE.md step 3, playbook/week-1.md Day 1, scraper/README.md and the CLAUDE.md file map.

Same-class defect sweep (the kit stating what the data doesn't support)

Both bugs shipped in this kit had the same shape, so the rest were hunted:

  • Both agency-site variants drew the 24-hour reminder as a text message with "Reply C to confirm or R to reschedule". The delivered layer 2 is an email with no reply handling — the exact thing seven flat "automatic reminder" promises were removed for earlier the same day. Variant A's bubble is now labelled as the morning-of text from the studio's own number (which is genuinely a text); variant B's three-card journey is now the three real layers: confirmation email → 24h email → morning-of text you send.
  • Both variants promised "We connect it to your website, Google profile, and Instagram bio." Nothing in fulfillment/delivery-checklist.md did that, and the service agreement excludes social media work. Copy corrected to "we show you exactly where to put the link", and a matching Stage 6 handoff step added so the sentence is true.
  • Variant A's browser mock showed book.yourstudio.com; delivery is a free .netlify.app address unless the client already owns a domain. Changed.
  • Variant B's card list said "24-hour reminder messages" where variant A said "email reminder" — aligned on email.
  • fulfillment/delivery-checklist.md claimed a delivery takes "an afternoon, ~2 hours by client three". Its own stage estimates sum to ~2 hours; that's now stated as the floor once you know the flow, not a learning curve.

Invented social proof (we have never signed a client)

Every confident claim about an outcome nobody has observed was reframed:

Where Was Now
playbook/growth.md the monthly report "is why nobody cancels" it's the retention lever; "we can't quote you a churn number — this kit has never run a book of care-plan clients"
fulfillment/care-plan-sop.md "A client who gets a clear report with a falling no-show number does not cancel" which side of churn you control, with the same disclaimer
fulfillment/care-plan-sop.md "I keep the no-shows dead, keep your reviews growing" as the pitch line describes the monthly work, plus an explicit ⚠️ that the old line was a guardrail-6 violation
fulfillment/care-plan-sop.md "Most owners stop doing it by week three" posed as the honest question, not a statistic
docs/see-it-first.md "clients love it"; "you will never have a refund conversation about it" no invented client feedback; "can't come as a nasty surprise later"
playbook/month-1.md "warm leads close at multiples of cold-call rates" log referral leads separately and find out
playbook/month-1.md "Pet parents pick groomers off Google reviews, and most studios never ask" reviews are one of the first things compared; check their profile before pitching
playbook/week-1.md + CLAUDE.md "I already built yours" is "the single biggest close-rate lever in the kit" it changes what the second call is — flagged as design reasoning, not a measured rate
sales/call-script.md "Honesty closes more deals here than it loses" dodging is the answer that definitely costs the call
fulfillment/reminder-system.md a text from their number "converts better" it's a number the customer can reply to
office/quoting-guide.md a fast $300 job "gets you a referral and a review" it's the job worth asking for both on
review-engine/sell-in-one-pager.md "they pick the studio with the most recent reviews" your reviews sit next to everyone else's; the gap is about who asks

Client-facing results claims (guardrail 6, both directions)

  • sales/call-script.md's stated rule was "Reminder systems cut no-shows" is fair — that is a results claim nobody here has measured. The rule now draws the line at what the system does ("every appointment gets a reminder before it happens") versus what it does to their business.
  • The pitch, the objection-6 answer, the SMS #1 template, and week-1's Day 4 script all ended on "that's where the no-shows drop" / "people show up". All four now describe the mechanism instead.
  • playbook/week-1.md said a high weak-web score means they "almost certainly have no online booking either", and that every phone/DM-booking studio on the list "is bleeding no-shows". The list can't know either thing — that's what discovery question 1 is for.
  • Both agency sites and both proposals asserted "most no-shows aren't rude clients, they're busy people nobody reminded". Reframed to the honest version: a reminder can't fix someone who never meant to come; it can fix the one who forgot.
  • scraper/README.md said a blank website column is "usually because they don't have one" and score 100 "most of the time" means no site. Nobody measured the split; both now say so.

Verified by running

  • Scraper: 4 real runs (1 city, 4 cities, 7 cities, one deliberate bad city), timings and counts as tabled above.
  • python3 -m pip install -r requirements.txt re-run to capture the real pip and OpenSSL warning text quoted in the new doc.
  • Both prior fixes re-grepped and confirmed clean: no "no website at all" phrasing survives anywhere, and no unqualified "automatic reminder" promise survives in sales/, office/, playbook/ or either agency site.

Still not tested live (unchanged)

Netlify deploys, a real Cal.com account, Google Places with a billed key, real calls and sends. Everything up to those steps runs.


v3 — parity levelling, 2026-08-03

A parity audit compared all three kits side by side. This was the weakest kit in absolute terms. The blockers found in that audit — no keyless rescue path, no Python install step, no LICENSE, no shipped do-not-call ledger, a scraper that hard-exited mid-run — were closed first. This entry covers the second pass.

What was added

Added Why
fulfillment/CLAIMS.md The booking page made eight promises about the client's studio and none of them were gated. "A booked slot is a guaranteed slot" was hardcoded, as was "we remind you before every appointment" — the one that collides with the manual morning-text layer
Claim tokens + CLAIM-BLOCKs in site-template/index.html Three {{CLAIM_*}} tokens and three fenced tiles, so an unconfirmed promise fails a grep instead of going live
README.md (root) A folder map for the human. The only complete map lived inside CLAUDE.md, which is written for the AI
fulfillment/review-engine/make_qr.py + requirements.txt The printable QR card shipped; the generator that fills it didn't, so the CEO had to find a third-party QR site themselves
office/README.md — the three new paperwork files, the money-order diagram, the three-documents-three-moments table, the cancellation note The kit gained a recurring agreement, a data-handling addendum and a do-not-call ledger, and the paperwork map never mentioned them
docs/rules-of-the-road.md — data-collection, ten-minute compliance setup, when-to-pay-a-lawyer The thinnest legal layer of the three, and it had nothing about the client customer lists the rebooking module moves onto the CEO's laptop
docs/setting-up-shop.md — getting paid + the 1099-K, bookkeeping, your business address Three sections the other two kits had and this one didn't

The one live house-rule violation is gone

fulfillment/rebooking-campaigns/run-a-campaign.md:33 said lapsed regulars are "the biggest untouched pool, because nobody has ever nudged them" — a stated fact about a client's own customers that nobody verified. Rewritten to state the reasoning instead of the result, matching the fix pattern already logged at line 392 of this file.

The EXAMPLE build now demonstrates the gate

site-template/EXAMPLE/ was rebuilt from a pretend set of written intake answers that includes a No:

  • "Nervous pets welcome" came back "not blanket — anxious dogs get a meet-and-greet first, and I've turned two away." The whole tile was deleted. The strip reflows to two tiles and looks completely normal.
  • "A booked slot is a guaranteed slot" came back no — she does occasionally have to move people. Softened to "booking ahead is the safest way to get the time you want."
  • The "every visit ends with a happy pet" outcome promise was replaced with something true about the service.

Its README now shows the whole table: which claims were confirmed, which came back weaker, and what happened to the page.

Verified

  • site-template/EXAMPLE/: zero unfilled tokens, zero surviving fences, the guarantee phrasing gone, two tiles remaining in the strip.
  • make_qr.py parses; qr-card.html and review-flow.md both updated to mention the local-generation path.
  • Guardrail numbering: the claims rule was added as guardrail 9, not inserted at 4, specifically so the existing cross-references to guardrails 4/5/6 in sales/, playbook/, office/ and fulfillment/ stay correct.
  • No income claims and no invented social proof remain — swept by grep across the whole kit.

Still not tested live (unchanged)

Netlify deploys, a real Cal.com account, Google Places with a billed key, real calls and sends. Everything up to those steps runs.


v4 — independent QA pass, 2026-08-03 (evening)

An independent QA hunter ran the kit end to end against three cities the docs never mention and filed 19 code findings plus a set of cross-kit consistency findings. This entry covers what was fixed, and — because the fixes changed the scraper — what was re-run to prove it.

The three that would have cost a refund

Finding What actually happened Fix
Every rerun destroyed the buyer's phone-lookup work The kit budgets 45–60 minutes for phone lookups, tells the buyer to paste the numbers into leads.csv and onto lookup-list.md, then tells them to rerun monthly and says "nothing found is lost". open(args.out, "w") made that sentence false about the only thing in the folder they typed themselves. Reproduced in thirty seconds. A run now reads the existing CSV and the Phone found: lines of the existing lookup list before writing, and carries every phone/email forward. Rows that carried the buyer's work and didn't come back cause the old file to be renamed leads-YYYY-MM-DD-HHMM.csv, announced on stdout. A lookup list with numbers written on it is renamed, never deleted.
dedupe() ignored the city Three real studios called "Paws & Suds" in Tulsa, Broken Arrow and Owasso collapsed into one row, silently — while scraper/README.md promised the opposite using that literal example. Hand-built lists were hit hardest, because a hand row is often just a name. City is in the dedupe key. Cross-city identity is merge_leads()' job, which has the phone match, the same-city name match and the 2-mile same-place check to do it properly.
A live site behind a WAF scored 85 = "site listed but unreachable" fetch_site() turned every status ≥400 into one error string, so a Cloudflare/Wordfence 403 — which small-business WordPress sites return to scripts all day — was handed to the buyer as "their link is broken and they may not know". That is the kit generating an unverified fact for a cold-call opener. Failures are classified. 403/429/503 → score 40, "site is up but blocked our check — LOOK AT IT YOURSELF before you mention it". Timeout → 45, "could be them, could be your connection". Only DNS failure / refused connection → 85, and it now says "nothing answered at that address". scraper/README.md's score table gained the 40–45 row.

The rest of the code findings

  • --out results/leads.csv into a folder that didn't exist threw a raw traceback after the scrape and wrote nothing. --out is now validated before the first network call: the folder is created, or the run exits with a plain-English message naming it.
  • --source google with a bad key hard-exited and threw away the OSM leads already in memory, while printing "Meanwhile the keyless default still works". The page-0 failure is non-fatal now: same diagnostic, then the run continues and writes the CSV, with the Google caveat repeated at the end.
  • Hand-list counts didn't add up. "Read 9" then "7 added, 1 merged" — one row eaten by an unreported dedupe. The line now reads "Read N row(s) (X with no name skipped, Y identical to another row in the same city) → K kept."
  • --no-enrich left weak_web_score blank for every lead with a website, and blank sorted where the docs say "site is basically fine". It writes not checked now, sorts with the hot rows, and the run says so.
  • A church shipped as a hot lead. "groom" matched inside "Bride-groom" in a real Philadelphia run. Overpass's regex engine has no word boundary, so the strict \bgroom check is applied on our side after the map answers: rows with no niche category and no word-boundary match are dropped, and rows that qualified only by name are flagged name match only — verify it's a grooming studio instead of being sold as call-first leads. place_of_worship, school, restaurant, cafe and friends joined the delete list.
  • Single-quoted <meta name='viewport'> scored "not mobile-friendly" — a line the docs tell the buyer to say word for word. Both that check and the <title> check are regex now.
  • CSV formula injection. A studio named =cmd|'/C calc'!A0 — or, far more likely, +Paws — went into leads.csv verbatim, and the kit tells the buyer to open that file in Excel. Any non-numeric field starting with = + - @ tab or CR is written with a leading apostrophe.
  • The User-Agent lied to the free Overpass servers. It said "one query per city"; a city can cost four queries and roughly ten POSTs. It now says "up to 4 queries per city incl. metro widening; backs off on 429/504", and scraper/README.md says the same thing in all three places it used to contradict itself.
  • Progress lines said "the map data" for hand-typed rows. They now say "on your list" / "on file" / "in the map data" by source — including inside lookup-list.md and the weak_web_reasons text — because blurring that distinction is exactly what the whole no-website rule exists to prevent. Plural bugs in the same block ("1 businesses", "1 look like") fixed.

Re-run to prove it (2026-08-03, after every change above)

Command Result Wall time
--city "Mesa, Arizona" (default, widener on) 15 inside city limits / 2 with a phone → 48 rows / 6 dialable / 42 lookups 3m 55s and 4m 34s on two separate runs
--city "Mesa" --city "Gilbert" --city "Chandler" --city "Tempe" per-city 15 / 9 / 8 / 3 inside the limits → 56 rows / 8 dialable / 48 lookups 12m 57s
--city "Ely, Nevada" --source google --api-key BOGUS --no-widen --no-enrich Google rejected the key (HTTP 400), run continued, CSV written with the 1 OSM lead 1m 10s
--merge-csv rerun with numbers pasted into the CSV and the lookup list both carried into the new run; gone? correctly not carried; old lookup list saved with a timestamp instant

⚠️ Wall times are pessimistic. Three other agents were running scrapers against Overpass from this machine at the same time, so a good share of those "Overpass is busy" waits are self-inflicted. The row counts are not affected.

The two documented tables now say which is which: the timed table above is --no-widen (city limits only); the widener table in scraper/README.md is the default command. docs/what-you-will-see.md's four-city transcript carried a note saying its counts were "superseded" by 35/2 — that was comparing a widened run against the city-limits floor. Corrected, with today's re-run.

Honesty findings fixed (these matter more than the crashes)

  • office/onboarding-emails.md"Most owners stop by week three" in a client-facing email. A statistic nobody measured, in a kit that has never signed a client. The same sentence had already been removed from care-plan-sop.md; this was the copy the grep missed. Now the honest question that was agreed there: "whether you'll still be doing it in month three when you're grooming all day."
  • fulfillment/review-engine/README.md"the studio with 80 recent reviews beats the studio with 9 old ones, every time" and "Most groomers know this". Two invented facts, one of them absolute. Replaced with the mechanism already approved in the sibling one-pager, plus an explicit "nobody here has measured what that does to their ranking".
  • fulfillment/site-template/index.html — the booking band promised "a confirmation right away and a reminder before your visit" hardcoded, outside every fence, while CLAIMS.md said "None of the claims is hardcoded". A buyer could delete the reminder tile, pass both pre-launch greps, and still publish the kit's #1 refund risk. It is {{CLAIM_BOOKING_CONFIRMATION}} now — claim 4 lives in two places and CLAIMS.md says so, the token count went 3 → 4, and the sentence in that file is true again.
  • prompts/01-prep-calls.md — step 5 taught "blank website almost always means phone-only or DM-only booking", six lines above the rule that bans exactly that inference, and step 6 told the operator to record "none". Both fixed: a blank website column is a ranking signal, never a statement, and it is recorded as not in map data.
  • playbook/month-1.md"some agencies quote $1,000+ for the full stack", unsourced and untagged. Cut; your evidence is your own last ten jobs.

Other repairs

  • agency-site/fill.py (and variant B): --i-mean-it used to build and publish a site full of literal "FILL IN YOUR NAME", including in the <title> and an empty tel: link, then print "All tokens filled." The flag now only covers the shipped example values (Mesa, the 555 number, "Happy Paws Digital"); anything still saying FILL IN is fatal with or without it, and an empty derived token like PHONE_LINK blocks the success message.
  • fulfillment/review-engine/make_qr.py: the default --out qr.png wrote into the shipped kit folder and silently overwrote the previous client's QR code. It refuses to overwrite without --force (printing the existing file's timestamp), and the default name is derived from the review link so two clients cannot collide. qr-card.html and review-flow.md now show the per-client folder command.
  • docs/what-you-will-see.md §7 quoted a "Missing dependency / Fix:" block the code has never printed. Replaced with the real 20-line message, reproduced by blocking the requests import.
  • fulfillment/site-template/photos/README.md contradicted itself about missing gallery photos ("a paw-print placeholder shows" vs "the missing tiles quietly disappear"). The template does the first; the second is gone, with the instruction for anyone who genuinely wants a smaller grid.
  • agency-site/EXAMPLE/a/ and /b/ now exist — both designs built for real by the kit's own fill.py from a throwaway demo config (Desert Paws Digital, the same made-up agency as the office/EXAMPLE-* paperwork). see-it-first.md used to promise "a finished, filled-in example" and then send the buyer to the raw {{TOKEN}} masters. It points at the built pages now, and its title says nine files because it lists nine.
  • office/intake-form.html gained section 5, "The promises — I only publish these if you say yes" — one tick box per CLAIMS.md row, claim 4 split into its two required yeses. CLAIMS.md demanded a written yes and the form had no way to collect one.
  • office/EXAMPLE-call-list.md and office/EXAMPLE-client-tracker.csv added — the two filled reference copies this kit was missing.
  • config/business.yaml gained the module prices (care_plan_default, review_engine_monthly, review_engine_setup_low/high, rebooking_monthly, rebooking_oneoff_low/high). recurring-services-agreement.html/.md pointed at five keys that did not exist (care_low, care_default, care_high, review_engine_monthly, rebooking_monthly); an operator following that header found nothing and would have had to invent a number on the one document that gates a recurring card charge.
  • One dial ramp, everywhere. The kit stated four different week-one numbers ("100 dials", the 12/15/18/20/20 ramp that sums to 85, "25 dials", and a playbook that scheduled 50). The carrier-safety ramp wins, because it is the one with a consequence attached: 12/15/18/20/20 across days 2–6, 85 dials, now in README.md, CLAUDE.md, START_HERE.md, docs/your-phone-setup.md and playbook/week-1.md including its scoreboard.
  • sales/outreach.md carries A/B/C evidence versions on every message now, not just the voicemail and SMS #1.

Still not tested live (unchanged)

Netlify deploys, a real Cal.com account, Google Places with a billed key, real calls and sends. Everything up to those steps runs.


PASS 3 — cold buyer run + repair, 2026-08-04

What happened. A fresh Claude was dropped into the delivered zip of this kit with no other context and told to run the business for a non-technical owner. It filled the config, scraped a real city, worked the rescue path, built a call list, built a client booking page and the storefront, and reached a first-client path. Verdict: yes, it could operate the business. It also found two harms the owner would never have seen, and eighteen places where it had to guess. This section records what was repaired and — the part that matters more — exactly what was checked, so nobody reads a clean bill of health that was never earned.

Standing rule this section exists to enforce (SHELF_GATE G14): an entry in this file may only say a class of defect is fixed if it names the grep that was run and the files that were read. Two earlier entries in this document claimed complete sweeps while identically-shaped survivors sat in files nobody opened. That is worse than a known bug, because it stops anyone looking again.

The two harms

1. weak_web_reasons was authorised as verified pitch material, and it lies

Three files (prompts/01-prep-calls.md, scraper/README.md, CLAUDE.md) told the operator that column was quotable word for word. It is one machine's guess from one automated fetch. score_weak_web() regexed the raw HTML, so anything inside a <script>, <style> or comment counted as page content.

Reproduced on a real business, 2026-08-04. happyhoundslex.com, a real Lexington KY grooming studio, embeds an Indian Type Foundry font licence inside a stylesheet. Old code vs new code, same bytes, same afternoon:

Before After Ground truth
weak_web_reasons copyright frozen at 2014 copyright frozen at 2020 visible footer: Copyright © 2020 Happy Hounds Grooming, LLC
harvested email info@indiantypefoundry.com (none) the studio publishes no email

swagpets.net hid Copyright (c) 2015 Daniel Eden (the animate.css licence header) the same way. A second real studio, lexingtongrooming.com, published hello@youremail.com — an unedited template address — which the old code returned as their business email.

Fixed: strip_noise() removes <script>, <style>, <noscript>, <svg> and comment blocks before the copyright, <font, and email detectors; the copyright reader prefers the <footer> and treats © 2015-2026 as 2026; EMAIL_JUNK now covers template addresses and type-foundry domains; the mailto: branch runs through that filter too (it didn't). Also fixed, and this is the real repair: the three files no longer authorise the column as a quote. Evidence A in prompts/01-prep-calls.md now requires a page somebody actually opened, and every file says "open the page, confirm it, then say it."

Verified by: scraper/self-test.py, 26 checks, no network — the first five are this exact failure. Plus a live fetch of all three studios above with the old and new scorers side by side.

2. The CLAIMS pre-launch gate could never come back clean

CLAIMS.md said run grep -rn "{{" . and grep -rn "CLAIM-BLOCK" . from the client folder and "both must print nothing." Stage 2 of the delivery checklist tells you to copy site-template/ into that folder — and it contains PLACEHOLDERS.md, which is 36 rows of {{TOKEN}} examples. On a genuinely clean build the greps printed 28 and 2. A weaker model either waves the gate through (and never trusts it again), or starts deleting lines out of index.html chasing hits that aren't there.

Fixed both ways, so all three files now say the same thing: the gate greps target index.html; and delivery-checklist.md stage 2 gained "delete PLACEHOLDERS.md and photos/README.md from the client copy", after which the folder-wide grep is clean too and is kept as a final sweep.

Claims that were still hardcoded in the client template

CLAIMS.md said "None of the claims is hardcoded." It was not true:

Was Now
<h1>Happy pets. Fresh cuts. <em>Zero phone tag.</em></h1> {{HERO_HEADLINE}} — claim 1 (outcome promise)
<div class="hero-note">Online booking takes under a minute…</div> {{HERO_NOTE}} — claim 3, second home
<h2>Recent happy clients</h2> {{GALLERY_HEADING}} — read as "happy clients" over six empty paw tiles on a no-photos build
build note at index.html:387 containing a live {{CLAIM_BOOKING_CONFIRMATION}} token rewritten so no token appears in it; a find-replace was writing the claim sentence into the shipped comment, and neither grep caught it

The token count in CLAIMS.md went 4 → 7 and the sentence in that file is true again. Grep 4 (read-every-promise) was widened with book online|any time|under a minute, because grep 4 is what found all three.

Verified by: grep -n '{{' , grep -n 'CLAIM-BLOCK' and grep -n 'build note\|CLAIMS-CHECK\|is claim 4 again' against fulfillment/site-template/index.html, plus a full read of every visible sentence in that file.

Lead engine — what changed and what was measured

Two fresh metros, cold, keyless, nobody in these docs had used either:

Run Rows Dialable Wall
Knoxville, Tennessee, 1 city 5 3 2m 25s
Knoxville + 6 metro towns 7 3 12m 50s
Knoxville + 5 hand-typed rows 12 8
Lexington, Kentucky, 1 city 1 1 ~4m, after 9m of "servers busy"
Lexington + 6 hand-typed rows (names + websites only, no phones) 6 3 14s

Six extra Knoxville towns bought two extra rows and zero extra phone numbers, for thirteen minutes. The docs now say that plainly, and --merge-csv moved from "Fix 3" to step 3a of day one. The Lexington run is the honest worst case on wall time: seven retries across three mirrors returned nothing at all and the tool exited with "wait five minutes"; nine minutes later the same command worked.

Engine repairs, each verified by a real run whose output is quoted in docs/what-you-will-see.md sections 3 and 3b:

  • takes_online_bookings + booking_evidence columns. The kit's own README said "one thing the score can't see: whether they take online bookings… no booking link = your opener" — and then didn't look. Two Knoxville leads both scored 0 / "site looks OK": one had no online booking at all (the best lead on the list), one already ran a live Wix booking page (the pitch doesn't apply). Now detected while the homepage is already in memory, and the ranking puts no first and yes last. In the Lexington merge run it correctly separated three independents (no) from Dogtopia and a vet salon (yes). It counts links only — a <script> config blob containing booking.cal.api.domain and a "Book now" button that dials a phone are both reported as no. Both are in the self-test.
  • phone_source column. OSM phones counted toward DIALABLE NOW while the kit banned dialling unverified numbers. They still count — they are real numbers — but the column says where each came from and the verdict box prints "N dialable numbers came from MAP DATA… glance at the listing first."
  • --merge-csv dedupe. A hand-typed row and its metro-widener twin ("Knoxville, Tennessee area") survived as two rows, so the buyer got a task to look up a number they typed themselves. City labels are now normalised and bare typed rows match on town.
  • Score stability. A blocked/timed-out fetch (40/45) no longer overwrites a real measurement from a previous run, so a Monday run and a Tuesday run stop producing two different call orders for unchanged websites.
  • Rerun safety. A narrower rerun used to delete leads with no backup — the protection only covered rows carrying typed work. Now any row that disappears triggers the dated backup, a count, and the exact --merge-csv leads-<date>.csv command to fold them back. Verified by running it and pasting the printed command back in: all six rows returned.
  • The fix list remembers. A run that proves the map is a desert, or a folder that has already scraped 4+ towns, permanently drops "add neighbouring cities" from the fix list (.leads-run-notes.json). It used to re-recommend seven towns of work the buyer had already done.
  • Redirected output is live. python3 leads.py … > run.log block-buffered the whole run, so the log sat empty and read as a crash. Verified fixed: the log holds four lines six seconds in.
  • Every printed command is pasteable. Merge-only runs printed literal --city "<Your City, State>"; the county fix printed <State>. Both now fill from the data. The self-test greps the source for <State>.

What was NOT checked in this pass

Stated plainly, because this section's whole purpose is to stop a false clean bill of health:

  • No phone call has ever been made with this kit. Every sentence in sales/ is reasoning, not observation.
  • No client has ever been signed, delivered to, or invoiced.
  • Google Places (--source google) is still untested against a live billed key. It is documented as the one path we could not field-test.
  • Cal.com Workflows availability was not tested on a real free-tier account — which is exactly why CLAIMS.md now has a written branch for "Workflows is walled" instead of leaving the operator to invent one.
  • Netlify deploys, Formspree, and the intake form against a real inbox are unchanged and still untested.
  • The variant-b/ storefront and this pass's storefront edits were checked by reading the built HTML, not by a human looking at a browser.

📄 scraper/README.md

Open file

Lead Scraper — find every pet groomer in your city

This is your lead machine. One command pulls every pet grooming business 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. No card. Free data.

Read this first — the honest version of what day one looks like

Free map data is wonderful and uneven, and grooming is a thinly-mapped niche. Some cities hand you thirty studios. Some hand you three. That is not a bug in this script, it is the world: OpenStreetMap is drawn by volunteers, and some places have an active mapping community and some don't.

So here is the real shape of day one, and the kit is built around it:

Day 1 is building the list. Day 2 is calling it.

A single scrape of a single city is a start, not a finished call list. What you're aiming for is about 25 leads with phone numbers — one solid morning of dialling once you allow for no-answers and wrong numbers. Some CEOs get there in one command. Most need to add three or four neighbouring towns and spend half an hour on the phone-lookup list. Both are normal and both end in the same place.

The number to watch is DIALABLE NOW, which the script prints in a box at the end. It counts rows that have a phone number and look like an actual independent studio. Row count is not a call list. Ignore it.

And there is no dead end here: if the map is empty where you live, fix 3 below builds your list by hand and merges it into the same pipeline — free, no key, no card, works in every city on Earth. Nothing on the day-one path requires a credit card.

Before your first run — you need Python

The scraper is a Python program. If you haven't installed Python yet, do ../docs/connect-claude.md, Step 2.5 first — it's five minutes and it is the single most common place people get stuck.

Windows: everywhere this file says python3, you type py -3 instead. So py -3 leads.py --city "Mesa, Arizona". If you type python3 on Windows you'll get the Microsoft Store opening at you, which is not Python.

Run it

cd scraper
python3 -m pip install -r requirements.txt   # first time only
python3 leads.py --city "Mesa, Arizona"

That's it. You get leads.csv in this folder, sorted hottest-first — plus lookup-list.md if any hot leads are missing a phone number (see below).

Always include the state in --city. "Mesa" alone could match five places on Earth. "Mesa, Arizona" matches one.

Or just ask your Claude: "run the lead scraper for my city and tell me how many dialable leads I got." It reads your city out of config/business.yaml, runs the command, reads the verdict, and tells you what to do next. That's the intended path — the commands are here so nothing is a black box.

Options

Flag What it does Default
--city "City, State" Where to scrape. Repeat it to merge cities: --city "A" --city "B"
--merge-csv FILE The free rescue path. Folds a list you built by hand into the run — no key, no card. Only a name column is required. Start from hand-list-template.csv. Repeat for several files; works with or without --city
--niche pet_groomer Business type to hunt pet_groomer
--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. A folder that doesn't exist yet is created for you, and the path is checked before the scrape starts, so a typo can never cost you a finished run leads.csv
--no-enrich Skip visiting lead websites (faster run). Leads with a website then get weak_web_score = not checked instead of a number, and no phone/email is pulled off their homepage. Unknown, not fine. off
--timeout 12 Seconds to wait per website check 12
--radius-miles 15 How far around the city centre to widen when a city comes back thin 15
--widen-below 25 Widen to the metro area when a city returns fewer than this many dialable leads 25
--no-widen Never widen past the city boundary off

Examples:

python3 leads.py --city "Tulsa, Oklahoma"
python3 leads.py --city "Mesa, Arizona" --city "Gilbert, Arizona"   # two cities, one list
python3 leads.py --city "Boise, Idaho" --out boise-leads.csv
python3 leads.py --city "Gilbert, Arizona" --no-enrich   # quick pull, no site checks

The engine also knows --niche plumber — same machine, different prey — but this kit is built around groomers, so leave the default alone unless you're experimenting.

When your city runs thin — four fixes, in order

Gate on dialable leads, not rows. If the run gives you fewer than about 25 leads with phone numbers, you don't have a morning of calling yet. Work down this list until you do. Fixes 1–3 are free and need no card.

Fix 1 (automatic) — the metro widener

City boundaries are political lines. Plenty of the studios that serve your city are mapped in the suburb next door, ten minutes up the road, and perfectly callable.

So when a city's own boundary comes back with fewer than 25 dialable leads, the scraper automatically searches about 15 miles around the city centre and merges the results. Note what triggers it: dialable leads, not rows. A city can hand back 15 studios with 2 phone numbers between them — that's a thin city wearing a fat row count, and you can't ring a row.

You don't do anything. You just see this:

      Got 15 unique named businesses inside the city limits (2 of them with a phone number — that's the number that matters).
      Only 2 of those are dialable, so widening to ~15 miles around the centre. This is the slow part of the run — give it a minute or two.
      +32 more from the surrounding area (marked "Mesa, Arizona area" in the city column) — 6 dialable now.
      Still short — one last, slower sweep that also matches businesses by name...
      +1 more from the surrounding area (marked "Mesa, Arizona area" in the city column) — 6 dialable now.

That is a real Mesa run, timed 2026-08-03: 15 rows / 2 dialable inside the city limits became 48 rows / 6 dialable once the metro was included — 3x the actual phone calls, from a step you didn't have to know about. It still isn't a full morning, which is why fixes 2 and 3 exist.

Those extra leads get city values ending in "area". That matters on the phone: you say "I'm local here in the Mesa area", not "here in Mesa", to a studio that's actually in Gilbert. Your Claude knows this rule.

Turn it off with --no-widen, or change the distance with --radius-miles 35.

Fix 2 — name the neighbouring towns yourself

Grooming shops are thinner on the map than, say, plumbers — in our timed runs a mid-size city gave 2 to 15 rows (Glendale 2, Tempe 3, Mesa 15). That's the niche, not a bug, and the fix is built in: pass --city more than once and you get ONE leads.csv, merged and deduped. Use plenty of cities:

python3 leads.py --city "Mesa, Arizona" --city "Gilbert, Arizona" --city "Chandler, Arizona" \
                 --city "Tempe, Arizona" --city "Scottsdale, Arizona" --city "Phoenix, Arizona"
  • Each lead keeps its own city value, so you always know where it came from.
  • The same studio appearing in two city pulls (same phone number) becomes one row, not two. Same-name studios in different cities are kept separate on purpose — "Paws & Suds" in two towns is usually two shops. Field-tested, timed, all keyless, same machine, same day. These runs were made with the metro widener switched off — city limits only — so they show you what the bare map holds:
Command (--no-widen) Leads With a phone Wall time
--city "Mesa, Arizona" 15 2 1m 28s
Mesa + Gilbert + Chandler + Tempe 35 2 3m 17s
…+ Scottsdale + Glendale + Phoenix 60 8 6m 48s
Knoxville, Tennessee (2026-08-04) 5 3 2m 25s
Knoxville + 6 metro towns 7 3 12m 50s
Lexington, Kentucky (2026-08-04) 1 1 ~4m, after 9m of "servers busy"

Two metros, two days. The wall-time range is 90 seconds to "come back later" — on 2026-08-04 a Lexington run could not get an answer at all across seven retries on three mirrors, exited with the "servers are busy, wait five minutes" message, and worked on the retry nine minutes later. That is the free map being busy, not a broken tool. And read the dialable column: six extra Knoxville towns bought zero extra phone numbers for thirteen minutes.

And the same commands with the widener on, which is now the default (field-tested 2026-08-03):

Command (widener on, the default) Leads Dialable Wall time
--city "Mesa, Arizona" 48 6 ~4 min
Mesa + Gilbert + Chandler + Tempe 56 8 ~13 min

Same map, same commands, 3–4x the phone calls. Note the wall time honestly: the widener runs up to two extra map queries per city (four in total, counting the boundary query and its lighter retry), and the free servers made us wait on nearly every one of them (Overpass is busy — waiting 45s is in that log). A four-city run is a "start it and go make coffee" job, not a thirty-second one. Add --no-widen if you want the fast, thin version.

Read the middle column. Rows are not phone calls — see the phone section below. And note how many cities it took: seven, the entire Phoenix metro, to clear 50 rows. Plan on 5–7 cities, not 2–3. Each city costs up to four map queries (boundary, its lighter retry, and the widener's two sweeps) with a pause between each, so a 7-city run is still well-behaved; it just takes 5–15 minutes depending on how busy the free servers are that hour.

A busy-server skip is not a failure. In a multi-city run, a city the free servers won't answer for is skipped with a message and the run carries on with everything else — the CSV is still written. Nothing found is lost. Rerun the same command in five minutes and the missing city merges in (duplicates collapse, so rerunning never double-counts).

And rerunning never eats your phone-lookup work. Before it writes anything, a rerun reads the leads.csv that's already there and carries every phone and email you typed into it across to the new rows — plus any number you wrote on a Phone found: line in lookup-list.md. If a row that had your number in it doesn't come back from the map this time, the old file is renamed leads-YYYY-MM-DD-HHMM.csv and the run says so out loud rather than overwriting it. Same for a lookup list you've written on. That is what makes the monthly rescrape safe to run.

Fix 3 — build 20 by hand and merge them in (FREE, no key, no card)

If you're still short after fixes 1 and 2, your area is an OpenStreetMap coverage desert and no amount of rerunning will change that. Grooming studios are exactly the kind of small, high-turnover business volunteers haven't got round to mapping. That's the data, not you, and it says nothing about how many groomers actually work near you.

This is the fix that always works, in every city, without giving anyone your card. You do the finding; the scraper does everything else.

  1. Copy the template: cp hand-list-template.csv my-list.csv (Windows: copy hand-list-template.csv my-list.csv.) Open it in a spreadsheet — it's one header row: name,phone,website,city,address,email.
  2. Search pet groomer near [your city] in Google Maps. Work down the results and type in the name and the phone number for each one. Website, city and address if it's there; skip them if it isn't. Only name is required. Twenty rows is a boring hour, and it is the single highest-value hour on a thin day one.
  3. Save as CSV, then merge it into a normal run:
python3 leads.py --city "Toledo, Ohio" --merge-csv my-list.csv

What happens to your rows: they're de-duplicated against the map data (a hand row and a mapped row for the same studio become one row with the blanks filled from both), their websites are fetched and scored like everything else, they get a lead_type, and they land in the same ranked leads.csv. The source column shows hand, or osm+hand where the two agreed.

You can also run the hand list on its own, with no city at all:

python3 leads.py --merge-csv my-list.csv

Ask your Claude to do the searching: "Search Google Maps for pet groomers in [my city] and fill in scraper/my-list.csv from the template — name and phone for each, no invented numbers." Then check a couple by hand before you dial: never dial a number nobody verified.

Fix 4 — turn on Google Places (faster, needs a card)

Google Places knows essentially every business in America, and its rows almost always arrive with phone numbers. It takes about ten minutes to set up, requires a Google Cloud project with billing enabled (a card on file), and there is a free monthly usage allowance. Full walkthrough below under "OPTIONAL: add Google Places as a second source" — including the honest note that we cannot field-test the live-key path, which is exactly why fix 3 comes first.

No phone on a hot lead? lookup-list.md

Map data usually has the studio but not the number, and in this niche that is the normal case, not a failure. Timed field-test numbers, so you can set your own expectations before the run: --city "Mesa, Arizona" returned 15 studios with 2 phone numbers (13 lookups); the seven-city Phoenix-metro run returned 60 studios with 8 phone numbers (52 lookups). Budget about a minute per lookup by hand — 30 seconds is the best case, not the average — so a full 50+ lead pot is 45–60 minutes of lookups, or considerably less if you hand the file to your Claude. So the scraper writes lookup-list.md next to your CSV (with --out boise-leads.csv it's named boise-leads-lookup-list.md): every weak-web-score-85+ lead with no phone, each with a pre-built Google Maps search link. Click the link, read the number off the Maps listing, paste it in — or paste the file's built-in prompt to your Claude and let it do the lookups with web search, which is the faster path and the one we'd actually use.

One rule, and it's in the file too: never dial an unverified number. If no listing turns up, the studio 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. It may start with a ' (e.g. '+1-865-675-1100). That apostrophe is a spreadsheet-safety character so Excel keeps the +1 instead of treating it as a formula; it is part of the text, not part of the number. Strip the leading apostrophe before you use the number anywhere except the CSV — a call sheet, a tel: link, a proposal.
email Email, same two sources. Often blank; the phone is your weapon anyway.
website Their site as recorded in the map data. Blank means the map has no site on file — sometimes because they don't have one, sometimes because nobody added it. We can't tell you the split; nobody has measured it. Blank is a lead, not a fact. Confirm before you say it out loud on a call.
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. With --no-enrich this reads not checked for every lead that has a website — nobody looked, so it is not a 0 and must not be read as "their site is fine". Those rows sort with the hot ones, not the tidy ones.
weak_web_reasons Plain-English why: "no SSL", "not mobile-friendly", "copyright frozen at 2014"... This is where to LOOK, not a line to read out. See the box under the score table.
category How the source tags them (shop=pet_grooming from OSM, etc.).
opening_hours When they're open, if known — call when they're in (avoid the morning drop-off rush).
lat, lon Map coordinates. Paste into Google Maps to see the shop.
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, hand (your own list via --merge-csv), or a combination like osm+hand (found by both — a good "this studio is real" signal).
takes_online_bookings The offer column. no = we opened their homepage and found no way to book on it — that is exactly what this kit sells, and those rows sort to the top. yes = they already take online bookings, so the booking-page pitch does not apply; they drop to the bottom of the file (still real leads for reminders and rebooking). unknown = there was no page to check. booking_evidence says why. Still open the page before you say it on a call.
booking_evidence What the answer above was based on: links to calendly.com, "Book Online" link, no booking link found on the homepage, no website to check.
phone_source Where the number came from. map data — glance at their listing before you dial means an OpenStreetMap volunteer typed it at some point and nobody has looked since; it counts as dialable, and it earns ten seconds on their Google listing first. your own list, your lookup list and their website were all read by a human.
lead_type studio = an independent grooming business, your customer. name match only — verify it's a grooming studio = the map does NOT tag them as a grooming business; they turned up because their name contains one of our keywords. Usually a real studio tagged lazily (shop=yes), occasionally something else entirely — a real Philadelphia run pulled in a church with "Bridegroom" in its name. Look before you dial; these are not counted in DIALABLE NOW. chain or vet? — verify before calling = the name or category looks like PetSmart/Petco/a vet hospital, where head office picks the booking system and the local manager can't buy anything. Flagged, not deleted — a real one-shop studio can be called "The Pet Spa Store", so a human glances before it's dropped. Your Claude drops flagged rows at call prep and tells you which.

The weak-web score

Score Meaning Your move
100 No website in the map data. Not the same as "no website" — OpenStreetMap simply has no site on file for them. Sometimes that's because there isn't one; sometimes nobody ever added it. Treat it as "worth looking up", never as a finding. Call first — it's still the best pot. But verify before you use it in an opener. Search the studio's name before you dial (30 seconds, or hand the list to your Claude). If they do have a site, the no-show/rebooking angle still sells. Never open with "I noticed you don't have a website" unless you checked.
85 Nothing answered at the address on file. The name doesn't resolve, or the connection is refused — the site is not there. Almost as good as 100. Their link is broken and they may not know. Still worth ten seconds in a browser first: a link can be broken from here and fine from their office.
40–45 We could not see the site. 40 = it answered and refused our check (403/429/503 — Cloudflare, Wordfence and friends do this to scripts all day while the site is perfectly fine for customers). 45 = it didn't load inside the timeout, which might be them and might be your wifi. This is not a weakness, it's a blind spot — never say anything about their website on the strength of it. Open the URL yourself, in a browser. Whatever you see there is what you may talk about.
35–80 Site exists but has real problems: no SSL padlock, not mobile-friendly, ancient markup, frozen copyright year. Read the reason, then open the page and confirm it before you say it. Once you have seen it with your own eyes it is a strong opener — a groomer's clients book from their phones, so "not mobile-friendly" is a heavy line here. (The 40 and 45 rows above land inside this range on purpose — they are the two that say "verify".)
0–30 Site is basically fine. Lower priority for a rebuild — lead with the no-show angle: reminders, rebooking campaigns, review engine. Check takes_online_bookings before you decide this row is cold: a score-0 site with no in that column is often the best lead in the file — good business, no way to book.

⚠️ weak_web_reasons is where to look. It is not a quote.

Every other column here is a fact somebody recorded. This one is a machine's guess from a single automated page fetch, and it is the one the operator reads at the highest-frequency moment in the business — the first ten seconds of a cold call.

Measured failure, Knoxville, 2026-08-04. A studio was scored "copyright frozen at 2010" because an Adobe/SIL font licence sat inside an inline <style> block on their page. Their visible footer says © 2024. Read word for word, that opens a cold call by telling a stranger something false about his own business that he can disprove in four seconds — the exact failure every other page in this kit exists to prevent.

That specific bug is fixed (the scorer now strips <script>, <style> and comment blocks before it looks at anything, prefers the copyright line in the <footer>, and reads a range like "© 2015–2026" as 2026). scraper/self-test.py checks it and 22 other behaviours; run it any time you want to see for yourself. But it is still one automated fetch of one page, and 40 and 45 mean outright "we could not see the site".

The rule, and it is the same rule as everywhere else in this kit: open the page, confirm what the column says, and only then may it come out of your mouth. If you can't open it, the observation does not exist — that lead is evidence C in sales/outreach.md, and evidence C is never wrong.

Can they take a booking? (takes_online_bookings)

The score above measures how OLD a site looks. The offer in this kit is online booking, so the question that decides whether the pitch even applies is a different one: can a customer book on that page right now?

The scraper answers it while the homepage is already open, and writes yes, no or unknown into takes_online_bookings. It counts a link only when the link is real — a known booking host (Cal.com, Calendly, Vagaro, Booksy, Acuity, Square, Wix Bookings, MoeGo, Gingr, PetExec and friends) in an href or an iframe, or an anchor/button whose own text says "Book now" / "Book online" / "Schedule an appointment". A "Book now" button that dials a phone number is not online booking and is reported as no.

Two honest limits:

  • no is a strong start, not proof. It means we found no booking link on the homepage. Some studios bury booking on a second page. Open the site before you say it on a call — same rule as everything else here.
  • Framework strings look like booking and aren't. A page-builder's runtime config genuinely contains things like booking.cal.api.domain on a site with no booking of any kind. That is why the check ignores everything inside <script> and <style>, and why it only counts real links. If you ever see a yes you can't find on the page, believe the page.

Why it matters, from one real run: two leads both scored 0 / "site looks OK" and both sorted to the bottom of the file. One was a studio whose own page says "call today to book your pet's spa appointment" — no online booking at all, i.e. the best prospect on the list. The other already ran a live "Book Online" page with a waitlist — a lead the core pitch does not apply to. Identical score, opposite meaning. That is the column's whole job.

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 grooming studios are exactly the kind of business OSM under-maps, and if your rows stay thin even across 5–7 cities, Google Places knows almost every business in America — and 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 "Boise, Idaho" --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 "Boise, Idaho" --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.

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 unit-tested 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. A map query for the city — asks the OpenStreetMap Overpass API (free, keyless) for everything inside that boundary tagged as a grooming business (shop=pet_grooming and friends), plus any pet business whose name contains groom/paw spa/dog wash/pet salon. (Big cities sometimes blow the free servers' time budget on that; the scraper then retries with a lighter version of the same question, which is why one city can cost two queries before the widener even starts.) Businesses the name-match drags in are handled two ways: a wrong-species category is deleted ("Men's Ultimate Grooming" is a barbershop; churches, schools and cafes go the same way), and anything that qualified only because its name contains the keyword is flagged in lead_type rather than sold to you as a studio — the map never actually said it was one.
  3. Metro widener — if that city's own boundary came back with fewer than 25 dialable leads, it searches roughly 15 miles around the city centre too, and labels those rows "<city> area".
  4. Your hand-built list — with --merge-csv, the rows you typed yourself join the pot on equal terms.
  5. Optional Google pull — with --source google, up to 60 Places results per city join the pot.
  6. Merge + dedupe — same map ID, same phone, same name in the same city, or same name within a couple of miles = the same business. Blanks get filled from whichever source knows more, so nobody gets dialled twice.
  7. 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.
  8. CSV out, weakest web presence first — plus lookup-list.md for hot leads still missing a phone, plus the DIALABLE NOW box and, if you're short, the ordered list of fixes with the exact command to type next.

The scraper is deliberately polite: up to four map queries per city (the boundary query, its lighter retry, and the widener's two sweeps — most cities use fewer), automatic backoff and mirror-switching when the free servers are busy, and a slow crawl over lead websites. Its User-Agent says exactly that, so the volunteers who run those servers can see what we are. Don't run it in a loop — once per city is all you need.

Troubleshooting

First: it worked, but it printed scary text

Some output looks like an error and isn't. Two of these print on every healthy run on a Mac, before anything else happens:

NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl'
module is compiled with 'LibreSSL 2.8.3'.
  warnings.warn(
WARNING: You are using pip version 21.2.4; however, version 26.0.1 is available.

Both are harmless. Ignore both. Do not run the pip upgrade command on day one — it fixes nothing you have and it's a detour. The first one is macOS's built-in Python being built against Apple's encryption library instead of the one a helper package prefers; every successful run in our testing printed it.

Two more that look bad and are fine:

  • Overpass is busy — waiting 8s, then retrying (attempt 2/4)... — the free map servers are shared with the whole world. The scraper waits and retries. It happened in most of our timed runs. Each retry adds 8–30 seconds.
  • Dropped 3 non-pet groomer business(es) (human salon/barber caught by the name match) — that's the filter protecting you from calling a barbershop.

The rule: the words that mean something actually broke are "Error", "Traceback", and "command not found". "Warning" is the computer clearing its throat. docs/what-you-will-see.md shows a full annotated run — real output, line by line, with a verdict on each line. Read it once before your first scrape and nothing on your screen will be a surprise.

Then: the things that really do stop the run

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

"Could not get an answer for [city] — the free map servers are busy right now. Skipping this city and carrying on." — not a failure. One city the free servers wouldn't answer for is skipped and the rest of the run continues; your CSV is still written with everything that did come back. Rerun the same command in five minutes and the missing city merges in — duplicates collapse, so rerunning never double-counts.

"The free OpenStreetMap servers were too busy to answer for any of your cities" — the only version of that problem that stops a run, and it means every city failed. Wait 5 minutes, run the exact same command again.

"OpenStreetMap has no pet groomers mapped inside ..." (or only a handful) — normal; grooming shops are under-mapped compared to bigger trades. Two of the seven cities in our Phoenix-metro test returned 2 and 3 rows. The scraper prints your fixes in order at the end of every thin run, with the exact command to type — and the full explanation of each one is in "When your city runs thin — four fixes, in order" above. Short version:

  1. The metro widener already ran automatically.
  2. Add neighbouring cities to the same run — five or six, not two.
  3. Build 20 by hand and merge them in with --merge-csv — free, no key, no card, works everywhere.
  4. OPTIONAL, needs a card: add Google Places as a second source.

Thin map data means competitors aren't finding these leads either; it doesn't mean the groomers aren't there.

"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).

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.

Two things about this niche, and neither is a claim about how the calls will go — nobody has made one with this kit. First, you are calling about a leak the owner already feels: no-shows eating two-hour slots. Second, you are not selling a website, you are selling the plumbing behind the bookings. Both of those change what you say, not what happens.

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.


Day 1 — Setup + Scrape (2–3 hours, most of it phone lookups)

Goal: kit running, your area scraped, a lead list on your screen.

  1. Connect your Claude. Follow docs/connect-claude.md start to finish. You're done when you open this folder in Claude Code, ask "what business is this?", and Claude answers like it runs the place.

  2. 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 ($300–$800 booking-page + reminder setups, $79–$199/mo care plans).

  3. Run the scraper. Skim docs/what-you-will-see.md first (five minutes) — it shows a real run printing, including the two warnings that look like errors and aren't. Then, in Claude Code, just say:

    Run the lead scraper for my city.

    Claude will run scraper/leads.py and produce leads.csv — every grooming studio OpenStreetMap knows about in your area, with whatever website the map data has on file and a "weak-web" score. A high score means the map has no site for them or the site it has is weak — which is a reason to look them up, not a finding. Score 100 in particular means only "OpenStreetMap has no website tag", so confirm before you say anything about their website on a call. No API key needed; day one is free.

  4. Fill in the missing phone numbers (45–60 min by hand). This is the real Day 1 work. Map data has the studios but usually not their numbers. A live Mesa run gave us 15 studios and 2 phone numbers; a live seven-city Phoenix-metro run gave us 60 studios and 8 phone numbers. That is normal and the scraper is not broken.

    Every phoneless hot lead — 13 of them in that Mesa run, 52 in the seven-city one — is waiting for you in scraper/lookup-list.md, which the scraper writes automatically next to leads.csv. Each row has a ready-made Google Maps search link:

    • Click the link → read the phone number off the Maps listing → paste it into leads.csv. Budget about a minute each — 30 seconds is the best case, and the average includes checking it's the right business.
    • Or paste the prompt that's already inside lookup-list.md to your Claude and let it do the lookups. Do this first; it's the difference between a 20-minute job and an hour.
    • Across the cities you need for a 50+ pot, expect around 50 lookups — our seven-city run left 52 of them. By hand that's 45–60 minutes. You do it once per lead, forever.
    • Never dial an unverified number. No Maps listing = the studio may be gone. Skip the row.

    Do this tonight, not tomorrow morning. Tomorrow morning is for dialing — and do step 5 before this one if you haven't scraped your neighbouring cities yet, so you only work the lookup list once.

  5. Scrape the neighbors too — more of them than you'd guess. Grooming shops are thin on the map: a mid-size city gives 3–15 rows. Our timed runs, all keyless, same day: Mesa alone 15 leads (1m28s) → four cities 35 leads (3m17s) → seven cities 60 leads (6m48s). It took the whole Phoenix metro to clear 50. Ask Claude:

    Run the scraper for the five or six nearest cities as well, all in one command.

    One command, one merged and deduped file — don't run them separately. Budget 5–15 minutes of scraping, and expect "Overpass is busy" retries along the way (normal; docs/what-you-will-see.md shows exactly what they look like).

  6. Look at your list. Ask Claude:

    Show me the top 20 leads by weak-web score.

    These are tomorrow's calls. Skim them. The list can't tell you how a studio takes bookings — that's what discovery question 1 on the call is for, and it's the question the whole pitch hangs on. What the list gives you is who to ask first.

  7. Optional but smart: deploy your own agency site tonight. agency-site/ has the instructions — it's a drag-and-drop deploy on Netlify, free, ~15 minutes. Having a real site makes tomorrow's calls easier ("check us out at...").

Done when: leads.csv exists with enough verified phone numbers to fill tomorrow morning — 25 is the bar, not 50 rows. Rows are not calls. In practice that means a 5–7 city scrape (50–60 rows in a well-mapped metro, fewer in a thin one) and a worked lookup-list.md. Plus config/business.yaml has your info in it.

If your area is genuinely thin and 25 verified numbers isn't reachable tonight, that's a real answer, not a failure — start calling with what you have and read the coverage ladder in scraper/README.md (more cities → the whole county → optional Google Places) before you decide the kit is broken.


Day 2 — Your First 12 Calls

Goal: 12 dials. Not 12 sales. 12 dials.

Twelve, not twenty-five, and that is deliberate: your number is brand new, and US carriers flag a fresh line that suddenly makes thirty calls in a morning (docs/your-phone-setup.md). Week one ramps 12 / 15 / 18 / 20 / 20 across days 2–6 — 85 dials, every one of them from a number that still rings through.

Nerves are normal. The script carries you.

⚠️ What if you don't have 12 numbers yet? In a thin metro you very probably don't — Knoxville gave 8 verified numbers after a full day-one session. Then you dial the 8. The ramp is a ceiling that protects a brand new phone number, not a quota you owe anybody: eight real conversations beats twelve dials where four are guesses. Do not pad the list with numbers nobody verified — a wrong-number call is a wasted slot and a bad thirty seconds. Call what you have in the morning, and have your Claude build the next fifteen by hand while you debrief. Tomorrow you'll have more than you can dial.

  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 work/call-list-<today>.md — 25 prioritized leads with a custom opener for each, plus your discovery questions and price anchor on one page. You are dialling the top 12 today; the rest carry into tomorrow.

  2. Print or open sales/call-script.md. It's short on purpose: 15-second opener, 3 discovery questions, pitch, price anchor, close. Read it out loud twice before your first dial. Note the close: on a cold call you're not closing the sale — you're closing a 10-minute mock-up call later this week. Day and time, pinned.

  3. Open sales/objections.md in a second window. Top 8 objections with exact responses. You will hear "I'm mid-groom, I'm busy" and "how much?" today. Both are covered.

  4. Call 10am–2pm local time. Groomers are with dogs all day, but avoid the 8–9:30am drop-off rush and the after-4pm pickup scramble. Never call on a Saturday — it's their biggest day.

  5. After every call, tell Claude what happened. One line is enough:

    Called Pawfect Grooming — no answer, left voicemail. Called The Dog House — talked to Maria, books by DM only, mock-up call Thursday 2pm.

    Claude keeps your pipeline. This is how nothing falls through the cracks.

  6. No-answers get the templates in sales/outreach.md — leave the voicemail, then send SMS #1 right after. The follow-up cadence is day 1 / day 3 / day 7, one personal message at a time, never a blast.

What a dozen calls realistically looks like: most won't pick up (they're elbow-deep in a doodle). Two or three will talk. One might say yes to seeing a mock-up. That's a good first day. Log everything.

Done when: 12 dials logged with Claude, voicemail + SMS #1 sent to every no-answer.


Day 3 — 15 More Calls + Build the Mock-Ups

  1. Before anything else, build mock-ups for yesterday's warm leads. This is your secret weapon. Run prompts/03-fulfill.md in demo mode — Claude fills the booking-page template from fulfillment/ with the studio's real name and services, watermarked as a demo. Ten minutes per mock-up, and it changes what the second call is: they're reacting to their own studio on a screen instead of listening to a description.
  2. Publish each mock-up so it's a link, not a file. Read fulfillment/publish-a-demo.md once — five minutes to make a free Netlify account, then 60 seconds per demo: drag the demo folder onto app.netlify.com/drop, rename the site, copy the URL. Do this the same day you build the mock-up. A demo you can't text is not a demo, and you will have already promised the link on the phone.
  3. Research anyone with a demo call booked using prompts/02-research-lead.md. Claude deep-dives one lead: their reviews, how they take bookings today, what's broken, what to say. Walking in knowing their business beats any script.
  4. 15 fresh dials from the next batch on your list. Same routine as Day 2 — one step up the ramp.
  5. Day-3 follow-ups go out to Day 2's no-answers (templates in sales/outreach.md).
  6. End of day, ask Claude:

    Pipeline status — who's warm, who do I call back, what did I learn today?

Done when: 27 total dials (12 + 15), and a mock-up built and published as a link for every warm lead.


Day 4 — The Mock-Up Calls (Your First Real Pitches)

By now you have demo calls on the calendar. Today you pitch properly.

  1. The pitch, in one breath: "Here's your booking page — your studio, your services, your prices. Clients book themselves at 10pm from the couch, every booking gets an instant confirmation and an email reminder the day before, and I set up a two-minute morning text routine on your own phone so the day-of reminder comes from you — that's the one people actually answer. Between them, nobody's appointment goes unmentioned before it happens, and anyone who needs to move it has two easy chances to tell you. Setups like this run $300–$800, I handle everything, and there's no monthly fee unless you want the care plan later."

    Say the morning-routine part out loud. It is two minutes of their day and it is part of what you're selling — a client who finds out about it after they paid is a client asking for their money back.

  2. Show, don't tell. Send the mock-up link while you're on the phone — the link you published on Day 3 with fulfillment/publish-a-demo.md. Watching a groomer click through their own page — real services, real name — is where this sale happens. Let them click. Be quiet.

  3. The close is in sales/call-script.md. Ask for the yes, take a deposit (half up front is standard — prompts/04-invoice.md shows Claude how to set up a Stripe or PayPal link), and book their delivery slot: "I'll have you live by Friday." Same-week delivery is your edge. The AI makes it possible.

  4. Keep dialing. 18 more calls around your demo appointments — day 4 of the ramp. The pipeline never stops feeding.

Done when: at least one full mock-up 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 week's setup slots — should I hold one for you?" Scarcity is honest here: you genuinely can only do so many setups per week while delivering same-week.
  2. When someone says yes:
    • Send the service agreement first. office/service-agreement.md — one page, plain English, five brackets to fill. It says what you're building, what you're not, how many revision rounds they get, who owns the accounts, and how the deposit works. Signed agreement + deposit = build starts. Never start on a handshake; it's the whole reason office/ has this file.
    • Take the deposit (prompts/04-invoice.md handles the invoice).
    • First time money has ever come to you? Read docs/setting-up-shop.md today — sole proprietor vs LLC, whether your agency name needs a DBA, a separate bank account, and setting ~25–30% aside for tax. One page, not legal or tax advice, and much easier to read now than in April.
    • Send the intake questions from the fulfillment/ checklist — services and prices, hours, photos, which phone gets the booking notifications.
    • Book the delivery slot — a real day on the calendar, this week or early next.
  3. 20 dials — day 5 of the ramp — around the callbacks.
  4. Day-7 follow-ups go out to Day 2's silent leads (sales/outreach.md).

Day 6 — Deliver (or Keep Calling)

If you closed: open prompts/03-fulfill.md in full-delivery mode. It drives Claude through the fulfillment/ templates step by step — the client's booking page, the booking-tool setup (Cal.com free tier), the automatic confirmation and 24-hour email reminder, and installing the morning text routine on the owner's phone. A first delivery takes an afternoon. fulfillment/delivery-checklist.md is the boss and it starts at Stage 0: signed service agreement and deposit in hand before you build. Deliver exactly what you sold — booking page live, reminders set up, owner taught the morning routine and able to do it without you.

If you haven't closed yet: normal. 20 more dials (the last step of the ramp) and another mock-up or two. Some CEOs may close on day 3, others not until week 2 or later. Grooming is a relationship niche — expect the follow-up cadence to do a lot of the work. The funnel arithmetic, clearly labelled as assumptions rather than results, is in playbook/month-1.md.


Day 7 — Review + Reset

Run prompts/05-weekly-review.md with Claude. It walks the week: calls made, conversations had, mock-ups sent, pipeline state, follow-ups due, and next week's plan.

Then rest. Real businesses take a day off.


Week 1 Scoreboard

Metric Target
Dials 85 — the 12/15/18/20/20 ramp (days 2–6). More is not better in week one: it's how a new number gets flagged
Real conversations 10–20
Mock-ups built + sent 3–6
Full pitches (demo calls) 2–4
Closes 0–2 — either is on track

Clearly labeled assumptions, exactly as playbook/month-1.md labels them. The dial ramp is a real constraint from docs/your-phone-setup.md and it is yours to control. Everything below the dials line is a guess. Nobody here has run a week of this business and counted — this kit has never signed a client — so treat those rows as a shape to compare against, never as a standard you are failing to meet. If your real numbers come in lower, that is information about your market, not a verdict on you.

One row on this table is not effort, and pretending otherwise is unfair: lead supply. How many dialable numbers your town yields is set by OpenStreetMap coverage and how many groomers exist near you, and our measured range is 2–8 dialable from a metro scrape before you build any by hand. That is why day 1 has you building a list rather than dialling one.

The only way to fail week 1 is to not make the calls.

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.