# LEADS: the AI system that finds business owners ready to sell (and the buyers for them)

Built for business brokers (Main Street and lower-middle-market M&A). Every morning it pulls every company in your
trades and your metro from official government data, checks how long each one has been around, reads its website,
checks the owner's license history, scores how likely the owner is to sell in the next 12-24 months, and writes a
confidential letter, email and call opener for the top 25. Then it flips sides and finds the buyers: new
holding companies and platforms that just raised money in your industry.

No scraping tools, no lead lists you have to rent. Official open data plus each company's own public website.
Service businesses get the same engine pointed the other way (new companies and new permits = new customers): see section 9.

**What a real run looked like** (Denver metro, HVAC + plumbing + landscaping, October 2026): 496 companies registered
20+ years ago, 12 A-tier and 118 B-tier sellers, 25 personalized drafts, 14 buyer signals. Spot-checked 32 companies by
hand against BBB, company websites, county contractor records and the state license roster: 58 of 62 checked facts
were right (about 94%). After the web check, all of the top 25 had a verified owner or contact and 23 had a working phone.
The sample files from that run are not included: they name real owners, and confidential means confidential.
Run it on your own market and you get the same files in a few minutes.

---

## 1. What you get

| File | What it does |
|---|---|
| `run.sh` | One command, every morning. Runs everything below in order and writes `out/TODAY.md` |
| `registry.py` | Pulls companies from the official state business registry: formation date, address, registered agent |
| `site_signals.py` | Finds and reads each company's own website: phone, "since 1978", owner name, family owned, succession talk, stale site |
| `licenses.py` | Official state license roster: how long the owner has been licensed (best free proxy for age), license lapsed |
| `places_enrich.py` | Optional, Google Places: phone, website, rating, review count, latest review date |
| `score_sellers.py` | Likely-to-sell score with a plain-English "why" for every company. Tiers A / B / C |
| `merge.py` | Pulls in the facts your AI agent verified on the web (`prompts/web-check.md`) |
| `buyers.py` | Buy side: SEC Form D filings (new platforms that raised money) + new local holding companies |
| `write_outreach.py` | Confidential letter + email + call opener per lead, using only that lead's real facts. Sends nothing |
| `permits.py`, `registry.py new` | Service-business version: new permits and brand-new companies |
| `report.py` | `out/TODAY.md`: only what's new since yesterday |
| `prompts/` | Setup prompt, daily agent prompt, web-check prompt for Claude Code or Codex |
| `templates/` | Owner letter, email and call script, scoring rubric, buyer intake |
| `schedule/` | cron line and Mac launchd file for the 7am run |

## 2. How the pieces fit

```
state registry (20+ yr old companies in your trades + metro)
   -> their website (phone, owner, "since", family, succession, stale site)
   -> state license roster (owner licensed since 1979? license lapsed?)
   -> [optional] Google Places (rating, reviews, review slowdown)
   -> [optional] your AI agent verifies the top 25 on the web
   -> score: likely to sell in 12-24 months, with reasons
   -> drafts: letter / email / call opener for the top 25
buy side: SEC Form D + new holdcos -> buyers.csv
                          -> out/TODAY.md every morning
```

## 3. 10-minute quickstart

