Local Lead Prospector
Compliance up front
Google Maps listing data is the source. Two paths exist and they are not equal.
- Preferred path: Google Places API (New). Official, paid, billed per request. Requires
GOOGLE_MAPS_API_KEYin the environment. Free tier is small, so a cost estimate is shown before any pull above 50 rows. - Fallback path: Playwright scrape of Google Maps. This is against Google's Terms of Service. It exists for cases where the user has no API key and accepts the risk. It only runs after the user explicitly confirms in the same turn. Do not run it silently.
Listing data only. Never produce decision-maker names, personal emails, or LinkedIn profiles. Enrichment past the public Google Maps listing is out of scope.
What you get per business
Each row in the CSV:
namecategory(primary type from Google)addressphone(international format when Google has it, blank when missing)website(blank when missing)rating(0.0 to 5.0)reviews(count)source_url(Google Maps URL for this place)
Missing phone or website cells stay blank. Do not fabricate or guess values.
Workflow
1. Confirm industry and region
Both must be specific. If either is vague, ask one clarifying question and stop.
- "Texas" is too broad. Ask for a city or metro.
- "small business" is too broad. Ask for an industry like "dentists", "HVAC", "law firms".
- "Austin, TX" is real.
- "dentists in Austin, TX" is ready to run.
2. Confirm count
Default is 40 rows. Ask only if the user has not stated a number. If the user asks for more than 100, push back and suggest splitting by sub-region (zip, neighborhood, adjacent city). Over 100 in one pull tends to hit the same dense cluster and waste cost.
3. Pick the method
- If
GOOGLE_MAPS_API_KEYis set, use the API. No prompt, no scrape. - If the env var is missing, explain that the API is preferred, point at
references/places_api_setup.md, and offer the scrape fallback. Do not run the scrape without explicit confirmation from the user in this turn.
4. Cost estimate before large pulls
If the requested count is above 50 and the API path is being used, surface the cost estimate first. Pricing tier is Places API (New) Text Search. At the time of writing, Text Search is billed per request and each request returns up to 20 results, so a 100-row pull is about 5 paginated requests. Show the estimate and ask the user to confirm before running.
5. Run the script
API path:
python scripts/fetch_places_api.py \
--industry "dentists" \
--region "Austin, TX" \
--count 40 \
--out leads.csv
Scrape path (only after user confirms):
python scripts/scrape_listings.py \
--industry "dentists" \
--region "Austin, TX" \
--count 40 \
--out leads.csv \
--yes-tos
6. Write the CSV and the summary
Both scripts emit the same CSV schema. Header line is byte-identical:
name,category,address,phone,website,rating,reviews,source_url
Then post the markdown summary in the chat (template below).
Summary template
After the run, post this exact structure back to the user:
## Lead list: <industry> in <region>
- Total leads: <N>
- With phone: <P> (<P/N>%)
- With website: <W> (<W/N>%)
- Source: Google Places API (New) | Google Maps scrape (ToS opt-in)
- CSV: <path to leads.csv>
### Sample (first 10)
| Name | Category | Phone | Website | Rating | Reviews |
|---|---|---|---|---|---|
| ... | ... | ... | ... | ... | ... |
### SDR notes
- Rows without a phone are usable for email-only sequences.
- Rows without a website are local, likely solo operators. Cold-call first.
- Ratings below 3.5 with high review counts often signal an operations gap, which is a real opening for a pitch about ops tooling.
- The CSV does not contain decision-maker names. Pair with manual LinkedIn lookups before sending personalized outreach.
Numbers are derived directly from the CSV that was written. Sample table is the first 10 rows. If fewer than 10 rows came back, show them all.
Failure handling
Surface clear messages. Never dump a Python traceback at the user.
- Zero rows. Exit cleanly. Tell the user the query returned nothing and suggest a broader industry term or a wider region. No traceback.
- Consent page (scrape only). The fallback dismisses Google's consent interstitial. If it cannot, it exits with "Google consent page blocked the scrape" and suggests trying the API path.
- Over-broad region. If the user is asking for a state or a country, do not run. Ask for a city or metro.
- Over-volume. If the user asks for more than 100, do not run. Suggest splitting by sub-region.
- API quota exceeded. Surface as "Google Places API quota exceeded for the current period. Either wait for reset or raise the quota in Google Cloud Console."
- API billing not enabled. HTTP 403 with "billing" in the body. Surface as "Google Places API billing is not enabled on this Cloud project. See references/places_api_setup.md."
- API key invalid.
API_KEY_INVALIDin the response. Surface as "GOOGLE_MAPS_API_KEY is invalid or restricted. Check the key in Google Cloud Console."
Dedupe
- API path dedupes on
place_idacross paginatedpageTokenresponses. - Scrape path dedupes on a
name + addresskey across scroll batches.
Out of scope
- Decision-maker enrichment (no names, no titles).
- Personal email lookup.
- LinkedIn scraping.
- Paid enrichment APIs (Apollo, ZoomInfo, Clearbit, etc.).
- CRM writes or push to Salesforce / HubSpot.
- Cross-run dedupe. Each run is independent. The SDR is responsible for merging lists across runs.
Setup
See references/places_api_setup.md for how to enable Places API (New), create a key, and set GOOGLE_MAPS_API_KEY.
The scrape fallback needs Playwright with Chromium:
pip install -r scripts/requirements.txt
python -m playwright install chromium
Do not run the scrape against Google Maps in CI or on a shared IP without understanding that it violates Google's Terms of Service.
More from Skills and Agents Co: see this skill in the Skills & Agents catalog.