How to Build an AI Agent That Generates Leads While You Sleep
Complete Python code for a lead generation agent that researches prospects, personalizes outreach, sends at scale, and follows up automatically. The exact system we use at ZOO.
The Problem
You're a dev agency, freelancer, or SaaS founder. You know outreach works — but doing it manually is a soul-crushing time sink.
We faced the same problem at ZOO. So we built an AI agent that does our outreach for us. In its first week, it sent 94 personalized emails to qualified prospects, generated 25 leads, and built a pipeline worth $77K-$197K/month.
Cost of the agent: $0 in tooling. Time invested: 4 hours to build.
Here's exactly how we built it — with complete code you can steal.
Architecture Overview
┌─────────────────────────────────────────────────┐
│ Lead Gen Agent v1.0 │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Research │──▶│ Personalize│──▶│ Send │ │
│ │ Module │ │ Engine │ │ Email │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Company │ │ Template │ │ SMTP │ │
│ │ Database │ │ + Vars │ │ Client │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Follow-up Scheduler │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Step 1: The Company Research Module
First, the agent needs to find and qualify prospects. Here's a Python module that scores leads automatically:
import json
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Company:
name: str
website: str
industry: str
description: str = ""
tech_stack: list = field(default_factory=list)
contact_email: str = ""
lead_score: float = 0.0
source: str = ""
def calculate_score(self) -> float:
"""Score leads based on fit criteria (0-10)."""
score = 0.0
high_value_tech = ['react', 'next.js', 'python', 'node', 'aws', 'ai', 'ml']
for tech in self.tech_stack:
if tech.lower() in high_value_tech:
score += 1.5
high_value_industries = ['saas', 'fintech', 'ai', 'ecommerce', 'healthtech']
if self.industry.lower() in high_value_industries:
score += 3.0
if self.contact_email and '@' in self.contact_email:
score += 2.0
if len(self.description) > 100:
score += 1.0
self.lead_score = min(score, 10.0)
return self.lead_score
class ProspectResearch:
def __init__(self, min_score: float = 4.0):
self.min_score = min_score
self.prospects: list[Company] = []
def add_from_yc_list(self, companies: list[dict]) -> list[Company]:
qualified = []
for c in companies:
company = Company(
name=c.get('name', ''),
website=c.get('website', ''),
industry=c.get('industry', 'saas'),
description=c.get('description', ''),
tech_stack=c.get('tech_stack', []),
source='yc'
)
company.calculate_score()
if company.lead_score >= self.min_score:
qualified.append(company)
qualified.sort(key=lambda x: x.lead_score, reverse=True)
self.prospects.extend(qualified)
return qualified
Step 2: The Personalization Engine
Generic emails get ignored. The agent personalizes each message using company data:
from string import Template
from datetime import datetime
class PersonalizationEngine:
def generate_email(self, prospect: dict, template_name: str = 'cold_outreach') -> dict:
vars = {
'company_name': prospect.get('name', 'your company'),
'industry': prospect.get('industry', 'tech'),
'website': prospect.get('website', ''),
'description_snippet': self._snippet(prospect.get('description', ''), 80),
'date': datetime.now().strftime('%B %d, %Y'),
}
# Subject line based on lead score
score = prospect.get('score', 0)
if score >= 7:
subject = f"Idea for {prospect['name']}'s next feature"
elif score >= 5:
subject = f"{prospect['name']} + ZOO — collaboration?"
else:
subject = f"Quick question about {prospect['name']}"
body = Template(self._default_template()).safe_substitute(vars)
return {'to': prospect.get('email', ''), 'subject': subject, 'body': body}
def batch_generate(self, prospects_path: str, output_path: str) -> list[dict]:
with open(prospects_path, 'r') as f:
prospects = json.load(f)
emails = [self.generate_email(p) for p in prospects if p.get('email')]
with open(output_path, 'w') as f:
json.dump(emails, f, indent=2)
return emails
Step 3: Email Sender (SMTP) with Rate Limiting
import smtplib
import ssl
import random
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
class OutreachSender:
def __init__(self, smtp_config: dict):
self.host = smtp_config['host']
self.port = smtp_config['port']
self.username = smtp_config['username']
self.password = smtp_config['password']
self.daily_limit = smtp_config.get('daily_limit', 50)
self.sent_today = 0
def send_email(self, email_data: dict) -> bool:
if self.sent_today >= self.daily_limit:
return False
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = email_data['subject']
msg['From'] = f"ZOO Team <{self.username}>"
msg['To'] = email_data['to']
msg.attach(MIMEText(email_data['body'], 'plain'))
context = ssl.create_default_context()
with smtplib.SMTP(self.host, self.port) as server:
server.starttls(context=context)
server.login(self.username, self.password)
server.send_message(msg)
self.sent_today += 1
return True
except Exception as e:
print(f"Failed: {e}")
return False
def send_batch(self, emails_path: str, delay_range=(30, 90)):
with open(emails_path, 'r') as f:
emails = json.load(f)
for i, email in enumerate(emails):
if not self.send_email(email):
break
if i < len(emails) - 1:
time.sleep(random.uniform(*delay_range))
Step 4: The Follow-Up Scheduler
80% of sales happen after the 5th follow-up. Most humans stop at 1. The agent doesn't.
from datetime import datetime, timedelta
from dataclasses import dataclass
@dataclass
class FollowUpRule:
sequence_number: int
wait_days: int
template_name: str
condition: str
class FollowUpScheduler:
DEFAULT_SEQUENCE = [
FollowUpRule(1, 3, 'follow_up_1', 'no_reply'), # Day 3: Bump
FollowUpRule(2, 7, 'follow_up_2', 'no_reply'), # Day 7: Case study
FollowUpRule(3, 14, 'follow_up_3', 'no_reply'), # Day 14: Free offer
FollowUpRule(4, 21, 'breakup', 'no_reply'), # Day 21: Breakup
]
FOLLOW_UP_TEMPLATES = {
'follow_up_1': "Hi {company_name} — just bumping this to the top of your inbox...",
'follow_up_2': "Hi {company_name}, we just published a relevant case study...",
'follow_up_3': "Hi {company_name}, last follow-up. Free strategy call this month...",
'breakup': "Hi {company_name}, I'll assume timing isn't right. Here's my calendar...",
}
def create_campaign(self, prospect: dict, original_email: dict):
campaign = {
'prospect': prospect,
'original_email': original_email,
'follow_ups': [],
'status': 'active'
}
for rule in self.DEFAULT_SEQUENCE:
send_at = datetime.now() + timedelta(days=rule.wait_days)
campaign['follow_ups'].append({
'sequence': rule.sequence_number,
'scheduled_at': send_at.isoformat(),
'template': rule.template_name,
'status': 'pending'
})
return campaign
def get_pending_follow_ups(self) -> list[dict]:
now = datetime.now()
pending = []
for campaign in self.campaigns:
if campaign['status'] != 'active':
continue
for fu in campaign['follow_ups']:
if fu['status'] == 'pending' and datetime.fromisoformat(fu['scheduled_at']) <= now:
pending.append(self._generate_follow_up(campaign, fu))
return pending
Results: What Happened When We Ran This
| Metric | Result |
|---|---|
| Prospects researched | 50+ YC S26 startups |
| Qualified leads | 25 (score ≥ 4/10) |
| Emails sent (Day 1) | 94 |
| Pipeline generated | $77K-$197K/month |
| Follow-up sequences | 25 active (4-step each) |
| Cost | $0 (open-source only) |
Key insight: Day 1 you'll almost never get replies. That's normal. The magic happens in follow-ups — that's where humans give up. The agent doesn't.
What to Build Next (Roadmap)
- Email open tracking — 1x1 pixel tracker to know who's reading
- AI reply classification — Auto-categorize replies (interested / not now / no)
- CRM integration — Push leads to your CRM automatically
- LinkedIn enrichment — Find decision-maker emails from company domains
- A/B testing — Test subject lines and templates automatically
Want Us to Build This for You?
This agent took us 4 hours to build. If you need lead generation but don't have time to build it yourself, we can set it up for you.
© 2026 Zoo.dev — AI-powered development for startups that ship fast.