You need Python 3.9+ (Mac: it's there, or `brew install python`). Then:

```bash
unzip leads-kit.zip && cd leads-kit
python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
cp leads.env.example leads.env
```

Open `leads.env` and set: `STATE`, `CITIES` (your metro), `INDUSTRIES` (2-3 trades you broker), `YOU` (your name
and firm), `LEADS_UA` (your name and email: SEC asks for it). Optional: `OPENAI_API_KEY` for the drafts.

```bash
./run.sh
```

First run takes 10-20 minutes (it reads a few hundred websites). Then open `out/TODAY.md` and `out/sellers_scored.csv`.
Next mornings are fast: everything already checked is skipped.

Easier: open the folder in Claude Code or Codex and paste `prompts/setup.md`. It does all of this with you.

Built-in states: **Colorado** (registry + license roster) and **New York** (registry). Other state: the setup prompt has
your agent find your state's official business-entity open data and license roster and plug them in. Most states publish both.

## 4. Keys and what they cost

| Key | Needed? | What it unlocks | Cost (official pricing pages, October 2026) |
|---|---|---|---|
| none | | Registry, websites, licenses, scoring, buyers, permits | Free |
| `OPENAI_API_KEY` | Optional | The written drafts. Without it you get `out/outreach_prompts.md` to paste into ChatGPT or Claude | gpt-5.4-mini: $0.75 / 1M input tokens, $4.50 / 1M output. 25 drafts cost a few cents |
| `GOOGLE_PLACES_API_KEY` | Recommended | Phone, website, rating, review count and latest review date, automatically, for most companies | Text Search Enterprise (needed for phone/rating/website): 1,000 free a month, then $35 per 1,000. Review dates use Place Details (Enterprise + Atmosphere): 1,000 free, then $25 per 1,000. run.sh caps it at 300 a day, inside the free tier |
| OpenCorporates | Not needed | Multi-state company data | Token required; free only for open-data projects, paid plans from £225/month |

Google Places key: console.cloud.google.com, create a project, enable "Places API (New)", Credentials, Create API key,
restrict it to Places API. Put it in `leads.env` as `GOOGLE_PLACES_API_KEY=...`.

Any OpenAI-compatible API works for drafts (xAI, OpenRouter, a local model): set `LEADS_BASE_URL` and `LEADS_MODEL`.

## 5. Sell side, step by step (what `run.sh` does)

1. **Old companies in your trades.** `registry.py old --state CO --city "Denver,Aurora,..." --names "PLUMBING,DRAIN" --before 2006`
   Drops nonprofits and associations. Flags whether a real person is the registered agent (owner-operator) or a law firm/agent service.
2. **Their website.** `site_signals.py` guesses the domain from the name and only keeps it if the page shows the company
   name AND your city, state or area code. Pulls phone, email, "since 19xx", owner name, family owned, succession and
   handoff language, kids in the business, already acquired, copyright year, https, mobile, online booking.
3. **License history.** `licenses.py lookup` adds the business license status and how long the owner has held a trade
   license. A Master Plumber licensed since 1979 is 45+ years in: retirement is close.
4. **Google Places** (if you have the key): rating, reviews, latest review date.
5. **Web check.** Paste `prompts/web-check.md` into Claude Code/Codex. It verifies the top 25 on BBB, the company site and
   license lookups, and writes `out/web_verified.csv`. The next run merges it in automatically.
6. **Score.** `score_sellers.py` (see `templates/scoring-rubric.md`). Kids already running it, new owner, already
   acquired or license lapsed push a company down, so you don't waste letters.

High-signal sources to check by hand once a week (no API, don't scrape them):
- **BizBuySell / BizQuest**: listings in your trades that disappear without a "sold" note are owners who tried and stalled. Note the business and look it up.
- **Local business journal** "family business" and anniversary stories (50 years in business = a conversation).
- **Obituaries and retirement announcements** that mention a company: handle with care, wait, be human.
- **Practice owners**: `licenses.py veterans --type DEN --city "Denver,Aurora" --before 1996` gives dentists, CPAs or vets licensed 30+ years.

## 6. Buy side

`buyers.py --industry "HVAC,plumbing,home services" --state CO --days 730` finds:
- **SEC Form D filings** from entities like "NHC HVAC Holdings" or "CC Plumbing Holdings": platforms that just raised money to buy companies, with the names of the people who filed.
- **New local holding companies** in your state registry ("<Trade> Holdings / Acquisition / Capital").

Then add by hand (see `templates/buyer-intake.md`): Searchfunder searchers, LinkedIn searches for search funds and
operating partners, Axial buy boxes, and local operators already acquiring. Qualify every buyer with the 6 intake questions before you show them anything.

## 7. Outreach that owners actually answer

`write_outreach.py` writes a letter, email and call opener per lead from that company's real facts
("[Company] has served [City] since [year]"). It never assumes they want to sell, never says
"retire" unless the owner said it publicly, and never mentions how you found them. Read every draft before you use it.

What works for owners 55+: a printed, hand-signed letter marked Personal & Confidential, a short call a week later, one
follow-up, then leave it. Templates in `templates/`.

Rules:
- **US email (CAN-SPAM, applies to B2B):** honest From and subject, your physical postal address, a clear opt-out, honor it within 10 business days. Penalties up to $53,088 per email.
- **Gmail/Yahoo sender rules:** SPF, DKIM, DMARC on your sending domain, spam complaints under 0.3% (aim for 0.1%), one-click unsubscribe if you ever send in bulk.
- **Calls and texts (TCPA):** call business lines, don't cold text.
- **UK/EU:** cold email to sole traders and partnerships needs consent under PECR; for limited companies it's allowed with an opt-out, and GDPR still applies (legitimate interest, tell them where you got their data, honor objections). Letters are the safe channel.
- **Confidentiality:** never tell anyone which owners you're talking to.

## 8. Daily automation

Plain: `crontab -e` and paste the first line from `schedule/cron.txt` (runs `./run.sh` at 7am).

With an agent that also fixes breakages and verifies new A-tier leads: use the `claude -p` or `codex exec` line in
`schedule/cron.txt`. It runs `prompts/daily-agent.md`.

Mac that sleeps at 7am: use `schedule/com.leads.daily.plist` with launchd instead (it runs when the Mac wakes).

Every morning: open `out/TODAY.md`. New A-tier sellers, new buyers, drafts waiting. Nothing is ever sent for you.

## 9. Service businesses: same engine, finding customers

Run with `MODE=service` in `leads.env`:
- `registry.py new --days 2`: every company registered in your metro in the last 2 days (they need insurance,
  bookkeeping, signs, websites, cleaning, IT, payroll).
- `permits.py --city austin --match "pool,new construction"`: new building permits (Austin, Chicago, LA built in; add your
  city's open-data permit feed). A pool permit means a fence, landscaping and lighting job soon.
- `write_outreach.py --mode service --offer "..."` writes openers from that real signal.

Speed matters: an HBR study (Oldroyd, McElheran, Elkington, 2011) found firms that contacted a lead within an hour were
nearly 7x more likely to qualify it than those who waited even one more hour. That's why it runs every morning.

## 10. Adapting it to other businesses

- **Other broker niches:** change `INDUSTRIES` (e.g. `"auto:AUTO REPAIR,COLLISION|dental:DENTAL"`) and use `licenses.py veterans` for licensed practices.
- **Commercial real estate / wealth managers / insurance:** owners of 30-year-old companies are the same people. Same list, different offer.
- **Agencies and B2B services:** use the new-company and permit feeds.

## 11. Troubleshooting

| Problem | Fix |
|---|---|
| `copy leads.env.example to leads.env` | You skipped step 3 of the quickstart |
| `0 found` from registry.py | Check `CITIES` spelling and the name words (they match inside the legal company name, e.g. `LANDSCAP` catches LANDSCAPING and LANDSCAPE) |
| SEC returns 403 | Set `LEADS_UA` to your real name and email |
| Few websites found | Normal without Google Places: guesses only keep sites that prove they're the same company. Add the Places key or run the web check |
| Wrong owner name | Run the web check; `web_verified.csv` always wins. The score text says "same name, verify" when it's a name match only |
| Drafts sound generic | The lead has few facts. Run the web check first, then delete that row from `out/outreach.csv` and run again |
| Run is slow | First run reads every site; later runs skip them. Lower the list with `--before` or fewer cities |
| My state isn't built in | Paste `prompts/setup.md` into Claude Code/Codex: it finds your state's registry + license open data |
| Cron didn't run on Mac | Use the launchd file, or give cron Full Disk Access |

## 12. Checklist

- [ ] venv + `pip install -r requirements.txt`
- [ ] `leads.env` filled (state, cities, 2-3 industries, your name, LEADS_UA)
- [ ] `./run.sh` once, open `out/TODAY.md`
- [ ] Web check on the top 25 (`prompts/web-check.md`)
- [ ] Read the drafts, print 10 letters this week
- [ ] Add the 7am schedule when you're happy with the list
