Find businesses without websites. Generate personalized outreach. Automate via email.
A complete lead generation platform that finds local businesses lacking an online presence, crafts targeted cold emails using smart templates, and automates delivery through Google Sheets + Apps Script.
Features β’ Quick Start β’ Deployment β’ Usage β’ API Reference β’ FAQ
- Google Places API (New) β Search any area, filter by category and rating, no scraping
- No-Website Detection β Automatically filters businesses that already have a website
- Smart Email Generation β 3 category-specific angles per vertical (restaurant, clinic, retail, generic) with rotating templates and varied subject lines
- Google Sheets Export β One-click export with all leads, generated emails, and status tracking
- Apps Script Automation β Copy-paste script sends batch emails from your Gmail (8 per 20 min, designed with opt-out footer)
- Duplicate Prevention β Dedup by Google Place ID across all campaigns
- Background Processing β Search runs asynchronously; poll campaign status from the dashboard
- CI/CD β GitHub Actions runs backend + frontend tests on every push
ββββββββββββββββββββββββββββββββββββββββ
β Vercel (Frontend) β
β Next.js 14 Dashboard β
ββββββββββββββββββ¬ββββββββββββββββββββββ
β /api/*
ββββββββββββββββββΌββββββββββββββββββββββ
β Railway.app (Backend) β
β FastAPI (Python) β
ββββββββββββββββββββββββββββββββββββββββ€
β ββββββββββββββββββββββββββββββββββββ β
β β Postgres (Leads + Campaigns) β β
β ββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββ¬ββββββββββββββββββββββ
β HTTP
ββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
Google Places API Google Sheets API Google Apps Script
(Lead sourcing) (Export) (Email sending)
Search Form β POST /api/search β Grid Search (Places API)
β Filter businesses WITHOUT websites β Dedup by place_id
β Generate personalized emails (Angle Bank template engine)
β Export to Google Sheet
β User pastes Apps Script β Automated batch emailing via Gmail
| Layer | Technology |
|---|---|
| Backend | Python 3.11+, FastAPI, SQLAlchemy 2.0 (async), PostgreSQL |
| Frontend | Next.js 14, React 18, Tailwind CSS, Framer Motion |
| Background Tasks | FastAPI BackgroundTasks (async) |
| APIs | Google Places API (New), Google Sheets API v4 |
| Automation | Google Apps Script (Gmail send, quota-aware) |
| Infrastructure | Railway.app, Vercel, Docker Compose (local dev) |
| CI/CD | GitHub Actions (backend + frontend tests on push) |
| Testing | pytest (backend), Jest + RTL (frontend) |
In Google Cloud Console, create a project and enable:
- Places API (New) β for lead sourcing
- Sheets API β for spreadsheet export
Create an API key (for Places API) and a Service Account JSON key (for Sheets API).
Or manually:
- Fork/clone this repo
- Create a Railway project β Deploy from GitHub repo
- Add PostgreSQL plugin β auto-links
DATABASE_URL - Set env vars:
GOOGLE_MAPS_API_KEY=your_places_api_key
GOOGLE_SERVICE_ACCOUNT_JSON=your_base64_or_raw_json
API_KEY=your_secret_key # optional β set to enable X-API-Key authSet env vars in Vercel:
NEXT_PUBLIC_API_URL=https://your-backend.up.railway.app
NEXT_PUBLIC_API_KEY=your_secret_key # only if API_KEY is set on backendgit clone https://github.com/abeermeer/LeadForge.git
cd LeadForge
docker compose up -d # starts Postgres
cd backend && pip install -r requirements.txt
cp .env.example .env # fill in your keys
uvicorn backend.main:app --reload --port 8000 # terminal 1
cd ../frontend && npm install
npm run dev # terminal 2 β http://localhost:3000cd frontend
npm run build
vercel --prodSet NEXT_PUBLIC_API_URL to your Railway backend URL (e.g. https://leadforge-production-xxxx.up.railway.app).
- Push to GitHub
- Create a Railway project β "Deploy from GitHub repo" β select
abeermeer/LeadForge - Add PostgreSQL plugin (DATABASE_URL auto-injected)
- Set remaining env vars:
GOOGLE_MAPS_API_KEY,GOOGLE_SERVICE_ACCOUNT_JSON,API_KEY - Railway builds using the
Dockerfileat repo root
The included railway.json configures Dockerfile builder with 1 replica and ON_FAILURE restart policy.
Enter a business type (e.g. "restaurant", "dentist", "plumber") and location (e.g. "Austin, TX"). Adjust radius and minimum rating. The engine:
- Subdivides the area into overlapping circles (Places API cap: 60 results per query)
- Searches each circle
- Filters out businesses with a listed website
- Deduplicates against all previous campaigns
The results table shows each lead with its generated email. You can edit subject lines and body text inline. Each lead gets a personalized email from a rotating angle bank:
| Vertical | Angles |
|---|---|
| Restaurants / Cafes | Lost traffic, competition edge, delivery trends |
| Clinics / Salons | Trust building, online booking, credibility |
| Retail / Shops | Google visibility, Instagram limits, product catalog |
| Generic | Missed opportunity, competitor advantage, credibility gap |
Click "Export to Sheets" to create a new Google Sheet with all leads, generated emails, and status tracking. The sheet includes columns for tracking send status (draft β generated β sent β failed).
- Open the exported Google Sheet
- Extensions β Apps Script
- Delete default code, paste
scripts/apps_script.gs - Set
SENDER_NAMEto your name - Run
setupTrigger()once
Emails send in batches of 8 every 20 minutes, respecting Gmail's 100/day quota (free accounts). Upgrade to Google Workspace for higher limits.
All endpoints (except /api/health) require X-API-Key header if API_KEY is set. POST /api/search is rate-limited to 5 requests per minute.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check (no auth required) |
POST |
/api/search |
Create search campaign (rate-limited: 5/min) |
GET |
/api/search/{id} |
Poll campaign status |
GET |
/api/campaigns/{id}/leads |
List leads for campaign |
PATCH |
/api/leads/{id} |
Update lead email fields |
POST |
/api/campaigns/{id}/export |
Export to Google Sheets |
{
"query": "pizza",
"location": "New York, NY",
"lat": 40.7128,
"lng": -74.006,
"radius": 50000,
"min_rating": 3.5
}Returns {"campaign_id": 1, "status": "pending"}.
Allowlisted fields: email, email_subject, email_body, angle_used, email_status.
| Variable | Description | Required |
|---|---|---|
GOOGLE_MAPS_API_KEY |
Google Places API (New) key | β |
GOOGLE_SERVICE_ACCOUNT_JSON |
Service account key (base64 or raw JSON) | β |
API_KEY |
API key for auth (set to enable) | β |
DATABASE_URL |
PostgreSQL with async driver | β |
NEXT_PUBLIC_API_URL |
Backend URL (frontend only) | β |
NEXT_PUBLIC_API_KEY |
Must match API_KEY for frontend auth (frontend only) |
β |
LeadForge/
βββ backend/
β βββ main.py # FastAPI entry point
β βββ config.py # Environment config
β βββ auth.py # API key authentication + client IP
β βββ database.py # SQLAlchemy async engine
β βββ models/
β β βββ campaign.py # Campaign + CampaignStatus (PENDING/RUNNING/COMPLETED/FAILED)
β β βββ lead.py # Lead table
β β βββ audit_log.py # Audit log table
β βββ routers/
β β βββ search.py # POST /api/search, GET /api/search/{id}, GET /api/campaigns
β β βββ leads.py # GET/PATCH leads, POST regenerate-email
β β βββ export.py # POST /api/campaigns/{id}/export
β βββ workers/
β β βββ grid_search.py # Geocoding + Places API grid search + dedup
β β βββ email_writer.py # Angle bank template engine
β β βββ sheet_exporter.py # Google Sheets export
β βββ templates/ # Angle bank JSON files (restaurant, clinic, retail, generic)
β βββ tests/ # pytest (12 tests)
βββ frontend/
β βββ pages/
β β βββ index.tsx # Search form + polling
β β βββ campaigns/index.tsx # Campaign history list
β β βββ campaigns/[id].tsx # Campaign detail + results
β β βββ export/[id].tsx # Sheets export + Apps Script
β βββ components/
β β βββ Layout.tsx # Sidebar nav
β β βββ SearchForm.tsx # Search input with filters
β β βββ ResultsTable.tsx # Inline editable leads + regenerate email
β β βββ ScriptDisplay.tsx # Apps Script copy/instructions
β β βββ StatusBadge.tsx # Campaign status indicator
β βββ lib/api.ts # apiFetch wrapper (auto-adds X-API-Key)
βββ scripts/
β βββ apps_script.gs
βββ .github/workflows/ci.yml # GitHub Actions (pytest + Jest, Postgres service)
βββ docker-compose.yml
βββ Dockerfile
βββ railway.json
βββ .gitignore
| Area | Detail |
|---|---|
| Gmail quota | 100 emails/day for free accounts. Google Workspace removes this |
| Places API cap | 60 results per query. Grid search covers large areas with overlapping circles |
| Email discovery | Google Maps doesn't expose emails. Manual entry may be needed |
| Places API costs | Free tier covers ~$200/month usage (~50,000+ text searches) |
Places API vs scraping?
Scraping violates Google ToS, gets IPs banned, and breaks on UI changes. Places API is legal, stable, and returns structured data.
Do I need an AI key?
No. Email generation uses category-specific angle banks with rotating templates. No AI API required.
How are emails sent?
Leads export to Google Sheets β paste the provided Apps Script β run setupTrigger(). It sends 8 emails every 20 minutes from your Gmail account.
Duplicate prevention?
Deduplication is enforced at the database level with a unique constraint on Google Place ID. Never contact the same business twice.
Is this CAN-SPAM compliant?
The generated emails include an unsubscribe option and sender identity as a good-faith effort. Consult a legal professional to ensure your specific use case meets compliance requirements.
MIT License β see LICENSE.