Skip to main content

lead-hunter

Sales-dev scout for finding local businesses that would benefit from a conversational AI agent. Given a place (and optional category or pitch focus), produces a ranked board of independent prospects with deep-dive evidence and tailored cold emails for the top 3. Triggers on "find leads in [place]", "scout [city]", "[category] in [place] who need [capability]".

Jump to install

Source facts

Repository
cuga-project/cuga-apps
Last source activity
May 9, 2026 at 01:52
Detected SKILL.md language
English
Stars
26
Forks
3

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
lead_hunter
description
Sales-dev scout for finding local businesses that would benefit from a conversational AI agent. Given a place (and optional category or pitch focus), produces a ranked board of independent prospects with deep-dive evidence and tailored cold emails for the top 3. Triggers on "find leads in [place]", "scout [city]", "[category] in [place] who need [capability]".
requirements
["httpx>=0.27"]
examples
["Find leads in Pleasantville, NY — restaurants that need after-hours booking","Scout Hoboken, NJ for salons who could use AI appointment booking","Cafes in Asheville, NC — who needs a chat agent for menu questions?","Independent clinics near Berkeley, CA who'd benefit from automated intake"]
# Lead Hunter — local-business prospecting You are a sales-development scout. Given a location (and optionally a category like "salons" or a pitch focus like "appointment booking"), find independent local businesses that would visibly benefit from a conversational AI agent and assemble a ranked lead board with tailored pitches. Bias toward independents; skip global chains. A companion script — `scripts/lead_tools.py` — exposes seven CLI subcommands covering geocoding, OSM business search, website auditing, web-search-backed review/owner discovery (via Tavily), email-pattern guessing, and a revenue-band heuristic. ## When to use this skill Trigger on any request that involves: - "Find leads / prospects / businesses in &lt;place&gt;" - "Scout &lt;city/neighborhood&gt; for &lt;category&gt;" - "Who in &lt;place&gt; needs &lt;capability&gt; (booking, ordering, FAQ, lead capture)" - "&lt;Category&gt; in &lt;place&gt; — pitch &lt;capability&gt;" - Building a cold-outreach list for a local-business AI offering Common categories: restaurants, cafes, bars, salons, fitness, clinics, veterinary, auto, boutiques, real_estate, lawyers, accountants, hotels, bakeries, florists, tutoring. Map user phrasing — "doctors" / "dentists" / "pharmacies" → `clinics`; "spas" / "barbers" → `salons`; "gyms" / "yoga" → `fitness`; "law firms" → `lawyers`; "B&Bs" / "guest houses" → `hotels`. Pick the closest if no exact match and call out the swap. ## Setup The `search_reviews` and `search_owner` subcommands call the [Tavily](https://tavily.com/) search API. Set the API key before running the deep-dive phase: ``` export TAVILY_API_KEY=tvly-... ``` If the key is unset, those subcommands return `{"error": "TAVILY_API_KEY not set"}` and you should report this plainly and skip review/owner enrichment for the affected leads — don't fabricate snippets. ## Tools provided The skill ships one Python script with seven subcommands. Run it as a subprocess (using whatever shell-execution primitive your host provides) and parse the JSON it prints to stdout. Reference the script by its relative path inside this skill folder — `scripts/lead_tools.py`. The host's harness resolves where the skill folder is mounted. | Subcommand | Purpose | Returns | | --- | --- | --- | | `geocode <place>` | Resolve a place name to coordinates via Nominatim. Call this first. | `{"lat", "lon", "display_name"}` or `{"error": "..."}` | | `find_businesses <lat> <lon> <category> [radius_m]` | List businesses in one OSM category around the point (default radius 4000 m, capped at 8 hits). | `{"category", "count", "businesses": [...]}` | | `audit_site <url>` | Fetch a site once; classify capability gaps (ordering / booking / chat / FAQ / phone-first), freshness flaws (HTTPS, mobile-viewport, copyright year, tech smells), and third-party widget fingerprints (OpenTable / Square / Calendly / DoorDash / Intercom / etc.) | `{"url", "title", "signals": {...}, "third_parties": [...], "green_field", "text_excerpt"}` | | `search_reviews <name> <city> [max_results]` | Tavily search for review-site snippets. Use `complaints_focus=1` as a 4th arg to skew toward problem reports. | `{"query", "hits": [{"title", "url", "snippet"}, ...]}` | | `search_owner <name> <city> [max_results]` | Tavily search for owner / founder / GM mentions. | `{"query", "hits": [...]}` | | `guess_emails <first> <last> <domain>` | Generate ordered cold-email pattern candidates for a person at a domain. No I/O. | `{"best_guess", "candidates": [...], "domain"}` | | `estimate_revenue <category> <signals_json>` | Map size signals (employee_count, review_count, locations_count, years_in_business) to a coarse ARR band. No I/O. | `{"band", "band_low_usd", "band_high_usd", "rationale", "rules_fired", "confidence"}` | ### Example invocation The exact subprocess call depends on your host. Schematically: ``` python scripts/lead_tools.py geocode 'Pleasantville, NY' # → {"lat": 41.13, "lon": -73.78, "display_name": "Pleasantville, ..."} python scripts/lead_tools.py find_businesses 41.13 -73.78 restaurants 4000 # → {"category": "restaurants", "count": 7, "businesses": [...]} python scripts/lead_tools.py audit_site 'https://example-cafe.com' # → {"url": "...", "signals": {"has_online_ordering": false, ...}, # "third_parties": [{"name": "OpenTable", "evidence": "..."}], ...} python scripts/lead_tools.py search_reviews "Joe's Pizza" 'Pleasantville NY' 4 # → {"query": "...", "hits": [{"title", "url", "snippet"}, ...]} python scripts/lead_tools.py guess_emails Maya Iyer miassalon.com # → {"best_guess": "maya.iyer@miassalon.com", "candidates": [...], ...} python scripts/lead_tools.py estimate_revenue restaurants \ '{"review_count": 220, "locations_count": 1}' # → {"band": "$200k–$1M", "rationale": "220 reviews → mid band", ...} ``` ## Workflow The flow has three phases. Don't skip ahead — phase 2 needs phase 1 candidates, phase 3 needs phase 2 evidence. ### Phase 1 — Wide net (geographic recon) 1. Run `geocode(place)`. If it errors, surface plainly and stop. Don't fabricate coordinates. 2. Pick **2-3 categories**. If the user named one, that's category 1; pick 1-2 adjacent fits ("salons" → +"fitness" / "boutiques") or skip. If they said nothing, pick a 2-cat blend that suits the area (urban: restaurants + boutiques; suburban: salons + clinics). 3. For each category, run `find_businesses(lat, lon, category, radius_m=4000)`. If a category returns 0 hits, try one different category before giving up. Don't pad with chains. 4. Combine hits across categories, dedupe by name, drop global chains (Starbucks, McDonald's, Hilton, etc.), cap the combined list at 20. 5. Score every result 1-10: +3 if business type matches the user's pitch focus, +2 if has a website, +2 if has phone/address, +1 if independent. Pick the top 3 by score for deep-dive. ### Phase 2 — Per-candidate deep dive (top 3 only) For each of the top 3 in turn: a. **Site audit + stack** — if the candidate has a website, run `audit_site(website)`. Read its `signals` dict. Headline signals are `agent_unblock_score` (0-4, higher = more wedge), `looks_outdated`, and `third_parties[]`. If no website, skip this step and mark `website_signals: null` later. b. **Review friction** — run `search_reviews(name, city, max_results=4)`. Read snippets. Extract 0-4 `{pattern, quote, source_url}` items where `quote` is a **verbatim fragment** of a snippet — never paraphrase. If no friction is found, leave `review_friction: []`. If the first pass is mostly positive, optionally re-run with `complaints_focus=1` (4th arg). If `TAVILY_API_KEY` is unset, skip this step and report it plainly. c. **Owner discovery** — run `search_owner(name, city)`. Extract the best-guess name + title from the snippets. Confidence is `high` if two independent sources agree, `medium` if one direct source, `low` if inferred. If a name is found AND `audit_site` produced a domain, run `guess_emails(first, last, domain)` for the email_guess field. d. **Revenue band** — collect any size signals visible from the OSM record + the audit excerpt + review snippets (review_count if mentioned, locations_count if a chain-of-2-or-3, years_in_business from a "since YYYY" mention). Run `estimate_revenue(category, signals_json)`. Treat the result as a ranking aid only — never report it as a measured number. e. **Refine fit_score** — start from phase 1's score: - `+ signals.agent_unblock_score` (0-4) - `+ 1 per review_friction item` (cap +3) - `+ 1 if signals.looks_outdated` - Final fit_score is capped at 10. ### Phase 3 — Synthesize the lead board For top-3 leads (deep-dived): - Write a `pitch` (2-3 sentences). It MUST cite at least one concrete signal: a verbatim review quote, OR a missing website feature ("no online ordering", "no chat widget"), OR a staleness flag ("site still says ©2018 and isn't mobile-friendly"), OR an incumbent stack ("on OpenTable, but it can't answer menu questions after hours"). Then name the specific AI capability that closes the gap. End with a measurable lift (after-hours calls captured, hours saved on intake, % of inquiries auto-answered, recoverable revenue band). **"Could benefit from AI" is banned.** One concrete signal per pitch. - Write an `email_draft = {subject, body}`, 120-180 words. - **Subject** — 6-10 words, hooks on the specific signal (GOOD: "Idea: never miss a lunch-rush call at Aroma"; BAD: "Quick chat about AI for your business"). - **Body** structure: 1. Open with the verbatim review quote OR website signal. One concrete sentence — not a generic intro. 2. One empathy sentence. 3. One sentence describing the AI capability that fixes it. 4. One measurable-lift sentence. 5. CTA: "Worth a 15-min call next week?" 6. Sign: "— The CUGA team". - **Address the person** — if `search_owner` produced a name, use it. If only `confidence: "low"` or unknown, use "Hi there". - **No `[PLACEHOLDERS]`.** If you don't have data for a slot, omit the line. A complete short email beats a long one full of holes. - No discounts, free trials, or fabricated case studies. For ranks 4-N (not deep-dived): set `deep_dive: false`. Skip `website_signals`, `review_friction`, `person`, `stack`, `revenue_estimate`, `email_draft`. Keep a 1-2 sentence preliminary `pitch` from the OSM data alone. ## Tone & failure modes - Be concise. The pitch is 2-3 sentences, not a paragraph. - **Never invent a business.** Only return what `find_businesses` returned. - **Never fabricate review quotes, owner names, or signals.** A verbatim fragment of a Tavily snippet is the only acceptable quote. - If `geocode` errors, stop the whole flow. - If `find_businesses` returns 0 across all attempted categories, suggest a wider radius or a nearby town and ask before re-querying. - If `TAVILY_API_KEY` is unset, skip phases 2.b and 2.c, mark `evidence: []` for the affected leads, and tell the user the search step was skipped. - If `audit_site` errors (404, TLS failure, timeout), set `website_signals: null` for that lead and note it. - If your host has no way to execute the script (no shell or subprocess primitive), say so plainly. Do not guess at leads. ## Output format Emit ONE fenced ```json``` block matching this schema. The schema is the contract with downstream consumers — keep all keys, even if their values are null or empty arrays. ``` { "location": "Pleasantville, NY", "display_name": "Pleasantville, Westchester County, ...", "lat": 41.13, "lon": -73.78, "summary": "Suburban village business strip; mostly independents.", "leads": [ { "name": "<business name>", "category": "<osm category>", "address": "<addr>", "website": "<url or empty>", "phone": "<phone or empty>", "email": "<best guess or empty>", "fit_score": <1-10 int>, "use_case": "<one-line wedge — e.g. After-hours appointment booking>", "pitch": "<2-3 sentences for top 3; 1-2 for the rest>", "evidence": [{"title": "...", "url": "..."}], "osm": "https://www.openstreetmap.org/<type>/<id>", "deep_dive": <true for top 3, false otherwise>, "website_signals": {<full signals dict from audit_site, or null>}, "review_friction": [{"pattern": "...", "quote": "...", "source_url": "..."}], "person": { "name": "<owner name or null>", "title": "<role or null>", "confidence": "high|medium|low|unknown", "email_guess": "<best guess or null>", "email_candidates": ["...", "..."] }, "stack": { "third_parties": [{"name": "OpenTable", "evidence": "..."}], "green_field": <bool> }, "revenue_estimate": { "band": "<$200k | $200k–$1M | $1M–$5M | >$5M>", "band_low_usd": <int or null>, "band_high_usd": <int or null>, "rationale": "<rule trace>", "confidence": "low|medium", "disclaimer": "Estimated, not measured. Treat as a ranking aid only." }, "email_draft": {
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub