Building an Automated Lead Generator with Strict TDD
Finding clients for a software consulting side hustle is usually harder than the actual work. You end up scrolling Google Maps, clicking broken links, and trying to guess whether a business has the budget for digital work.
So I built the Local Business Lead Generator to automate the hunt for high-value service businesses (landscapers, plumbers, HVAC) that are successful enough to pay for services, but outdated enough to need them. Hereâs how it works: strict TDD, tiered analysis, and deep platform fingerprinting.
The Core Architecture
The system is a modular pipeline that takes a location and industry and outputs a prioritized list of leads. Concerns are cleanly separated into API (data ingestion), Analysis (business logic), and Models (data structures).
1. Data Ingestion (The Search Engine)
I use SerpAPI to scrape Google Maps results. Unlike generic web search, Maps data gives you the context needed for scoring, specifically, the Maps ranking.
- Hypothesis: a business ranking #1-3 on Maps is getting real traffic. If their website is broken, thatâs a high-value emergency theyâll pay to fix.
- Implementation: the
SearchEngineclass handles pagination and parses raw SerpAPI JSON into cleanBusinessInfodataclasses.
2. The Tiered Analysis Pattern
This is the most technically interesting part. I needed to analyze websites for SSL, mobile responsiveness, and tech stack, and the obvious approaches both had problems. requests is fast but fails on JavaScript-heavy sites (Wix, Squarespace). Playwright is accurate but slow and resource-heavy. So I went with a hybrid tiered approach instead.
- Tier 1 (Fast): lightweight HTTP request first. If it returns good HTML, parse it.
- Tier 2 (Heavy): if I detect JavaScript dependency indicators (
<noscript>tags, suspiciously low content length), spin up a headless browser.
You get the speed of requests for 80% of sites and the accuracy of Playwright for the messy 20%.
3. Deep Platform Fingerprinting
To sell anything effectively, you need to know what the client is already using. The analyzer doesnât just check if a site exists, it fingerprints the stack from HTML signatures.
I look for specific artifacts in the DOM and headers to identify:
- CRMs: Salesforce (
slds-scope), HubSpot (hs-script), GoHighLevel (leadconnector) - Builders: Wix, Squarespace, WordPress
- Funnels: ClickFunnels, Leadpages
If a client is running Salesforce but their website is broken, theyâre a high-value target, they invest in tech but implemented it poorly. If theyâre on ClickFunnels, they already understand conversion funnels and might be ready for automation. The signals change what youâre selling, not just whether to pitch.
The QA-First Approach: Strict TDD
Given my background in QA, I wasnât going to build this as spaghetti code. Every component went through a strict red-green-refactor cycle using Pythonâs standard unittest library.
- Zero external dependencies in the core. Just standard library (
unittest,dataclasses,typing). Portable, doesnât break from dependency hell. - Full coverage. Every component (from
SocialAnalyzertoLeadScorer) was written as a test before implementation. - Mocking strategy. Heavy use of
unittest.mockto simulate API responses. I tested the 404 logic and rate-limit handling without spending a dime on API credits during development.
Current status: 87 tests passing, 100% pass rate.
The Scoring Algorithm
Data is useless without prioritization. The LeadScorer module calculates an Opportunity Score (0-100) based on weighted factors:
- Maps Ranking (40%): higher rank = more established business = higher budget
- Website Quality Gap (30%): lower quality = bigger pain = easier sale
- Social Presence (20%): missing social channels = low-hanging fruit for upsells
- Platform Opportunity (10%): specific stacks (outdated WordPress, etc.) suggest clear upgrade paths
The output isnât just a number either, it generates human-readable reasoning:
Top ranking business (Rank #2) but has Poor website quality (Score: 25). High-value target with strong local presence.
Whatâs Next: The Intelligence Layer
The current system runs on deterministic rules. The next phase is an LLM layer that replaces hard-coded logic with semantic understanding:
- Smart normalization: mapping âguys who cut treesâ to âTree Serviceâ automatically
- Content intelligence: reading the website text to determine if they offer emergency services (which implies they need an AI voice receptionist)
- Competitive gap analysis: â3 of your top 5 competitors offer online booking, but you donât.â
What started as a lead-finding tool turned into a decent case study for how TDD and solid error handling can turn a simple scraper into something you actually trust to run unattended.