AI & Business May 8, 2026·12 min

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

MetricResult
Prospects researched50+ YC S26 startups
Qualified leads25 (score ≥ 4/10)
Emails sent (Day 1)94
Pipeline generated$77K-$197K/month
Follow-up sequences25 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.