Build a Local Business Lead Generator
Query Google Maps and use Claude AI to write personalized outreach emails.
Introduction
30 Second Summary
Finding potential clients and writing personalized emails for each one is tedious work that doesn't scale. Most people spend hours researching businesses, looking up contact details, and crafting individual messages.
In this project, you will build a Python command-line tool that searches Google Maps for local businesses and uses AI to write a personalized cold outreach email for each one.
What You'll Build
A lead generation pipeline that combines the Google Places API for business discovery with Claude AI for personalized email generation, all orchestrated from a single Python script.
By the end of this project, you'll have:
- A working search function that finds real businesses near any location using the Google Places API (New) Nearby Search endpoint.
- An AI-powered email generator that crafts personalized outreach emails adapting automation suggestions to each business type.
- A complete pipeline that exports business details and draft emails to a CSV file ready for review in any spreadsheet application.
- Secret Mission: Add website scraping to generate hyper-personalized emails based on actual business homepage content.
Are there any prerequisites?
You should have basic Python familiarity (variables, functions, loops). No prior API experience is needed.
Set Up Your Accounts and Environment
Your lead generator needs two external services to work. It needs Google Places API to find real businesses near any location, and it needs Claude AI to write personalized outreach emails for each one.
Before writing any code, you need accounts with both services and API keys that let your Python script communicate with them. You also need a project folder and a few Python packages installed.
In this step, get ready to:
- Get a Google Places API key from Google Cloud Console.
- Get a Claude API key from Anthropic.
- Install Python packages and configure your project folder.
Create a Google Cloud account and get a Places API key
The Google Places API gives your script access to Google Maps data. You send it a location and business type, and it returns real businesses with their names, addresses, websites, and phone numbers.
- Open your browser and go to the Google Cloud Console:
- https://console.cloud.google.com/
- Sign in with your Google account. If you do not have a Google Cloud account, create one when prompted.
Once signed in, you need to create a new project to organize your API usage.
- Click the project selector dropdown at the top of the page.
- Click New Project.
- Enter lead-generator as the project name.
- Click Create.
- Wait for the project to be created, then select it from the project selector dropdown at the top.
Next, enable billing for your project. Google requires a billing account to activate APIs.
- In the left navigation menu, click Billing.
- Follow the prompts to link a billing account to your project. You will need a credit card, but you will not be charged for this project.
Will I be charged?
Google gives you 5,000 free Nearby Search requests per month for the Pro SKU, and 1,000 free requests per month for the Enterprise SKU (which includes website and phone data). This project uses at most a handful of requests, so you will stay well within the free tier.
Now enable the Places API (New) for your project.
- In the left navigation menu, click APIs & Services.
- Click Library.
- In the search bar, type Places API.
- Click Places API (New) from the results. Make sure you select the "New" version, not the Legacy one.
- Click Enable.
Finally, create an API key so your Python script can authenticate with the API.
- In the left navigation menu, click APIs & Services.
- Click Credentials.
- Click Create credentials at the top of the page.
- Select API key.
- Copy the generated key and save it somewhere safe. You will need it shortly.
What is an API key?
An API key is a unique string that identifies your project to a service. When your script sends a request to Google, it includes this key so Google knows which account to bill and which permissions to apply.
Create an Anthropic account and get a Claude API key
Claude is the AI model that will write personalized outreach emails for each business your script finds. You need an Anthropic API key to send prompts to Claude from your Python code.
- Open your browser and go to the Anthropic Console:
- https://console.anthropic.com/
- Sign up for a new account (you can use Google or email). If you already have an account, sign in.
- Once signed in, navigate to API Keys in the left sidebar or settings area.
- Click Create Key.
- Give the key a name like lead-generator and confirm.
- Copy the key immediately. Anthropic only shows it once.
Save your keys somewhere temporary
Paste both API keys into a temporary note or text file. You will add them to your project's .env file in just a moment.
Set up your Python project
With both API keys ready, it is time to create your project folder, install the Python packages you need, and store your keys securely.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir lead-generator
cd lead-generator
- Install the three Python packages your project needs by running:
pip3 install requests anthropic python-dotenv
What are these packages?
`requests` lets you make HTTP calls to the Google Places API. `anthropic` is the official Python SDK for communicating with Claude. `python-dotenv` loads environment variables from a .env file so you can keep API keys out of your code.
Seeing a permission error?
If you see a "permission denied" error, try running the command with pip3 install --user requests anthropic python-dotenv instead. This installs packages to your user directory without needing admin access.
Help me fix a pip install permission error on macOS.
Now create the .env file that will hold your API keys. This keeps sensitive credentials separate from your code.
- Create an empty .env file by running:
touch .env
- Open .env in your preferred text editor and paste the following template:
GOOGLE_PLACES_API_KEY=your-google-api-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here
- Replace your-google-api-key-here with the Google API key you copied earlier.
- Replace your-anthropic-api-key-here with the Anthropic API key you copied earlier.
- Save the file.
Why use a .env file?
A .env file keeps your API keys out of your source code. If you ever share or upload your project, the keys stay private as long as you do not include the .env file. The python-dotenv package automatically reads this file and makes the values available to your script.
Verify that all packages installed correctly by running this test command:
- Confirm your packages work by running:
python3 -c "import requests; import anthropic; import dotenv; print('All packages installed')"
You should see All packages installed printed in your terminal with no errors.
Seeing a ModuleNotFoundError?
If you see an error like "ModuleNotFoundError: No module named 'requests'", the install may not have targeted the same Python version. Try running python3 -m pip install requests anthropic python-dotenv to ensure the packages install for your python3 binary.
Help me fix a ModuleNotFoundError after pip3 install.
✔️ Awesome, I've got everything!
Great. Double check you have saved your .env file with both keys filled in.
ⓧ I'd like to double check the full code
Your .env file should look exactly like this (with your actual keys replacing the placeholders):
GOOGLE_PLACES_API_KEY=your-google-api-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here
Your environment is ready and both API keys are configured. Next up, you will write the function that searches Google Maps for real businesses near any location.
Search for Nearby Businesses with Google Places API
Your environment is ready with API keys stored securely and all packages installed.
Now it's time to write the code that actually searches Google Maps for real businesses. You'll build a Python function that sends a request to the Google Places API and returns structured data about nearby businesses.
In this step, get ready to:
- Create lead_generator.py with imports and API configuration.
- Write a function that queries Google Places for businesses near any location.
- Test the function with real Google Maps data.
Create lead_generator.py with imports and configuration
Before writing the search logic, you need a file that loads your API keys from .env and sets up the libraries you'll use throughout the project.
- Create a new file called lead_generator.py in your lead-generator folder.
- Add the following imports and configuration code:
import os
import csv
import time
import requests
from dotenv import load_dotenv
from anthropic import Anthropic
# Load API keys from the .env file
load_dotenv()
# Store API keys in variables for easy access
GOOGLE_API_KEY = os.getenv("GOOGLE_PLACES_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
# Create the Anthropic client for later use
client = Anthropic(api_key=ANTHROPIC_API_KEY)
What does this code do?
load_dotenv() reads your .env file and loads the key-value pairs as environment variables. This keeps your API keys out of your source code.
os.getenv() retrieves those values by name. The csv and time imports aren't used yet, but you'll need them in the next step when you build the full pipeline.
- Save lead_generator.py.
- Confirm there are no import errors by running:
python3 lead_generator.py
You should see no output at all. No output means no errors.
Seeing a ModuleNotFoundError?
If you see an error about a missing module, one of your packages didn't install correctly. Run pip3 install requests anthropic python-dotenv again from your lead-generator folder.
Also make sure your .env file is in the same folder as lead_generator.py. load_dotenv() looks for it in the current directory.
Help me fix my import errors in lead_generator.py
Build the business search function
The Google Places API (New) Nearby Search endpoint accepts a location, radius, and business type, then returns a list of matching places. Your function will send an HTTP POST request and parse the response into a clean Python dictionary for each business.
- Add the following function below your configuration code in lead_generator.py:
def search_nearby_businesses(latitude, longitude, radius, business_type, max_results=10):
"""Search for businesses near a location using Google Places API (New)."""
url = "https://places.googleapis.com/v1/places:searchNearby"
# Set authentication and specify which fields to return
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.types,places.websiteUri,places.nationalPhoneNumber,places.primaryType"
}
# Build the search parameters
payload = {
"includedTypes": [business_type],
"maxResultCount": max_results,
"locationRestriction": {
"circle": {
"center": {
"latitude": latitude,
"longitude": longitude
},
"radius": radius
}
}
}
# Send the request and parse the JSON response
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
# Extract relevant fields from each place
businesses = []
for place in data.get("places", []):
business = {
"name": place.get("displayName", {}).get("text", "Unknown"),
"address": place.get("formattedAddress", "No address"),
"types": place.get("types", []),
"primary_type": place.get("primaryType", "business"),
"website": place.get("websiteUri", ""),
"phone": place.get("nationalPhoneNumber", "")
}
businesses.append(business)
return businesses
What does this function do?
The X-Goog-Api-Key header authenticates your request with Google. The X-Goog-FieldMask header tells the API exactly which fields to return. Without it, you get nothing back.
The payload defines your search area as a circle (center point plus radius in meters) and filters by business type. response.raise_for_status() throws an error if something goes wrong (like an invalid API key), so you'll know immediately instead of silently getting bad data.
The parsing loop uses .get() with default values for every field. This handles businesses that might not have a website or phone number listed on Google Maps.
Why a POST request instead of GET?
Most APIs you've seen probably use GET requests with query parameters in the URL. The Places API (New) uses POST because the search parameters (nested location data, arrays of types) are too complex for a URL string. Sending them as a JSON body in a POST request keeps things structured and readable.
- Save lead_generator.py.
- Run python3 lead_generator.py again to confirm there are no syntax errors. You should see no output.
Seeing a SyntaxError?
Check that all your curly braces and square brackets are properly closed in the headers and payload dictionaries. A missing comma between key-value pairs is the most common cause.
Help me debug a SyntaxError in my search function
✔️ Awesome, I've got everything!
Great. Make sure you've saved lead_generator.py before moving on to test it.
ⓧ I'd like to double check the full code
import os
import csv
import time
import requests
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_PLACES_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
client = Anthropic(api_key=ANTHROPIC_API_KEY)
def search_nearby_businesses(latitude, longitude, radius, business_type, max_results=10):
"""Search for businesses near a location using Google Places API (New)."""
url = "https://places.googleapis.com/v1/places:searchNearby"
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.types,places.websiteUri,places.nationalPhoneNumber,places.primaryType"
}
payload = {
"includedTypes": [business_type],
"maxResultCount": max_results,
"locationRestriction": {
"circle": {
"center": {
"latitude": latitude,
"longitude": longitude
},
"radius": radius
}
}
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
businesses = []
for place in data.get("places", []):
business = {
"name": place.get("displayName", {}).get("text", "Unknown"),
"address": place.get("formattedAddress", "No address"),
"types": place.get("types", []),
"primary_type": place.get("primaryType", "business"),
"website": place.get("websiteUri", ""),
"phone": place.get("nationalPhoneNumber", "")
}
businesses.append(business)
return businesses
Test the search function with real data
Your function is written, but you haven't actually called it yet. Time to send a real request to Google and see what comes back.
- Test your function by running this command in your terminal:
python3 << 'EOF'
from lead_generator import search_nearby_businesses
businesses = search_nearby_businesses(37.7937, -122.3965, 1000.0, "restaurant", 5)
print(f"Found {len(businesses)} businesses:\n")
for b in businesses:
print(f" {b['name']}")
print(f" Address: {b['address']}")
print(f" Phone: {b['phone']}")
print(f" Website: {b['website']}")
print()
EOF
What does this test do?
This searches for restaurants within 1000 meters of downtown San Francisco (latitude 37.7937, longitude -122.3965). You should see a list of 5 real restaurants with their names, addresses, phone numbers, and websites.
Some businesses might show empty phone or website fields. That's normal. Not every listing on Google Maps has all contact details filled in.
You should see a list of real restaurant names and addresses printed in your terminal. The exact businesses will depend on what's near that San Francisco location.
Getting a 403 or REQUEST_DENIED error?
This means your API key isn't authorized for the Places API. Go back to the Google Cloud Console, navigate to APIs & Services, then Library, and confirm that "Places API (New)" shows as Enabled.
Also double check that your GOOGLE_PLACES_API_KEY value in .env matches the key shown in the Credentials page. A trailing space or missing character will cause this error.
Help me fix my Google Places API authentication error
Seeing an empty list with no businesses?
If the script runs without errors but prints "Found 0 businesses", the search location might not have that business type nearby. Try changing the radius to a larger value like 5000.0 (5 km) or try a different business type like cafe.
Help me figure out why my Places API search returns no results
You now have a working connection to Google Maps that pulls real business data on demand. Next up, you'll feed that data into Claude AI to generate personalized outreach emails for each business.
Generate Personalized Emails with Claude AI
In the previous step, you built a function that pulls real business data from Google Maps. Now you have names, addresses, and contact info ready to go.
Finding leads is only half the battle. The other half is writing a personalized email for each one. Doing this manually takes ages. In this step, you will use Claude AI to generate tailored outreach emails that adapt to each business type automatically.
In this step, get ready to:
- Write a function that sends business data to Claude and receives a personalized email back.
- Test the email generation with sample business data to confirm it adapts per business type.
Write the email generation function
The Anthropic Messages API accepts a prompt and returns Claude's response as text. Your function will package each business's details into a structured prompt that tells Claude exactly what kind of email to write.
- In lead_generator.py, scroll to the bottom of the file (below the search_nearby_businesses function).
- Add the following function:
def generate_outreach_email(business):
"""Generate a personalized outreach email using Claude AI."""
business_type = business["primary_type"].replace("_", " ")
website_info = f"Website: {business['website']}" if business["website"] else "No website found"
prompt = f"""You are a business automation consultant writing a cold outreach email.
Business details:
- Name: {business['name']}
- Type: {business_type}
- Address: {business['address']}
- {website_info}
Write a short, personalized cold email (under 150 words) to this business offering automation services.
Based on their business type, suggest 2-3 specific automations that would help them. For example:
- Restaurants: online ordering, reservation confirmations, review response automation
- Dental offices: appointment reminders, patient follow-ups, insurance verification
- Gyms/fitness: membership onboarding, class booking, payment reminders
- Salons: appointment scheduling, no-show follow-ups, loyalty program automation
- Retail stores: inventory alerts, customer re-engagement emails, order tracking
Adapt your suggestions to match their specific business type. Be concise, professional, and focus on the pain points they likely experience.
Sign the email as "Alex" from "AutoFlow Solutions"."""
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
What does this code do?
This function takes a single business dictionary (the same shape returned by search_nearby_businesses) and turns it into a personalized email.
- business["primary_type"].replace("_", " ") converts Google's type format (like dental_clinic) into readable text (dental clinic).
- The prompt variable is a formatted string that gives Claude the business context and specific instructions. The examples in the prompt teach Claude what kinds of automation to suggest for each business type.
- client.messages.create sends the prompt to claude-haiku-4-5. This is the fastest and cheapest Claude model at $1 per million input tokens.
- message.content[0].text extracts the generated email text from Claude's response object.
Why prompt engineering matters here
The prompt includes example automations for several business types. This technique is called few-shot prompting. It teaches Claude the pattern you expect without hard-coding every possible business category.
Claude reads the business type, matches it against the examples, and generalizes to types not explicitly listed. A florist would get suggestions about order tracking and delivery confirmations even though florists are not in the example list.
- Save lead_generator.py.
Test the email generation with sample data
You need to confirm that Claude produces a relevant, professional email that adapts to the business type you pass in.
- Test the function by running this command in your terminal:
python3 -c "
from lead_generator import generate_outreach_email
sample_business = {
'name': 'Sunrise Bakery',
'primary_type': 'bakery',
'address': '123 Main St, San Francisco, CA',
'website': 'https://sunrisebakery.com',
'phone': '555-0123'
}
print(generate_outreach_email(sample_business))
"
What should I see?
You should see a professional cold email printed in your terminal. The email should mention the business name (Sunrise Bakery), reference its type (bakery), and suggest 2-3 specific automations relevant to a bakery. It should be signed by "Alex" from "AutoFlow Solutions".
Each time you run this, the email will be slightly different because Claude generates fresh content. The suggestions should always be relevant to the business type you pass in.
Not seeing an email output?
- Check that your .env file has a valid Anthropic API key set for ANTHROPIC_API_KEY. The key should start with sk-ant-.
- Make sure you saved lead_generator.py before running the test command. Python imports the file from disk, not from your editor's buffer.
- If you see an authentication error, confirm your Anthropic account has API credits available at console.anthropic.com.
Help me debug my Claude API call
✔️ Awesome, I've got everything!
Great. Double check you have saved lead_generator.py before moving on.
ⓧ I'd like to double check the full code
Here is the full lead_generator.py at this point:
import os
import csv
import time
import requests
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_PLACES_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
client = Anthropic(api_key=ANTHROPIC_API_KEY)
def search_nearby_businesses(latitude, longitude, radius, business_type, max_results=10):
"""Search for businesses near a location using Google Places API (New)."""
url = "https://places.googleapis.com/v1/places:searchNearby"
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.types,places.websiteUri,places.nationalPhoneNumber,places.primaryType"
}
payload = {
"includedTypes": [business_type],
"maxResultCount": max_results,
"locationRestriction": {
"circle": {
"center": {
"latitude": latitude,
"longitude": longitude
},
"radius": radius
}
}
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
businesses = []
for place in data.get("places", []):
business = {
"name": place.get("displayName", {}).get("text", "Unknown"),
"address": place.get("formattedAddress", "No address"),
"types": place.get("types", []),
"primary_type": place.get("primaryType", "business"),
"website": place.get("websiteUri", ""),
"phone": place.get("nationalPhoneNumber", "")
}
businesses.append(business)
return businesses
def generate_outreach_email(business):
"""Generate a personalized outreach email using Claude AI."""
business_type = business["primary_type"].replace("_", " ")
website_info = f"Website: {business['website']}" if business["website"] else "No website found"
prompt = f"""You are a business automation consultant writing a cold outreach email.
Business details:
- Name: {business['name']}
- Type: {business_type}
- Address: {business['address']}
- {website_info}
Write a short, personalized cold email (under 150 words) to this business offering automation services.
Based on their business type, suggest 2-3 specific automations that would help them. For example:
- Restaurants: online ordering, reservation confirmations, review response automation
- Dental offices: appointment reminders, patient follow-ups, insurance verification
- Gyms/fitness: membership onboarding, class booking, payment reminders
- Salons: appointment scheduling, no-show follow-ups, loyalty program automation
- Retail stores: inventory alerts, customer re-engagement emails, order tracking
Adapt your suggestions to match their specific business type. Be concise, professional, and focus on the pain points they likely experience.
Sign the email as "Alex" from "AutoFlow Solutions"."""
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
You now have an AI-powered email writer that adapts to any business type. Next up, you will wire both functions together into a complete pipeline and export everything to a CSV file.
Build the Complete Pipeline and Export to CSV
You have two powerful functions ready to go. One finds businesses via Google Places, and the other generates personalized emails via Claude. Now it's time to connect them into a single pipeline that runs end-to-end.
In this step, you'll write a CSV export function, wire everything together in a main() function, and run the complete pipeline to produce a ready-to-use lead file.
In this step, get ready to:
- Write a CSV export function that saves lead data with generated emails.
- Build a main pipeline that searches for businesses, generates emails for each, and exports results.
- Run the full script and open your leads file in a spreadsheet.
Write the CSV export function
Before you build the main pipeline, you need a function that takes your list of leads and writes them out to a structured file. CSV (comma-separated values) is perfect here because any spreadsheet app can open it.
- In lead_generator.py, scroll to the bottom of the file (below the generate_outreach_email function) and add the following function:
def export_to_csv(leads, filename="leads.csv"):
"""Export leads with generated emails to a CSV file."""
fieldnames = ["business_name", "business_type", "address", "website", "phone", "generated_email"]
with open(filename, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for lead in leads:
writer.writerow({
"business_name": lead["name"],
"business_type": lead["primary_type"].replace("_", " "),
"address": lead["address"],
"website": lead["website"],
"phone": lead["phone"],
"generated_email": lead["email"]
})
print(f"Exported {len(leads)} leads to {filename}")
What does this code do?
- csv.DictWriter lets you write rows using dictionary keys instead of manually tracking column positions. It maps each key in your dictionary to the correct CSV column.
- newline="" prevents Python from adding extra blank lines between rows on Windows. The csv module handles line endings itself.
- The function remaps field names (like lead["name"] to business_name) so the CSV has clean, readable column headers.
- Save lead_generator.py.
You won't see a change yet. The next function will call export_to_csv as the final step of the pipeline.
Wire the pipeline together in main()
The main() function is where everything connects. It searches for businesses, loops through the results to generate an email for each one, and then exports the combined data to CSV.
- Below the export_to_csv function, add the main() function and the entry point guard:
def main():
latitude = 37.7937
longitude = -122.3965
radius = 1000.0
business_type = "restaurant"
max_results = 5
print(f"Searching for {business_type}s near ({latitude}, {longitude})...")
businesses = search_nearby_businesses(latitude, longitude, radius, business_type, max_results)
print(f"Found {len(businesses)} businesses.\n")
leads = []
for i, business in enumerate(businesses):
print(f"Generating email for {business['name']}...")
email = generate_outreach_email(business)
business["email"] = email
leads.append(business)
if i < len(businesses) - 1:
time.sleep(1)
export_to_csv(leads)
print("\nDone! Open leads.csv to review your generated outreach emails.")
if __name__ == "__main__":
main()
What does this code do?
- The configuration variables at the top (latitude, longitude, radius, business_type, max_results) make it easy to change your search without editing the function logic.
- time.sleep(1) pauses for one second between Claude API calls. This is polite rate limiting that prevents your script from overwhelming the API.
- The if __name__ == "__main__": guard ensures main() only runs when you execute the script directly, not when you import it as a module.
The default coordinates (37.7937, -122.3965) point to the Embarcadero area in San Francisco. You can change these to any location you want to search.
- Save lead_generator.py.
Seeing a syntax error on save?
- Check that def main(): is at the same indentation level as your other function definitions (no extra spaces).
- Make sure the if __name__ block is at the very bottom of the file, not indented inside another function.
Why is my main() function throwing a syntax error?
Run the complete pipeline
Everything is wired up. Time to run the full script and generate your first batch of leads with personalized AI emails.
- Run the pipeline by executing this command in your terminal:
python3 lead_generator.py
The script takes about 10-15 seconds to complete because it pauses between each email generation call.
- Confirm that leads.csv was created by running:
ls
You should see leads.csv listed alongside lead_generator.py and .env. Open leads.csv in any spreadsheet application (Google Sheets, Excel, Numbers) to see your results. Each row contains a real business with a personalized AI-generated email.
Script errors out immediately?
- If you see a KeyError mentioning email, make sure the line business["email"] = email appears inside your loop before leads.append(business).
- If you see an authentication error, double-check that your .env file has valid API keys for both Google Places and Anthropic.
- If the script hangs or returns zero businesses, try changing radius to 2000.0 to widen the search area.
Help me debug my lead generator script
✔️ Awesome, I've got everything!
Great. Double check you've saved lead_generator.py and confirmed your leads.csv file is generated.
ⓧ I'd like to double check the full code
Here is the complete lead_generator.py file. Compare yours against it:
import os
import csv
import time
import requests
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_PLACES_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
client = Anthropic(api_key=ANTHROPIC_API_KEY)
def search_nearby_businesses(latitude, longitude, radius, business_type, max_results=10):
"""Search for businesses near a location using Google Places API (New)."""
url = "https://places.googleapis.com/v1/places:searchNearby"
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.types,places.websiteUri,places.nationalPhoneNumber,places.primaryType"
}
payload = {
"includedTypes": [business_type],
"maxResultCount": max_results,
"locationRestriction": {
"circle": {
"center": {
"latitude": latitude,
"longitude": longitude
},
"radius": radius
}
}
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
businesses = []
for place in data.get("places", []):
business = {
"name": place.get("displayName", {}).get("text", "Unknown"),
"address": place.get("formattedAddress", "No address"),
"types": place.get("types", []),
"primary_type": place.get("primaryType", "business"),
"website": place.get("websiteUri", ""),
"phone": place.get("nationalPhoneNumber", "")
}
businesses.append(business)
return businesses
def generate_outreach_email(business):
"""Generate a personalized outreach email using Claude AI."""
business_type = business["primary_type"].replace("_", " ")
website_info = f"Website: {business['website']}" if business["website"] else "No website found"
prompt = f"""You are a business automation consultant writing a cold outreach email.
Business details:
- Name: {business['name']}
- Type: {business_type}
- Address: {business['address']}
- {website_info}
Write a short, personalized cold email (under 150 words) to this business offering automation services.
Based on their business type, suggest 2-3 specific automations that would help them. For example:
- Restaurants: online ordering, reservation confirmations, review response automation
- Dental offices: appointment reminders, patient follow-ups, insurance verification
- Gyms/fitness: membership onboarding, class booking, payment reminders
- Salons: appointment scheduling, no-show follow-ups, loyalty program automation
- Retail stores: inventory alerts, customer re-engagement emails, order tracking
Adapt your suggestions to match their specific business type. Be concise, professional, and focus on the pain points they likely experience.
Sign the email as "Alex" from "AutoFlow Solutions"."""
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def export_to_csv(leads, filename="leads.csv"):
"""Export leads with generated emails to a CSV file."""
fieldnames = ["business_name", "business_type", "address", "website", "phone", "generated_email"]
with open(filename, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for lead in leads:
writer.writerow({
"business_name": lead["name"],
"business_type": lead["primary_type"].replace("_", " "),
"address": lead["address"],
"website": lead["website"],
"phone": lead["phone"],
"generated_email": lead["email"]
})
print(f"Exported {len(leads)} leads to {filename}")
def main():
latitude = 37.7937
longitude = -122.3965
radius = 1000.0
business_type = "restaurant"
max_results = 5
print(f"Searching for {business_type}s near ({latitude}, {longitude})...")
businesses = search_nearby_businesses(latitude, longitude, radius, business_type, max_results)
print(f"Found {len(businesses)} businesses.\n")
leads = []
for i, business in enumerate(businesses):
print(f"Generating email for {business['name']}...")
email = generate_outreach_email(business)
business["email"] = email
leads.append(business)
if i < len(businesses) - 1:
time.sleep(1)
export_to_csv(leads)
print("\nDone! Open leads.csv to review your generated outreach emails.")
if __name__ == "__main__":
main()
Your lead generation pipeline is complete. You now have a reusable tool that finds real businesses and writes personalized outreach emails for each one. Next up, the Secret Mission takes this further with website scraping for hyper-personalized emails.
Secret mission
Add Website Scraping for Hyper-Personalized Emails
Your current emails are personalized by business type, but what if they could reference the actual services listed on a business's website? In this mission, you'll add a website scraper that feeds real homepage content into Claude's prompt for hyper-personalized outreach.
Clean Up Your Resources
Clean Up Your Resources
Both Google Cloud and Anthropic are pay-per-use services with no idle charges. You only incur costs when you actively run the script.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Google Cloud project with Places API enabled.
- Anthropic API key (used for Claude Haiku email generation).
- Local lead-generator project folder (contains lead_generator.py, .env, and leads.csv).
Keep everything running
No action needed. Choose this if you plan to keep iterating on your lead generator or reusing it for different locations and business types.
- Both Google Cloud and Anthropic are pay-per-use. You will not be charged while the script is idle.
- Set up a billing alert in Google Cloud Console to avoid surprise charges if you run many searches in the future.
- Your local files and installed Python packages remain on your machine.
Pause - I'll come back to this later
Free up space while keeping your accounts intact for later. Since both APIs are pay-per-use with no idle charges, pausing is mostly about local cleanup.
- No action needed on Google Cloud or Anthropic. Neither service charges you while idle.
- Optionally delete leads.csv if it contains business data you no longer need.
Delete - I don't want to use this again
Remove all project resources and revoke API access to start completely fresh.
- Delete your Google Cloud project. In the Google Cloud Console, go to the Manage resources page. Select your project, click Delete, enter the project ID, and click Shut down.
- Disable your Anthropic API key. Go to console.anthropic.com, navigate to the API Keys page, and disable or delete the key you created for this project.
- Delete your local project folder by running this command:
cd ~/Desktop
rm -rf lead-generator
- Uninstall the Python packages you installed for this project by running:
pip3 uninstall requests anthropic python-dotenv -y
Nice Work!
Nice Work!
Nice work! You just built a complete lead generation pipeline that finds real businesses via Google Maps and writes personalized outreach emails using AI.
You've learned how to:
- Query the Google Places API (New) Nearby Search endpoint to find businesses by location and type, extracting contact details like name, address, website, and phone number.
- Integrate Claude AI to generate personalized cold outreach emails that adapt their automation suggestions to each business type.
- Export a complete lead list with AI-generated emails to a reviewable CSV file using Python's built-in csv module.
- Secret Mission: Added website scraping to produce hyper-personalized emails based on actual business content.
Ready to quiz yourself?