List expander skill
Expand a hard-to-build niche list from ~10 known-good seed companies into a full qualified TAM.
by growthenginenowoslawski·MIT license·★ 736 Stars on the repo·GitHub ↗
npx degit growthenginenowoslawski/coldoutboundskills/skills/list-expander#main ~/.claude/skills/list-expanderChecked ·commit main
Files of List expander
Show the full text169 lines
List Expander — seed companies → lookalikes → mined filters → qualified TAM
The problem this solves
Niche lists come out tiny because we search databases with narrow explicit keywords, but real good-fit companies (e.g. Atlantic Medical Group, Hackensack Meridian) often don't carry that keyword in their database record. Instead of guessing keywords top-down, this skill works bottom-up: take companies you KNOW fit, discover how the database actually tags them, expand via lookalike engines, and only then derive the wide-net filters — with measured precision per filter — before pulling and AI-qualifying at scale.
Pipeline (5 phases)
All scripts live in scripts/, run with npx tsx, and are dependency-free (node ≥ 18 native
fetch). Every script takes --help. Keys load from the repo-root .env (see .env.example),
falling back to ~/.env. Artifacts land in ~/output/list-expander/{run}/.
Phase 1 fingerprint.ts seeds → how each shows up in Prospeo + its live homepage
Phase 2 lookalikes.ts seeds → candidates via Prospeo lookalike + Exa + Parallel
(qualify candidates → lookalikes-confirmed.csv)
Phase 3 mine-filters.ts confirmed fits → candidate filters → scorecard (volume × precision)
Phase 4 pull.ts winning filters → wide pull, auto-shard, dedup, exclusions
score-batch.ts scale AI qualification (gpt-5-nano by default)
verify-website.ts second pass on QUALIFIED rows: live homepage fetch →
dead / suspended-parked / live; live sites re-judged on
their CURRENT content (catches stale-DB ghosts). Always run —
DB descriptions happily qualify dead companies otherwise.
Phase 5 contact-count.ts verified-email TAM ceiling (free Prospeo count trick)
report.ts single-file HTML transparency report for the client
Phase 0 — inputs
- ~10 seed companies known for sure to fit (domains).
- 1–2 sentence ICP description ("multi-site medical/specialty groups in the US, ≥$3M revenue").
- Optional: exclusion CSV (companies already in campaigns).
Phase 1 — fingerprint the seeds
npx tsx scripts/fingerprint.ts --domains="a.com,b.com,..." --run=<slug>
Prints coverage (which seeds Prospeo is missing — that gap IS the under-count story for the
client), plus industry and keyword-tag frequency tables. Writes fingerprint.json/csv.
Phase 2 — generate lookalikes
npx tsx scripts/lookalikes.ts --domains="<seeds>" --run=<slug> \
--objective="<NL description of the COMPANY TYPE (not your product!)>" \
--country="United States #US" --pages=3
- Prospeo
company_lookalike(required lane):{"domain": "<seed>"}— one call per seed, single domain only (arrays 400). Composable with location/headcount/keyword filters. One large health system seed returned 5,727 lookalikes. - Exa findSimilar (optional lane): content similarity, and it returns homepage text in the same call — free evidence for qualification.
- Parallel.ai entity-search (optional lane): ~$0.005/req, pads to
match_limitwith junk — always qualify before trusting; returns LinkedIn URLs, not domains.
Optional lanes whose key is unset are skipped with a log line, so the run still completes on
Prospeo alone. Then qualify candidates (Claude sub-agents for a small set, or score-batch.ts
with a draft prompt) → write lookalikes-confirmed.csv. Target 50–150 confirmed.
⚠️ EXHAUSTIVE-SWEEP DEFAULT
Phase 3's scorecard is for TRANSPARENCY, not selection. The pull in Phase 4 must include: (a) EVERY industry carried by ≥1 confirmed fit — whole industry, headcount band + geo only, no keyword narrowing; (b) EVERY discriminative keyword from confirmed fits across ALL industries. Score everything with the cheap model. Only band + geography are legal pre-filters. Snowball until net-new drops under 2–3%. Never trim the sweep to save AI cost — recall is the product. Pull keywords unless they are stopword-grade.
Phase 3 — mine + score filters
npx tsx scripts/mine-filters.ts --csv=<confirmed.csv> --run=<slug> --propose --country="United States #US"
# Claude reviews/edits {run}/candidates.json: prune generic n-grams, add synonym keywords
# (the "every medical group contains 'group'" trap — kill terms that are frequent but not discriminative)
npx tsx scripts/mine-filters.ts --run=<slug> --scorecard
# score the 25-company samples:
for f in ~/output/list-expander/<slug>/samples/*.csv; do
npx tsx scripts/score-batch.ts --csv=$f --prompt-file=<icp-prompt.txt> --out=${f%.csv}-scored.csv; done
mkdir -p ~/output/list-expander/<slug>/samples-scored && mv ~/output/list-expander/<slug>/samples/*-scored.csv $_
npx tsx scripts/mine-filters.ts --run=<slug> --scorecard --precision-from=~/output/list-expander/<slug>/samples-scored
--propose fingerprints each confirmed company against Prospeo and fetches its live homepage,
then mines 2–3-grams across that combined text. The homepage is the evidence source that matters:
it says what a company calls itself today, which is exactly what a keyword filter has to match.
Pass --no-scrape to mine from Prospeo descriptions only (faster, thinner).
Output: filter-scorecard.csv — per filter: Prospeo count, sampled precision, estimated
qualified yield. Review with the user before Phase 4.
Phase 4 — wide pull + scale qualification
Tune the qualification prompt FIRST via /icp-prompt-builder (interactive, 10-company batches,
2 clean rounds to converge).
# Write {run}/winners.json (filter_sets + base_filters — format documented in pull.ts header)
npx tsx scripts/pull.ts --run=<slug> --test # 2 pages/set sanity check FIRST
npx tsx scripts/pull.ts --run=<slug> --exclude=<existing.csv>
npx tsx scripts/score-batch.ts --csv=<run>/pull-all.csv --prompt-file=<icp-prompt.txt> --scrape --concurrency=8
npx tsx scripts/verify-website.ts --run=<slug> --prompt-file=<icp-prompt.txt> --concurrency=40
Verify the first test output shows real successes before the full run. Any filter set whose
total_count exceeds 24k is auto-sharded (country → 51 states → headcount-band bisection) so the
tail is never truncated.
Phase 5 — TAM ceiling + report
npx tsx scripts/contact-count.ts --csv=<qualified.csv> --run=<slug> \
--titles="COO,VP Operations,..." --seniorities="C-Suite,Vice President,Head,Director"
npx tsx scripts/report.ts --run=<slug> --title="<Vertical> — TAM Expansion"
open ~/output/list-expander/<slug>/report.html
Requirements / env
Put these in the repo-root .env (copy .env.example), or ~/.env.
REQUIRED
| Var | What for | Sign up |
|---|---|---|
PROSPEO_API_KEY |
Every phase: company search, lookalikes, counts | https://prospeo.io/ → dashboard → API (copy the X-KEY) |
OPENAI_API_KEY |
AI qualification in score-batch.ts + verify-website.ts |
https://platform.openai.com/api-keys |
OPTIONAL (each is one lane; unset = that lane logs skipped: <VAR> not set and the run continues)
| Var | What for | Sign up |
|---|---|---|
EXA_API_KEY |
Exa findSimilar lookalike lane in lookalikes.ts |
https://exa.ai/ |
PARALLEL_AI_API_KEY |
Parallel.ai entity-search lookalike lane in lookalikes.ts |
https://parallel.ai/ |
OPENAI_API_KEY_NANO |
A separate cheap-model key; used in preference to OPENAI_API_KEY when set |
https://platform.openai.com/api-keys |
OPENAI_ICP_MODEL |
Override the qualification model (default gpt-5-nano) |
— |
PROSPEO_MIN_INTERVAL_MS |
Slow Prospeo pacing below the built-in 450ms floor. Can only make it slower — values under 450 are clamped | — |
No database is required. Every artifact is a file under ~/output/list-expander/{run}/.
Verified API facts
| Filter | Syntax | Notes |
|---|---|---|
company_lookalike |
{"domain": "x.com"} or {"icp_text": "..."} |
single domain only; icp_text describing the product surfaces vendors — describe the company |
company_keywords |
{"include": [...], "exclude": [...]} |
multi-word phrases OK; combine with company_industry for precision |
company_key_customers |
{"include": [...]} |
matches by who their customers are |
company_headcount_custom |
{"min": N, "max": N} |
use this, not headcount_range (enum format unverified) |
company_products_services, company_icp |
❌ broken/unusable via API | use company_keywords / company_lookalike.icp_text instead |
Prospeo pacing (measured): PROSPEO_MIN_INTERVAL_MS=200 (5 req/s) trips "Rate limit exceeded"
after ~1,000 requests; 450 ms (~2.2 req/s) ran 1,550+ requests clean. The account limit is
GLOBAL, so lib.ts paces every process through a shared slot file
(~/.cache/prospeo-lock/) with a 450 ms floor, backs off 45 s on a rate-limit, and makes that
penalty visible to every other running process. Identical request+page re-runs within 30 days are
FREE (free:true) — re-pulling after a partial failure costs nothing.
Prospeo count trick: a page-1 call's pagination.total_count sizes any filter cheaply; add
person_contact_details:{email:["VERIFIED"]} on /search-person for the verified-email ceiling.
Seniority enum: Founder/Owner, C-Suite, Partner, Vice President, Head, Director, Manager, Senior, Entry, Intern — never "VP", never "President".
Related skills
/icp-prompt-builder— tune the qualification prompt before Phase 4 (the score-batch script it refers to lives HERE:scripts/score-batch.ts)/prospeo-search-api— full Prospeo filter reference/prospeo-full-export— title-first paginated lead export once you have the company list/blitz-list-builder— domain-first contact discovery on the qualified companies/disco-like— a 4th lookalike source (seed domains or NL ICP text)/list-quality-scorecard— grade the final CSV before it goes anywhere near a campaign
| 1 | |
| 2 | name list-expander |
| 3 | description Expand a hard-to-build niche list from ~10 known-good seed companies into a full qualified TAM. Seed fingerprinting (how do good-fit companies ACTUALLY show up in the company database's filters) → lookalike generation (Prospeo company_lookalike, Exa findSimilar, Parallel.ai entity search) → filter mining with a precision/volume scorecard → wide pull with auto-sharding → cheap-AI qualification → live-website verification → client transparency report. Use when a vertical list feels "too small" (medical groups, hedge funds, Medicare brokerages), when known good-fit companies don't show up in keyword searches, or when someone says "expand this list", "the TAM should be bigger", "find more companies like these". |
| 4 | |
| 5 | |
| 6 | # List Expander — seed companies → lookalikes → mined filters → qualified TAM |
| 7 | |
| 8 | ## The problem this solves |
| 9 | |
| 10 | Niche lists come out tiny because we search databases with narrow explicit keywords, but real |
| 11 | good-fit companies (e.g. Atlantic Medical Group, Hackensack Meridian) often don't carry that |
| 12 | keyword in their database record. Instead of guessing keywords top-down, this skill works |
| 13 | bottom-up: take companies you KNOW fit, discover how the database actually tags them, expand via |
| 14 | lookalike engines, and only then derive the wide-net filters — with measured precision per filter |
| 15 | — before pulling and AI-qualifying at scale. |
| 16 | |
| 17 | ## Pipeline (5 phases) |
| 18 | |
| 19 | All scripts live in `scripts/`, run with `npx tsx`, and are dependency-free (node ≥ 18 native |
| 20 | fetch). Every script takes `--help`. Keys load from the repo-root `.env` (see `.env.example`), |
| 21 | falling back to `~/.env`. Artifacts land in `~/output/list-expander/{run}/`. |
| 22 | |
| 23 | |
| 24 | Phase 1 fingerprint.ts seeds → how each shows up in Prospeo + its live homepage |
| 25 | Phase 2 lookalikes.ts seeds → candidates via Prospeo lookalike + Exa + Parallel |
| 26 | (qualify candidates → lookalikes-confirmed.csv) |
| 27 | Phase 3 mine-filters.ts confirmed fits → candidate filters → scorecard (volume × precision) |
| 28 | Phase 4 pull.ts winning filters → wide pull, auto-shard, dedup, exclusions |
| 29 | score-batch.ts scale AI qualification (gpt-5-nano by default) |
| 30 | verify-website.ts second pass on QUALIFIED rows: live homepage fetch → |
| 31 | dead / suspended-parked / live; live sites re-judged on |
| 32 | their CURRENT content (catches stale-DB ghosts). Always run — |
| 33 | DB descriptions happily qualify dead companies otherwise. |
| 34 | Phase 5 contact-count.ts verified-email TAM ceiling (free Prospeo count trick) |
| 35 | report.ts single-file HTML transparency report for the client |
| 36 | |
| 37 | |
| 38 | ### Phase 0 — inputs |
| 39 | ~10 seed companies known for sure to fit (domains). |
| 40 | 1–2 sentence ICP description ("multi-site medical/specialty groups in the US, ≥$3M revenue"). |
| 41 | Optional: exclusion CSV (companies already in campaigns). |
| 42 | |
| 43 | ### Phase 1 — fingerprint the seeds |
| 44 | |
| 45 | npx tsx scripts/fingerprint.ts --domains="a.com,b.com,..." --run=<slug> |
| 46 | |
| 47 | Prints coverage (which seeds Prospeo is missing — **that gap IS the under-count story for the |
| 48 | client**), plus industry and keyword-tag frequency tables. Writes `fingerprint.json/csv`. |
| 49 | |
| 50 | ### Phase 2 — generate lookalikes |
| 51 | |
| 52 | npx tsx scripts/lookalikes.ts --domains="<seeds>" --run=<slug> \ |
| 53 | --objective="<NL description of the COMPANY TYPE (not your product!)>" \ |
| 54 | --country="United States #US" --pages=3 |
| 55 | |
| 56 | **Prospeo `company_lookalike`** (required lane): `{"domain": "<seed>"}` — one call per seed, |
| 57 | single domain only (arrays 400). Composable with location/headcount/keyword filters. |
| 58 | One large health system seed returned 5,727 lookalikes. |
| 59 | **Exa findSimilar** (optional lane): content similarity, and it returns homepage text in the |
| 60 | same call — free evidence for qualification. |
| 61 | **Parallel.ai entity-search** (optional lane): ~$0.005/req, pads to `match_limit` with junk — |
| 62 | always qualify before trusting; returns LinkedIn URLs, not domains. |
| 63 | |
| 64 | Optional lanes whose key is unset are **skipped with a log line**, so the run still completes on |
| 65 | Prospeo alone. Then qualify candidates (Claude sub-agents for a small set, or `score-batch.ts` |
| 66 | with a draft prompt) → write `lookalikes-confirmed.csv`. Target 50–150 confirmed. |
| 67 | |
| 68 | ### ⚠️ EXHAUSTIVE-SWEEP DEFAULT |
| 69 | Phase 3's scorecard is for TRANSPARENCY, not selection. The pull in Phase 4 must include: |
| 70 | (a) EVERY industry carried by ≥1 confirmed fit — whole industry, headcount band + geo only, no |
| 71 | keyword narrowing; (b) EVERY discriminative keyword from confirmed fits across ALL industries. |
| 72 | Score everything with the cheap model. Only band + geography are legal pre-filters. Snowball |
| 73 | until net-new drops under 2–3%. Never trim the sweep to save AI cost — recall is the product. |
| 74 | Pull keywords unless they are stopword-grade. |
| 75 | |
| 76 | ### Phase 3 — mine + score filters |
| 77 | |
| 78 | npx tsx scripts/mine-filters.ts --csv=<confirmed.csv> --run=<slug> --propose --country="United States #US" |
| 79 | # Claude reviews/edits {run}/candidates.json: prune generic n-grams, add synonym keywords |
| 80 | # (the "every medical group contains 'group'" trap — kill terms that are frequent but not discriminative) |
| 81 | npx tsx scripts/mine-filters.ts --run=<slug> --scorecard |
| 82 | # score the 25-company samples: |
| 83 | for f in ~/output/list-expander/<slug>/samples/*.csv; do |
| 84 | npx tsx scripts/score-batch.ts --csv=$f --prompt-file=<icp-prompt.txt> --out=${f%.csv}-scored.csv; done |
| 85 | mkdir -p ~/output/list-expander/<slug>/samples-scored && mv ~/output/list-expander/<slug>/samples/*-scored.csv $_ |
| 86 | npx tsx scripts/mine-filters.ts --run=<slug> --scorecard --precision-from=~/output/list-expander/<slug>/samples-scored |
| 87 | |
| 88 | `--propose` fingerprints each confirmed company against Prospeo **and fetches its live homepage**, |
| 89 | then mines 2–3-grams across that combined text. The homepage is the evidence source that matters: |
| 90 | it says what a company calls itself *today*, which is exactly what a keyword filter has to match. |
| 91 | Pass `--no-scrape` to mine from Prospeo descriptions only (faster, thinner). |
| 92 | |
| 93 | Output: `filter-scorecard.csv` — per filter: Prospeo count, sampled precision, estimated |
| 94 | qualified yield. **Review with the user before Phase 4.** |
| 95 | |
| 96 | ### Phase 4 — wide pull + scale qualification |
| 97 | Tune the qualification prompt FIRST via `/icp-prompt-builder` (interactive, 10-company batches, |
| 98 | 2 clean rounds to converge). |
| 99 | |
| 100 | # Write {run}/winners.json (filter_sets + base_filters — format documented in pull.ts header) |
| 101 | npx tsx scripts/pull.ts --run=<slug> --test # 2 pages/set sanity check FIRST |
| 102 | npx tsx scripts/pull.ts --run=<slug> --exclude=<existing.csv> |
| 103 | npx tsx scripts/score-batch.ts --csv=<run>/pull-all.csv --prompt-file=<icp-prompt.txt> --scrape --concurrency=8 |
| 104 | npx tsx scripts/verify-website.ts --run=<slug> --prompt-file=<icp-prompt.txt> --concurrency=40 |
| 105 | |
| 106 | Verify the first test output shows real successes before the full run. Any filter set whose |
| 107 | `total_count` exceeds 24k is auto-sharded (country → 51 states → headcount-band bisection) so the |
| 108 | tail is never truncated. |
| 109 | |
| 110 | ### Phase 5 — TAM ceiling + report |
| 111 | |
| 112 | npx tsx scripts/contact-count.ts --csv=<qualified.csv> --run=<slug> \ |
| 113 | --titles="COO,VP Operations,..." --seniorities="C-Suite,Vice President,Head,Director" |
| 114 | npx tsx scripts/report.ts --run=<slug> --title="<Vertical> — TAM Expansion" |
| 115 | open ~/output/list-expander/<slug>/report.html |
| 116 | |
| 117 | |
| 118 | ## Requirements / env |
| 119 | |
| 120 | Put these in the repo-root `.env` (copy `.env.example`), or `~/.env`. |
| 121 | |
| 122 | **REQUIRED** |
| 123 | | Var | What for | Sign up | |
| 124 | |---|---|---| |
| 125 | | `PROSPEO_API_KEY` | Every phase: company search, lookalikes, counts | https://prospeo.io/ → dashboard → API (copy the X-KEY) | |
| 126 | | `OPENAI_API_KEY` | AI qualification in `score-batch.ts` + `verify-website.ts` | https://platform.openai.com/api-keys | |
| 127 | |
| 128 | **OPTIONAL** (each is one lane; unset = that lane logs `skipped: <VAR> not set` and the run continues) |
| 129 | | Var | What for | Sign up | |
| 130 | |---|---|---| |
| 131 | | `EXA_API_KEY` | Exa `findSimilar` lookalike lane in `lookalikes.ts` | https://exa.ai/ | |
| 132 | | `PARALLEL_AI_API_KEY` | Parallel.ai entity-search lookalike lane in `lookalikes.ts` | https://parallel.ai/ | |
| 133 | | `OPENAI_API_KEY_NANO` | A separate cheap-model key; used in preference to `OPENAI_API_KEY` when set | https://platform.openai.com/api-keys | |
| 134 | | `OPENAI_ICP_MODEL` | Override the qualification model (default `gpt-5-nano`) | — | |
| 135 | | `PROSPEO_MIN_INTERVAL_MS` | Slow Prospeo pacing below the built-in 450ms floor. Can only make it **slower** — values under 450 are clamped | — | |
| 136 | |
| 137 | No database is required. Every artifact is a file under `~/output/list-expander/{run}/`. |
| 138 | |
| 139 | ## Verified API facts |
| 140 | |
| 141 | | Filter | Syntax | Notes | |
| 142 | |---|---|---| |
| 143 | | `company_lookalike` | `{"domain": "x.com"}` or `{"icp_text": "..."}` | single domain only; `icp_text` describing the *product* surfaces vendors — describe the *company* | |
| 144 | | `company_keywords` | `{"include": [...], "exclude": [...]}` | multi-word phrases OK; combine with `company_industry` for precision | |
| 145 | | `company_key_customers` | `{"include": [...]}` | matches by who their customers are | |
| 146 | | `company_headcount_custom` | `{"min": N, "max": N}` | use this, not `headcount_range` (enum format unverified) | |
| 147 | | `company_products_services`, `company_icp` | ❌ broken/unusable via API | use `company_keywords` / `company_lookalike.icp_text` instead | |
| 148 | |
| 149 | **Prospeo pacing (measured):** `PROSPEO_MIN_INTERVAL_MS=200` (5 req/s) trips "Rate limit exceeded" |
| 150 | after ~1,000 requests; **450 ms (~2.2 req/s) ran 1,550+ requests clean**. The account limit is |
| 151 | GLOBAL, so `lib.ts` paces every process through a shared slot file |
| 152 | (`~/.cache/prospeo-lock/`) with a 450 ms floor, backs off 45 s on a rate-limit, and makes that |
| 153 | penalty visible to every other running process. Identical request+page re-runs within 30 days are |
| 154 | FREE (`free:true`) — re-pulling after a partial failure costs nothing. |
| 155 | |
| 156 | **Prospeo count trick:** a page-1 call's `pagination.total_count` sizes any filter cheaply; add |
| 157 | `person_contact_details:{email:["VERIFIED"]}` on `/search-person` for the verified-email ceiling. |
| 158 | Seniority enum: `Founder/Owner, C-Suite, Partner, Vice President, Head, Director, Manager, Senior, |
| 159 | Entry, Intern` — never "VP", never "President". |
| 160 | |
| 161 | ## Related skills |
| 162 | `/icp-prompt-builder` — tune the qualification prompt before Phase 4 (the score-batch script it |
| 163 | refers to lives HERE: `scripts/score-batch.ts`) |
| 164 | `/prospeo-search-api` — full Prospeo filter reference |
| 165 | `/prospeo-full-export` — title-first paginated lead export once you have the company list |
| 166 | `/blitz-list-builder` — domain-first contact discovery on the qualified companies |
| 167 | `/disco-like` — a 4th lookalike source (seed domains or NL ICP text) |
| 168 | `/list-quality-scorecard` — grade the final CSV before it goes anywhere near a campaign |
| 169 |
Discussion
Alternatives
Browse more free Claude skills or everything in Product.