Automated business discovery, 99-point qualification scoring, and AI-powered personalized outreach.
Built for dual-brand agency operations: Orvyqmedia (Marketing) and Zien Technologies (Web, Automation & 3D).
Live API Endpoint Β· API Documentation (Swagger) Β· Report Bug
The Lead Scraper UI is an ultra-fast, dark-themed responsive dashboard built with React 18, Tailwind CSS, TanStack Query, and Zustand. It allows switching brands in one click, filtering high-value targets, and drafting bespoke outreach.
- Brand Context Switcher: Seamlessly toggles theme accents, pitch templates, and industry qualification bonuses between Orvyqmedia (
orv) and Zien Technologies (zien). - Visual Website Qualification Chips:
β Has Website(Emerald green badge, clickable directly to target site).π΄ No Website(High-contrast crimson badge withπ₯flame indicator for instant pitch targeting).
- Interactive Website Filter Row: Toggle between
[ All ],[ π΄ No Website ], and[ β Has Website ]with live lead counts. - Pipeline Distribution Meter: Visual multi-segment health gauge showing real-time distribution of Hot, Warm, and Cold leads, alongside high-converting No-Website pitch ratios.
- One-Click Claude AI Outreach Modal: Generates personalized cold DMs specifically tailored to the target's niche, online reputation, and missing website status.
The application consists of a decoupled client-server architecture deployed on Render (FastAPI backend) and Vercel (React SPA frontend), integrated with Supabase PostgreSQL and Google Places API (New).
- Google Places API (New): Uses
POST https://places.googleapis.com/v1/places:searchTextwithX-Goog-FieldMask. A single call retrieves the business display name, formatted address, rating, review counts, website URI, national phone number, place types, and Google Maps URI without executing separate, costly Details API requests. - Deterministic Qualification Engine: Every lead is scored against a 99-point rubric factoring in digital presence gaps, review density, reputation, and brand-specific target industries.
- Atomic Free-Tier Guard: Prevents unexpected cloud bills by enforcing a strict 900 request/month cap tracked in a dedicated Supabase
usagetable via a PostgreSQL atomic RPC function. - 24/7 Keepalive Sentinel: Implements
@app.api_route("/ping", methods=["GET", "HEAD"])pinged every 5 minutes by UptimeRobot to eliminate Render free-tier container sleep.
The scoring engine prioritizes businesses with the highest probability of needing web development, rebranding, or marketing services.
| Signal | Criteria | Points | Rationale |
|---|---|---|---|
| Base Score | All valid places | +40 | Initial baseline score for qualified local businesses. |
| No Website | websiteUri is empty / null |
+30 | π₯ High-Value Pitch Target: Ideal customer for Zien or Orvyqmedia. |
| Social / Bio Link | Domain is Instagram, FB, Linktree, wa.me, etc. |
+25 | Active digital marketing presence, but lacks an authoritative custom website. |
| Low Google Rating | rating < 4.0 |
+10 | Needs reputation management, customer review strategy, or rebranding. |
| Low Review Volume | reviews < 50 or unreviewed |
+10 | Under-indexed on local SEO and Google Maps rankings. |
| Zien Target Niche | Brand is zien & niche contains architect, interior, real estate, or construction |
+10 | High-ticket potential for custom web design, automation, and 3D architectural rendering. |
- π₯ Hot Lead (
score >= 70): Highest outreach priority. Missing a website or relying solely on social media within high-ticket niches. - π‘ Warm Lead (
45 <= score < 70): Established business with partial online presence or low review counts. - βͺ Cold Lead (
score < 45): High review count, 4.8+ rating, and existing established custom website.
The scraper inspects the websiteUri and classifies it into three explicit statuses:
flowchart LR
URL["Raw websiteUri"] --> CheckNull{"Is Empty or Null?"}
CheckNull -->|Yes| None["website_status = 'none' (+30 pts)"]
CheckNull -->|No| CheckDomain{"Matches Social / Bio Domain?"}
CheckDomain -->|Instagram / FB / Linktree / wa.me / Bio.link / etc.| Social["website_status = 'social_only' (+25 pts)"]
CheckDomain -->|Custom Domain| HasWeb["website_status = 'has_website' (+0 pts)"]
"none": Business has no website listed on Google Maps."social_only": Business links tofacebook.com,instagram.com,linktr.ee,wa.me,bio.link,beacons.ai,taplink.cc, etc."has_website": Business has a legitimate custom domain (e.g.,https://example-architects.in).
To prevent unexpected Google Cloud billing, the backend integrates an atomic usage sentinel backed by Supabase:
- Before making any outbound request to Google Places API, the server queries the
usagetable for the current month (YYYY-MM). - If
calls >= 900, the server terminates the request immediately and returns:HTTP Status:{ "detail": "Monthly Google Places API limit reached (900 calls). Free tier cap enforced. Never calling Google past 900. Resets next month." }429 Too Many Requests. - For every successful Google Places page request, the server executes the PostgreSQL RPC
increment_usage(p_month, p_count)to atomically update the counter.
POST /scrape/maps
Content-Type: application/json
{
"niche": "architect",
"city": "Bangalore",
"brand": "zien",
"max_results": 20
}Response:
{
"leads": [
{
"place_id": "ChIJb8vJ12345678",
"brand": "zien",
"niche": "architect",
"name": "Studio Morph Architects",
"address": "Indiranagar, Bangalore, Karnataka 560038",
"phone": "+91 80 2520 1234",
"website": null,
"website_status": "none",
"rating": 4.1,
"reviews": 16,
"score": 99,
"label": "hot",
"why": "No website (+30), Reviews 16 < 50 (+10), Target industry for Zien (+10)",
"maps_url": "https://maps.google.com/?cid=123456789"
}
],
"count": 20,
"brand": "zien",
"source": "maps",
"requests_made": 1
}GET /leads?brand=zien&website_status=none&label=hot&limit=50&offset=0GET /leads/export?brand=zien&website_status=noneDownloads leads-zien-YYYY-MM-DD.csv including name, brand, niche, score, label, website_status, website, phone, rating, reviews, address, why, maps_url.
GET /ping
HEAD /pingResponse: {"pong": true} (Status 200).
- Python 3.11+
- Node.js 18+ & npm
- Supabase Account & Google Maps API Key
git clone https://github.com/ekanth14/lead-scraper.git
cd lead-scrapercd backend
python -m venv .venv
# Activate Virtualenv (Windows)
.venv\Scripts\activate
# Activate Virtualenv (macOS/Linux)
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envConfigure your backend/.env:
GOOGLE_MAPS_API_KEY=AIzaSy...
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-supabase-key
FRONTEND_URL=http://localhost:5173Run database setup:
- Copy the contents of
backend/supabase_setup.sqland execute in your Supabase SQL Editor.
Start Backend Server:
uvicorn app.main:app --reload --port 8000cd ../frontend
npm install
npm run devOpen http://localhost:5173 in your browser.
- Windows: Double-click
start.bator runpowershell -ExecutionPolicy Bypass -File start.ps1. - macOS / Linux: Run
bash start.sh.
-- Free-Tier Usage Guard
CREATE TABLE usage (
month text PRIMARY KEY,
calls integer NOT NULL DEFAULT 0
);
-- Leads Storage
CREATE TABLE leads (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
place_id text UNIQUE NOT NULL,
brand text NOT NULL, -- 'orv' or 'zien'
niche text,
name text NOT NULL,
address text,
phone text,
website text,
website_status text NOT NULL DEFAULT 'none', -- 'none', 'social_only', 'has_website'
rating numeric,
reviews integer,
score integer NOT NULL DEFAULT 40,
label text NOT NULL DEFAULT 'cold', -- 'hot', 'warm', 'cold'
why text,
maps_url text,
source text DEFAULT 'maps',
created_at timestamptz DEFAULT now()
);
-- Fast Indexing
CREATE INDEX idx_leads_place_id ON leads(place_id);
CREATE INDEX idx_leads_brand ON leads(brand);
CREATE INDEX idx_leads_website_status ON leads(website_status);
CREATE INDEX idx_leads_label ON leads(label);
CREATE INDEX idx_leads_score ON leads(score DESC);This project is open-source and licensed under the MIT License.