An AI-assisted lead-discovery system that finds local US businesses with strong Google reviews but no real website, scores them by sales potential, writes a personalized WhatsApp message for each, and emails you the best leads every day — ready for you to copy, paste, and send by hand.
It does the boring part (finding and qualifying prospects). You do the human part (sending the message and closing the deal).
- What it does
- What it does NOT do
- How it works
- Why it's smarter than a basic scraper
- Tech stack
- Prerequisites
- Step-by-step setup
- Running it
- Configuration reference
- API endpoints
- Project structure
- Troubleshooting
- Costs
- Deploying for daily automation
- Security notes
- FAQ
Every time it runs, the system:
- Searches Google Maps for a business type across your chosen cities (e.g.
plumber in Frisco Texas). - Collects each business's name, phone, website, rating, review count, category, and Maps link.
- Filters down to genuine opportunities: strong reviews, a phone number, and no real website.
- Scores and ranks them so the hottest prospects rise to the top.
- Writes a personalized WhatsApp message for each top lead using AI — friendly, human, no spammy buzzwords.
- Saves the leads to your database (and a local backup file).
- Emails you a clean daily report with everything you need to reach out.
You open the email, copy a message, and send it manually over WhatsApp. That's it.
This is not an autonomous sales bot. By design it does not:
- ❌ Send WhatsApp messages automatically
- ❌ Use Twilio or any WhatsApp API
- ❌ Reply to or manage conversations
- ❌ Include a CRM, login system, or web dashboard
It finds and prepares leads. You stay in control of every message that goes out. This keeps it simple, cheap, reliable, and compliant.
┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐
│ Apify │──▶│ Filter │──▶│ Score │──▶│ OpenAI │──▶│ Save │──▶│ Email │
│ (scrape │ │ (no site, │ │ & rank │ │ (analysis │ │ (DB + │ │ (daily │
│ Google │ │ reviews, │ │ best │ │ + WhatsA │ │ local │ │ report │
│ Maps) │ │ phone) │ │ first) │ │ message) │ │ backup) │ │ to you)│
└──────────┘ └───────────┘ └──────────┘ └───────────┘ └──────────┘ └─────────┘
Each step is defensive: if one external service has a hiccup, the run keeps going and tells you what happened. AI only runs on leads that already passed filtering, so you never waste money analyzing businesses you'd reject anyway.
A naive tool just checks "is the website field empty?" This one is built to find better leads:
- Detects fake/weak web presence. A business whose only "website" is a Facebook page, Instagram profile, Yelp listing, or a Google auto-generated
business.sitepage still has no real, ownable website — and is a perfect prospect. The system treats these as "no website" instead of skipping them. - Ranks by real sales signals. Instead of a flat list, leads are scored using review volume, rating tiers, and review velocity (how actively a business collects reviews) — surfacing the prospects most likely to buy.
- Writes genuinely human outreach. Messages reference the business by name and its actual reputation, so the recipient feels "this person actually looked at my business" — not a mass blast.
- Never loses your work. Every run writes a local backup (
reports/latest.html) before touching the database or email, so leads survive even if a service fails.
| Layer | Technology | Why |
|---|---|---|
| Backend | FastAPI (Python 3.12) | Fast, modern, auto-generated API docs |
| Lead source | Apify Google Maps Scraper | Reliable Google Maps data without fighting Google directly |
| AI | OpenAI (gpt-4o-mini) |
Cheap and great at short, human-sounding copy |
| Database | Supabase (PostgreSQL) | Free tier, simple, hosted |
| Resend | Developer-friendly email delivery | |
| Deployment | Docker / Azure Container Apps | Run it anywhere; schedule a daily job |
Before you start, make sure you have:
- A computer with Windows, macOS, or Linux.
- Python 3.11 or 3.12 installed. Check with
python --version. (Download Python) - About 20 minutes to create the four free accounts below.
- A credit/debit card for Apify and OpenAI (both have free credit to start; you only pay for what you use, which is cents per run).
No prior coding experience is required to run it — just follow the steps carefully.
Download this folder (or clone it from GitHub) to your computer. Open a terminal inside the project folder (the one containing requirements.txt).
Windows tip: open the folder in File Explorer, click the address bar, type
cmd, and press Enter — a terminal opens in that folder.
Create an isolated environment and install the libraries:
Windows:
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txtmacOS / Linux:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtWhen it finishes, you're ready to add your keys.
You need four services. All have free tiers. Take your time and paste each key somewhere safe (you'll put them in the .env file next).
- Go to https://platform.openai.com/signup and create an account.
- Add a small amount of credit under Settings → Billing (e.g. $5 lasts a long time for this).
- Go to https://platform.openai.com/api-keys → Create new secret key.
- Copy the key (starts with
sk-...). You won't be able to see it again, so save it now.
→ This is your OPENAI_API_KEY.
- Go to https://supabase.com → Start your project and sign up (free).
- Click New project. Give it a name, set a database password, pick a region near you, and create it. Wait ~2 minutes for it to provision.
- In the project, click the ⚙️ Project Settings (gear icon) → API.
- Copy two values:
- Project URL (looks like
https://abcdxyz.supabase.co) → this is yourSUPABASE_URL. - Under Project API keys, the
service_rolekey (click Reveal) → this is yourSUPABASE_SERVICE_ROLE_KEY.
- Project URL (looks like
⚠️ Theservice_rolekey is powerful (it bypasses security rules). Keep it secret. Never put it in a website or share it publicly.
- Go to https://apify.com and sign up (free trial credit included).
- Click your profile → Settings → API & Integrations (or go to https://console.apify.com/settings/integrations).
- Copy your Personal API token (starts with
apify_api_...).
→ This is your APIFY_API_TOKEN.
The scraper used is the popular "Google Maps Scraper" by Compass (
compass~crawler-google-places). You don't need to install anything — the system calls it for you.
- Go to https://resend.com and sign up (free).
- Go to https://resend.com/api-keys → Create API Key → copy it (starts with
re_...).
→ This is your RESEND_API_KEY.
Important about the "from" and "to" addresses: On the free plan without a verified domain, Resend will only deliver emails to the same email address you signed up with, and you must send from
onboarding@resend.dev. So setNOTIFICATION_EMAILto the exact email you used to register at Resend. Later, to send to any address (or use your own brand), verify a domain at https://resend.com/domains and changeEMAIL_FROMto an address on that domain.
The project includes a template called .env.example. Make a copy named .env and fill in the keys you just collected.
Windows:
copy .env.example .envmacOS / Linux:
cp .env.example .envNow open .env in any text editor (Notepad is fine) and fill it in:
# --- OpenAI ---
OPENAI_API_KEY=sk-your-openai-key-here
OPENAI_MODEL=gpt-4o-mini
# --- Supabase ---
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
# --- Apify ---
APIFY_API_TOKEN=apify_api_your-token-here
APIFY_GOOGLE_MAPS_ACTOR=compass~crawler-google-places
# --- Resend (email) ---
RESEND_API_KEY=re_your-resend-key-here
EMAIL_FROM=Website Lead Hunter <onboarding@resend.dev>
NOTIFICATION_EMAIL=the-email-you-signed-up-to-resend-with@gmail.com
# --- What to search for ---
TARGET_NICHE=plumber
TARGET_CITIES=Frisco,McKinney,Denton,Tyler,Waco,Lubbock,Amarillo
TARGET_REGION=Texas
# --- Quality thresholds ---
MIN_RATING=4.5
MIN_REVIEWS=50
# --- Limits (control cost) ---
MAX_RESULTS_PER_CITY=80
MAX_LEADS_PER_RUN=100
TOP_LEADS_IN_EMAIL=25
# --- Your name (signs the WhatsApp messages) ---
SENDER_NAME=Akash
LOG_LEVEL=INFO🔒 The
.envfile holds your secrets and is never uploaded to GitHub (it's in.gitignore). Only.env.example(with no real keys) is shared.
The system needs one table in Supabase to store leads.
- In your Supabase project, click SQL Editor in the left sidebar → New query.
- Open the file
scripts/schema.sqlfrom this project, copy all of it, and paste it into the editor. - Click Run.
You should see "Success. No rows returned." The table is ready.
Already created the table before and seeing a "column not found" error? Run
scripts/migration_fix_columns.sqlinstead — it safely adds any missing columns to an existing table.
Run these two quick checks before doing a full (paid) run:
a) Check your config loads and email delivery works:
python -m scripts.test_emailThis sends one test email. Look for SUCCESS: test email sent and check your inbox (and spam). If it fails, the error message tells you exactly what's wrong (usually the Resend "to" address — see Troubleshooting).
b) (Optional) Run the automated tests:
pytest -qAll tests should pass. These check the filtering, scoring, and pipeline logic without using any paid services.
You're ready. There are two ways to run a full cycle (search → filter → score → AI → email).
python -m scripts.run_onceThis is the simplest way and the one you'd put on a daily schedule. It runs the whole pipeline, prints a summary, saves a local backup to reports/latest.html, and emails you the top leads.
⏱ The Apify scrape is the slow part — a full run across several cities can take a few minutes. That's normal.
uvicorn app.main:app --reloadThen open in your browser:
- http://localhost:8000/health — confirms your configuration is OK
- http://localhost:8000/docs — interactive page where you can click POST
/agent/run - http://localhost:8000/leads/top — view your best stored leads
If a run scraped successfully but the email or database failed, you can reprocess the last Apify scrape for free (no new scraping cost):
python -m scripts.reprocess_lastEverything is controlled from the .env file — no code changes needed to retarget the system (e.g. switch from plumbers to electricians, or change cities).
| Variable | Example | What it does |
|---|---|---|
TARGET_NICHE |
plumber |
The type of business to search for |
TARGET_CITIES |
Frisco,McKinney,Denton |
Comma-separated cities to search |
TARGET_REGION |
Texas |
State/region added to each search |
MIN_RATING |
4.5 |
Minimum Google star rating to keep |
MIN_REVIEWS |
50 |
Minimum number of reviews to keep |
MAX_RESULTS_PER_CITY |
80 |
Cap on businesses scraped per city (controls cost) |
MAX_LEADS_PER_RUN |
100 |
Max qualified leads kept per run |
TOP_LEADS_IN_EMAIL |
25 |
How many top leads go in the email + get AI messages |
OPENAI_MODEL |
gpt-4o-mini |
The AI model (cheap and effective) |
SENDER_NAME |
Akash |
Your name, used to sign the messages |
EMAIL_FROM |
... <onboarding@resend.dev> |
The email "from" address |
NOTIFICATION_EMAIL |
you@gmail.com |
Where the daily report is sent |
LOG_LEVEL |
INFO |
How much detail to log |
Want to target a different business? Just edit, for example:
TARGET_NICHE=roofer
TARGET_CITIES=Austin,Dallas,Houston
TARGET_REGION=Texas…and run again. No code changes.
When running the server (uvicorn app.main:app):
| Method | Path | Description |
|---|---|---|
POST |
/agent/run |
Run the full workflow and email the report |
GET |
/leads |
List all stored leads (newest first) |
GET |
/leads/top |
List the highest-scoring leads |
GET |
/health |
Check the service is up and configured |
GET |
/docs |
Interactive API documentation |
website-lead-hunter/
├── app/
│ ├── api/routes.py # The web endpoints
│ ├── agents/lead_hunter.py # The main workflow (ties everything together)
│ ├── services/
│ │ ├── apify_service.py # Scrapes Google Maps
│ │ ├── scoring_service.py # Filters + scores + ranks leads
│ │ ├── openai_service.py # Writes analysis + WhatsApp messages
│ │ ├── email_service.py # Builds + sends the daily email
│ │ └── report_store.py # Local backup of every run
│ ├── database/supabase_client.py# Saves/reads leads in Supabase
│ ├── models/business.py # Data shapes for a business / lead
│ ├── schemas/api.py # API request/response shapes
│ ├── utils/ # Logging + data cleaning helpers
│ ├── config.py # Reads all settings from .env
│ └── main.py # App entry point
├── scripts/
│ ├── schema.sql # Run this once to create the DB table
│ ├── migration_fix_columns.sql # Fixes an older/incomplete table
│ ├── run_once.py # Run the whole pipeline once
│ ├── reprocess_last.py # Re-run on the last scrape (no Apify cost)
│ └── test_email.py # Send a test email to check setup
├── tests/ # Automated tests (no paid services used)
├── .env.example # Template for your secrets (copy to .env)
├── requirements.txt # Python dependencies
├── Dockerfile # For containerized deployment
└── README.md # This file
"Resend rejected the email (HTTP 403): You can only send testing emails to your own email address"
On Resend's free plan you can only email the address you signed up with. Set NOTIFICATION_EMAIL to your exact Resend signup email. To email other addresses, verify a domain at https://resend.com/domains and update EMAIL_FROM.
"Could not find the 'ai_analysis' column of 'businesses' in the schema cache"
Your database table is missing columns. Run scripts/migration_fix_columns.sql in the Supabase SQL Editor.
"Missing required configuration: ..." when running
One or more keys in .env are empty. Open .env and fill in the listed variables. Run python -m scripts.test_email or open /health to check.
The run found lots of businesses but qualified very few
That's normal and good — it means most businesses already have websites. Lower MIN_REVIEWS (e.g. to 25) or add more cities to find more "no website" prospects.
pip install fails or python isn't recognized
Make sure Python 3.11+ is installed and added to your PATH. On Windows, re-run the installer and tick "Add Python to PATH".
The scrape is taking several minutes
Expected. Scraping multiple cities on Google Maps takes time. Lower MAX_RESULTS_PER_CITY or use fewer cities to speed it up.
All four services have free tiers, and ongoing usage is small:
- Apify — billed per scrape; a typical multi-city run is a few cents to a couple of dollars depending on result count. The free trial credit covers many runs.
- OpenAI — only the top leads get an AI message. With
gpt-4o-mini, a daily run is typically well under 1 cent. - Supabase — free tier is plenty for storing leads.
- Resend — free tier covers a daily email easily.
Tip: keep
MAX_RESULTS_PER_CITYandTOP_LEADS_IN_EMAILmodest to keep costs low while you're testing.
Running it once a day by hand is totally fine. To fully automate it:
Easiest (your own computer): use a scheduler.
- Windows: Task Scheduler → create a task that runs
python -m scripts.run_oncedaily in this folder. - macOS / Linux: add a
cronjob, e.g.0 8 * * * cd /path/to/project && .venv/bin/python -m scripts.run_once.
Cloud (always-on): the included Dockerfile builds a container you can deploy to Azure Container Apps, Google Cloud Run, Railway, Render, etc. Set the same environment variables as secrets, then either:
- run it as a scheduled job that executes
python -m scripts.run_onceonce a day, or - keep the web app running and have a scheduler call
POST /agent/rundaily.
- Never commit your
.envfile. It's already in.gitignore. Only.env.example(no real keys) is shared. - The Supabase
service_rolekey bypasses database security rules — keep it server-side only, never in a browser or public repo. - If you ever expose a public API on top of this, enable Supabase Row Level Security with explicit policies.
- All scraped data is treated as untrusted and cleaned before use.
- If you accidentally pushed a key to GitHub, rotate it immediately in the relevant dashboard.
Does this send WhatsApp messages for me? No. It only writes the messages and emails them to you. You send them manually. That's intentional — it keeps you in control and avoids spam/automation risks.
Can I use it for a different industry or country?
Yes. Change TARGET_NICHE, TARGET_CITIES, and TARGET_REGION in .env. No code changes needed.
Will it message the same business twice? The system de-duplicates businesses and stores them, so you can track who you've already found. (Tracking replies is out of scope — you manage conversations yourself.)
What if a service goes down mid-run?
The run keeps going and logs what failed. Your leads are always saved locally to reports/latest.html first, so nothing is lost. You can also re-run the last scrape for free with scripts/reprocess_last.py.
Is any of my data sent to third parties? Only what's required: search terms go to Apify, lead details go to OpenAI to write messages, leads are stored in your Supabase, and the report is sent via Resend. Nothing else.
This project is provided as-is for personal and commercial use. Add your preferred license here (e.g. MIT) before publishing.
Built to find better leads, not just more leads. 🚀