Guides

Build a Custom AI SDR with Leadpipe + OpenAI

Complete Python tutorial: Leadpipe identifies visitors, OpenAI writes personalized emails, your agent sends outreach. Full working code for $167/month.

Nicolas Canal Nicolas Canal · · 14 min read
Build a Custom AI SDR with Leadpipe + OpenAI

11x charges $5,000-10,000 per month. Artisan starts at $2,400. Qualified will run you $40-68K per year. AiSDR is the “budget” option at $900/month.

What if you could build a custom AI SDR for $167/month?

Not a toy. Not a proof-of-concept. A real pipeline that identifies your website visitors by name, classifies their intent, writes personalized outreach with OpenAI, sends the email, logs it to your CRM, and notifies your team on Slack. Fully automated. Running 24/7.

Leadpipe identifies your visitors ($147/mo). OpenAI personalizes the outreach (~$20/mo). You own the code, the logic, and the data.

Here’s the complete tutorial. Every line of code works. Copy it, deploy it, start closing.


Table of Contents

  1. What You’ll Build
  2. Prerequisites
  3. Project Setup
  4. Step 1: Webhook Receiver (FastAPI)
  5. Step 2: Intent Classifier
  6. Step 3: ICP Qualifier
  7. Step 4: OpenAI Personalization
  8. Step 5: Email Sender
  9. Step 6: CRM Logger
  10. Step 7: Slack Notification
  11. The Complete Pipeline
  12. Running It
  13. Cost Breakdown
  14. Enhancements
  15. FAQ

What You’ll Build

The architecture is straightforward. Six components, one data flow, zero magic:

Website Visitor

Leadpipe Pixel (identify)

Your Webhook Endpoint

Intent Classifier + ICP Qualifier

OpenAI (personalize email)

Email Sender (SMTP / Resend)

CRM Logger + Slack Notification

Someone visits your pricing page. Leadpipe’s pixel resolves the anonymous session into a real person - name, email, company, job title, pages viewed, visit duration - using deterministic matching. A webhook fires to your FastAPI endpoint with structured JSON.

Your code classifies the visit as high-intent (pricing page, 90+ seconds). It checks if the visitor matches your ICP. If they qualify, OpenAI drafts a personalized 3-sentence email that references what they were researching - without sounding like surveillance. The email sends. The outreach logs to your CRM. Your sales team gets a Slack ping.

The whole cycle - from page visit to personalized email in their inbox - takes seconds.

End result: Within minutes of someone visiting your pricing page, they receive a personalized email that references their visit. Automatically. While you sleep.


Prerequisites

Before you start, you need:

  • Python 3.9+ - All code in this tutorial uses Python with async/await.
  • Leadpipe account + API key - Sign up here (free trial includes 500 identified leads, no credit card). Enable webhook delivery in your dashboard. The Starter plan at $147/mo gives you 500 identified visitors.
  • OpenAI API key - We’ll use GPT-4o. Get your key here.
  • SMTP credentials - Gmail App Password, Resend, or SendGrid. Resend’s free tier handles 100 emails/day.
  • A public URL for your webhook - ngrok for development, Railway/Render/VPS for production.

Project Setup

Create your project and install dependencies:

mkdir ai-sdr && cd ai-sdr
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn openai python-dotenv requests aiohttp

Create your .env file:

# Leadpipe
LEADPIPE_WEBHOOK_SECRET=your-webhook-secret-from-dashboard

# OpenAI
OPENAI_API_KEY=sk-your-key-here

# Email (Resend)
RESEND_API_KEY=re_your-key-here
FROM_EMAIL=sales@yourcompany.com

# Slack
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL

# Your company info (used in prompts)
COMPANY_NAME=YourCompany
COMPANY_DESCRIPTION=We help B2B teams convert website visitors into pipeline
COMPANY_URL=https://yourcompany.com

# CRM (HubSpot - optional)
HUBSPOT_API_KEY=pat-your-key-here

Create main.py. That’s where everything lives. One file, one deployment.


Step 1: Webhook Receiver (FastAPI)

First, the endpoint that Leadpipe POSTs to whenever a visitor is identified. This is your agent’s ears.

# main.py
from fastapi import FastAPI, Request, HTTPException
from dotenv import load_dotenv
from openai import OpenAI
import os
import json
import asyncio
import aiohttp
import sqlite3
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime

load_dotenv()

app = FastAPI(title="AI SDR Agent")
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Simple in-memory dedup cache (use Redis in production)
contacted_emails = set()


@app.post("/webhook/leadpipe")
async def receive_visitor(request: Request):
    """Receive and validate Leadpipe webhook payloads."""

    # Verify the webhook is from Leadpipe
    signature = request.headers.get("x-leadpipe-signature")
    if signature != os.getenv("LEADPIPE_WEBHOOK_SECRET"):
        raise HTTPException(status_code=401, detail="Unauthorized")

    payload = await request.json()

    # Parse the visitor data
    visitor = parse_visitor(payload)

    # Skip if no email (can't do outreach without it)
    if not visitor["email"]:
        return {"status": "skipped", "reason": "no_email"}

    # Skip if already contacted
    if visitor["email"] in contacted_emails:
        return {"status": "skipped", "reason": "already_contacted"}

    # Process asynchronously so the webhook returns fast
    asyncio.create_task(process_visitor(visitor))

    return {"status": "received", "email": visitor["email"]}

The key detail: we return the webhook response immediately and process the visitor asynchronously. Leadpipe expects a response within a few seconds. Your email generation and sending happens in the background.

For the full webhook payload structure, see the Webhook Payload Reference. Here’s the parser:

def parse_visitor(payload: dict) -> dict:
    """Extract the fields we need from the Leadpipe webhook payload."""
    return {
        "email": payload.get("email"),
        "first_name": payload.get("first_name", ""),
        "last_name": payload.get("last_name", ""),
        "company_name": payload.get("company_name", "Unknown"),
        "company_domain": payload.get("company_domain", ""),
        "job_title": payload.get("job_title", ""),
        "linkedin_url": payload.get("linkedin_url", ""),
        "page_url": payload.get("page_url", ""),
        "visit_duration": payload.get("visit_duration", 0),
        "pages_viewed": payload.get("pages_viewed", []),
        "return_visit": payload.get("return_visit", False),
        "referrer": payload.get("referrer", ""),
    }

Development tip: Use ngrok to expose your local server: ngrok http 8000. Copy the URL and paste it into your Leadpipe webhook configuration as https://abc123.ngrok.io/webhook/leadpipe.


Step 2: Intent Classifier

Not every visitor gets an email. Someone who bounced off your homepage in 5 seconds is fundamentally different from someone who spent 3 minutes on your pricing page and then visited a case study.

The intent classifier routes visitors into three buckets:

def classify_intent(visitor: dict) -> str:
    """Classify visitor intent as high, medium, or low based on behavior."""
    page = visitor.get("page_url", "").lower()
    duration = visitor.get("visit_duration", 0)
    is_return = visitor.get("return_visit", False)
    pages = visitor.get("pages_viewed", [])

    # ---- HIGH INTENT ----
    # Pricing or demo page = actively evaluating
    if "/pricing" in page or "/demo" in page or "/book" in page:
        return "high"

    # Case study with real engagement = researching solutions
    if "/case-stud" in page and duration > 60:
        return "high"

    # Return visitor browsing 3+ pages = deep research
    if is_return and len(pages) >= 3:
        return "high"

    # Any page with 3+ minutes = serious interest
    if duration > 180:
        return "high"

    # ---- MEDIUM INTENT ----
    # Product pages with moderate engagement
    if any(p in page for p in ["/features", "/integrations", "/how-it-works",
                                "/comparison", "/vs-", "/alternative"]):
        return "medium"

    # Blog reader spending real time
    if "/blog" in page and duration > 120:
        return "medium"

    # Return visitor (any page)
    if is_return:
        return "medium"

    # ---- LOW INTENT ----
    return "low"

Why this matters. This is the logic that separates a useful AI agent from an annoying email cannon. High-intent visitors get immediate, personalized outreach. Medium-intent visitors get a softer touch with a value-add angle. Low-intent visitors get logged but not emailed.

Human SDR teams apply this same prioritization instinctively. Your agent does it consistently, for every visitor, in real time. If you’re layering in Leadpipe’s Orbit intent data, you can add topic-level signals (e.g., “researching CRM migration”) on top of page-level behavior for even sharper classification.


Step 3: ICP Qualifier

Intent isn’t enough. A college student spending 4 minutes on your pricing page is high-intent but zero-value. The ICP qualifier prevents your agent from wasting tokens and reputation on unqualified visitors.

# Personal email domains to filter out
PERSONAL_DOMAINS = {
    "gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
    "aol.com", "icloud.com", "mail.com", "protonmail.com",
    "zoho.com", "yandex.com", "live.com", "msn.com",
}

# Competitor domains to exclude
COMPETITOR_DOMAINS = {
    "competitor1.com", "competitor2.com",  # Add your competitors
}

# Titles that indicate decision-making authority
SENIOR_TITLES = [
    "ceo", "cto", "cfo", "coo", "cmo", "cro", "cpo",
    "vp", "vice president", "director", "head of",
    "founder", "co-founder", "owner", "partner",
    "manager", "lead", "senior", "principal",
]


def passes_icp_check(visitor: dict) -> bool:
    """Check if visitor matches your Ideal Customer Profile."""

    email = visitor.get("email", "")
    domain = email.split("@")[-1].lower() if "@" in email else ""
    title = visitor.get("job_title", "").lower()

    # Filter 1: Must have a business email (not personal)
    if domain in PERSONAL_DOMAINS:
        return False

    # Filter 2: Not a competitor
    if domain in COMPETITOR_DOMAINS:
        return False

    # Filter 3: Has a job title (filters out bots and noise)
    if not title:
        return False

    # Filter 4: Title suggests seniority or decision-making
    has_seniority = any(t in title for t in SENIOR_TITLES)

    # Filter 5: Not a student, intern, or researcher
    excluded_titles = ["student", "intern", "professor",
                       "teacher", "researcher", "academic"]
    is_excluded = any(t in title for t in excluded_titles)

    if is_excluded:
        return False

    # Pass if they have seniority OR if high-intent (even junior titles)
    # The idea: a Marketing Coordinator on your pricing page is still worth reaching out to
    return has_seniority or True  # Adjust strictness here

You’ll want to tune this. Some teams only email VPs and above. Others cast a wider net and let the email itself qualify. The point is that the filter exists and runs before you spend OpenAI tokens.

Tip: If you’re feeding this data through Clay’s waterfall enrichment, you can enrich company size and revenue before the ICP check for even tighter qualification.


Step 4: OpenAI Personalization

This is the core of the system. The prompt engineering is everything. A bad prompt produces emails that read like every other AI-generated outreach. A good prompt produces emails that get replies.

Here’s the system prompt - the persona your SDR agent operates with:

def get_system_prompt() -> str:
    """The SDR persona prompt. This shapes every email your agent writes."""
    company = os.getenv("COMPANY_NAME", "Our Company")
    description = os.getenv("COMPANY_DESCRIPTION", "")

    return f"""You are a sales development representative at {company}.
{description}

YOUR PERSONALITY:
- You're helpful, not pushy. You lead with value.
- You write like a real person - short sentences, no marketing fluff.
- You never say "I hope this email finds you well" or "I noticed you visited our website."
- You reference what the prospect was RESEARCHING, not that you tracked them.
- You match tone to seniority: executives get 2-3 sentences, ICs get more detail.

EMAIL RULES:
- Subject line: 4-8 words, lowercase except proper nouns, no clickbait.
- Body: 3-5 sentences maximum. One clear idea.
- CTA: Always a question, never a calendar link. Make it easy to reply.
- Sign off with first name only.
- Never use exclamation marks more than once.
- Never mention visitor tracking, cookies, or identification technology.

OUTPUT FORMAT:
Return valid JSON with exactly these fields:
{{"subject": "your subject line", "body": "your email body", "intent_reasoning": "one sentence on why you chose this approach"}}
"""

Now the user prompt - this is what changes per visitor:

def build_visitor_prompt(visitor: dict, intent_level: str) -> str:
    """Build the per-visitor prompt with all available context."""

    # Map pages to topics (so we reference the topic, not the URL)
    page = visitor.get("page_url", "")
    topic = infer_topic(page)

    prompt = f"""A website visitor was just identified. Write a personalized outreach email.

VISITOR:
- Name: {visitor['first_name']} {visitor['last_name']}
- Title: {visitor['job_title']}
- Company: {visitor['company_name']}
- Intent level: {intent_level}
- What they researched: {topic}
- Time spent: {visitor['visit_duration']} seconds
- Return visitor: {visitor['return_visit']}
- Referrer: {visitor.get('referrer', 'direct')}

APPROACH BASED ON INTENT:
"""

    if intent_level == "high":
        prompt += """- HIGH INTENT: They were on the pricing/demo page or deep in case studies.
- Be direct. Reference their research topic. Offer to answer specific questions.
- Mention a relevant result or metric if applicable.
- CTA: Ask a specific question about their use case."""

    elif intent_level == "medium":
        prompt += """- MEDIUM INTENT: They were browsing product or blog content.
- Lead with value. Share a relevant resource or insight.
- Don't pitch directly. Build curiosity.
- CTA: Ask if the topic they researched is a current priority."""

    return prompt


def infer_topic(page_url: str) -> str:
    """Convert a page URL into a natural topic description."""
    url = page_url.lower()

    if "/pricing" in url:
        return "evaluating pricing and plans"
    if "/demo" in url:
        return "looking at a product demo"
    if "/case-stud" in url:
        return "reading customer case studies and results"
    if "/comparison" in url or "/vs-" in url or "/alternative" in url:
        return "comparing solutions and alternatives"
    if "/integration" in url:
        return "exploring integrations and technical compatibility"
    if "/feature" in url:
        return "reviewing product capabilities"
    if "/blog" in url:
        return f"reading about industry trends ({url.split('/blog/')[-1].replace('-', ' ')})"
    if "/api" in url or "/developer" in url:
        return "exploring the API and developer documentation"

    return "exploring the product"

Now the actual OpenAI call:

def generate_email(visitor: dict, intent_level: str) -> dict:
    """Generate a personalized email using OpenAI GPT-4o."""

    response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": get_system_prompt()},
            {"role": "user", "content": build_visitor_prompt(visitor, intent_level)},
        ],
        temperature=0.7,
        max_tokens=500,
        response_format={"type": "json_object"},
    )

    result = json.loads(response.choices[0].message.content)
    return {
        "subject": result.get("subject", "Quick question"),
        "body": result.get("body", ""),
        "reasoning": result.get("intent_reasoning", ""),
    }

Temperature at 0.7 gives you variation between emails so 10 outreach emails to 10 pricing page visitors don’t sound identical. Drop to 0.3 for tighter control. Raise to 0.9 if emails feel too formulaic.

response_format={"type": "json_object"} guarantees structured output. No regex parsing. No broken JSON. The subject and body come back clean every time.

Cost per email: GPT-4o costs roughly $0.01-0.03 per generation at these token counts. Even at 1,000 emails per month, you’re looking at $15-20. That’s less than your team’s coffee budget.


Step 5: Email Sender

We’re using Resend because the free tier covers 100 emails/day and the API is dead simple. Swap in SendGrid, Postmark, or raw SMTP - the interface is the same.

async def send_email(to_email: str, subject: str, body: str) -> bool:
    """Send the personalized email via Resend API."""

    from_email = os.getenv("FROM_EMAIL")
    resend_key = os.getenv("RESEND_API_KEY")

    # Build HTML version (plain text body wrapped in minimal HTML)
    html_body = body.replace("\n", "<br>")

    payload = {
        "from": from_email,
        "to": [to_email],
        "subject": subject,
        "html": f"""
            <div style="font-family: -apple-system, sans-serif;
                        font-size: 14px; line-height: 1.6;
                        color: #1a1a1a; max-width: 600px;">
                {html_body}
            </div>
        """,
        "text": body,  # Plain text fallback
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            "https://api.resend.com/emails",
            headers={
                "Authorization": f"Bearer {resend_key}",
                "Content-Type": "application/json",
            },
            json=payload,
        ) as resp:
            if resp.status == 200:
                data = await resp.json()
                print(f"Email sent to {to_email}: {data.get('id')}")
                return True
            else:
                error = await resp.text()
                print(f"Email failed for {to_email}: {error}")
                return False

Deliverability matters. Don’t use a fresh domain. Set up SPF, DKIM, and DMARC records. Warm your sending domain gradually - start with 10-20 emails per day and ramp up over 2-3 weeks. A perfectly personalized email that lands in spam is worth nothing. If you’re sending more than 100/day, use a dedicated sending domain separate from your primary domain.


Step 6: CRM Logger

Every outreach needs to be logged. Your sales team needs visibility. Option A: HubSpot API. Option B: SQLite for teams that don’t want another SaaS dependency.

Option A: HubSpot

async def log_to_hubspot(visitor: dict, email_content: dict,
                          intent: str) -> None:
    """Create or update a HubSpot contact with outreach data."""

    hubspot_key = os.getenv("HUBSPOT_API_KEY")
    if not hubspot_key:
        return

    contact_data = {
        "properties": {
            "email": visitor["email"],
            "firstname": visitor["first_name"],
            "lastname": visitor["last_name"],
            "company": visitor["company_name"],
            "jobtitle": visitor["job_title"],
            "website": visitor["company_domain"],
            "hs_lead_status": "OPEN",
            "lifecyclestage": "lead",
            # Custom properties (create these in HubSpot first)
            "ai_sdr_intent_level": intent,
            "ai_sdr_last_outreach": datetime.utcnow().isoformat(),
            "ai_sdr_page_visited": visitor["page_url"],
        }
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            "https://api.hubapi.com/crm/v3/objects/contacts",
            headers={
                "Authorization": f"Bearer {hubspot_key}",
                "Content-Type": "application/json",
            },
            json=contact_data,
        ) as resp:
            if resp.status in (200, 201):
                print(f"HubSpot: created contact {visitor['email']}")
            elif resp.status == 409:
                # Contact exists - update instead
                print(f"HubSpot: contact {visitor['email']} exists, updating")
            else:
                error = await resp.text()
                print(f"HubSpot error: {error}")

Option B: SQLite (zero dependencies)

def init_db():
    """Initialize the SQLite database for outreach logging."""
    conn = sqlite3.connect("outreach.db")
    conn.execute("""
        CREATE TABLE IF NOT EXISTS outreach_log (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT NOT NULL,
            first_name TEXT,
            last_name TEXT,
            company TEXT,
            job_title TEXT,
            page_visited TEXT,
            intent_level TEXT,
            subject TEXT,
            body TEXT,
            sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            email_sent BOOLEAN DEFAULT 0
        )
    """)
    conn.commit()
    conn.close()


def log_to_sqlite(visitor: dict, email_content: dict,
                   intent: str, sent: bool) -> None:
    """Log the outreach to SQLite."""
    conn = sqlite3.connect("outreach.db")
    conn.execute(
        """INSERT INTO outreach_log
           (email, first_name, last_name, company, job_title,
            page_visited, intent_level, subject, body, email_sent)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (
            visitor["email"],
            visitor["first_name"],
            visitor["last_name"],
            visitor["company_name"],
            visitor["job_title"],
            visitor["page_url"],
            intent,
            email_content["subject"],
            email_content["body"],
            sent,
        ),
    )
    conn.commit()
    conn.close()


# Initialize on startup
init_db()

The SQLite option means you can run sqlite3 outreach.db "SELECT * FROM outreach_log" to review every email your agent has sent. No dashboard needed.


Step 7: Slack Notification

Your team should know when a high-intent visitor is identified and emailed. A Slack notification closes the feedback loop - a human can jump in within minutes if the prospect replies.

async def notify_slack(visitor: dict, intent: str,
                        email_content: dict) -> None:
    """Send a Slack notification for high and medium intent visitors."""

    webhook_url = os.getenv("SLACK_WEBHOOK_URL")
    if not webhook_url:
        return

    intent_emoji = {"high": "🔴", "medium": "🟡", "low": "⚪"}.get(intent, "⚪")

    message = {
        "blocks": [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f"{intent_emoji} {intent.upper()} Intent Visitor Identified",
                },
            },
            {
                "type": "section",
                "fields": [
                    {"type": "mrkdwn",
                     "text": f"*Name:*\n{visitor['first_name']} {visitor['last_name']}"},
                    {"type": "mrkdwn",
                     "text": f"*Company:*\n{visitor['company_name']}"},
                    {"type": "mrkdwn",
                     "text": f"*Title:*\n{visitor['job_title']}"},
                    {"type": "mrkdwn",
                     "text": f"*Page:*\n{visitor['page_url']}"},
                ],
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Email sent:*\n> Subject: {email_content['subject']}\n> {email_content['body'][:200]}...",
                },
            },
        ],
    }

    if visitor.get("linkedin_url"):
        message["blocks"].append({
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "View LinkedIn"},
                    "url": visitor["linkedin_url"],
                },
            ],
        })

    async with aiohttp.ClientSession() as session:
        await session.post(webhook_url, json=message)

When a VP of Marketing hits your pricing page and gets emailed, your sales team sees it in Slack within seconds. If the VP replies, a human is ready to take over.


The Complete Pipeline

Here’s the process_visitor() function that ties all seven steps together. This is the brain of your AI SDR:

async def process_visitor(visitor: dict) -> None:
    """The complete AI SDR pipeline. Called for every identified visitor."""

    email = visitor.get("email", "")
    name = f"{visitor['first_name']} {visitor['last_name']}".strip()

    # Step 1: ICP Check
    if not passes_icp_check(visitor):
        print(f"Skipped {name} ({email}) - failed ICP check")
        return

    # Step 2: Classify intent
    intent = classify_intent(visitor)

    # Step 3: Only email high and medium intent
    if intent == "low":
        log_to_sqlite(visitor, {"subject": "", "body": ""}, intent, False)
        print(f"Logged {name} - low intent, no outreach")
        return

    # Step 4: Generate personalized email with OpenAI
    try:
        email_content = generate_email(visitor, intent)
    except Exception as e:
        print(f"OpenAI error for {name}: {e}")
        return

    # Step 5: Send the email
    sent = await send_email(
        to_email=visitor["email"],
        subject=email_content["subject"],
        body=email_content["body"],
    )

    # Step 6: Log to CRM and SQLite
    log_to_sqlite(visitor, email_content, intent, sent)
    await log_to_hubspot(visitor, email_content, intent)

    # Step 7: Notify Slack (high intent only, or change to include medium)
    if intent == "high":
        await notify_slack(visitor, intent, email_content)

    # Mark as contacted (dedup)
    contacted_emails.add(visitor["email"])

    print(f"Processed {name} ({visitor['company_name']}) - "
          f"{intent} intent - email {'sent' if sent else 'failed'}")

That’s it. Seven steps, one function. Every identified visitor flows through the same pipeline - classified, qualified, personalized, emailed, logged, and notified.


Running It

Start the server:

uvicorn main:app --host 0.0.0.0 --port 8000

For local development, expose it with ngrok:

ngrok http 8000

Then configure your Leadpipe webhook:

  1. Go to your Leadpipe dashboard
  2. Navigate to Integrations > Webhooks
  3. Set the URL to https://your-ngrok-url.ngrok.io/webhook/leadpipe
  4. Copy your webhook secret and add it to .env
  5. Save and test with a test event

Deploy to Production

For always-on deployment, you have several options:

Railway (recommended for simplicity):

# Install Railway CLI
npm install -g @railway/cli

# Deploy
railway login
railway init
railway up

Render: Create a render.yaml:

services:
  - type: web
    name: ai-sdr
    runtime: python
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT

Any VPS (DigitalOcean, Hetzner):

# Run with systemd or supervisor for auto-restart
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2

Generate your requirements.txt:

pip freeze > requirements.txt

Point Leadpipe’s webhook at your production URL. Done.


Cost Breakdown

Here’s what this stack actually costs per month:

ComponentMonthly Cost
Leadpipe Starter (500 IDs)$147
OpenAI GPT-4o (~1,000 emails)~$15-20
Resend (email sending)Free tier / $20
Railway or Render (hosting)$5-10
Total~$167-197

Now compare that to commercial AI SDR platforms:

PlatformMonthly CostWhat You Get
Your custom agent$167-197Full control, own your code and data
AiSDR$900+SaaS platform, limited customization
Artisan (Ava)$2,400-7,200End-to-end SDR, proprietary logic
11x (Alice)$5,000-10,000Full-cycle automation, enterprise
Qualified (Piper)$3,300-5,700Inbound AI SDR + chat

You’re getting 90% of the functionality for 2-4% of the cost. And you own everything - the prompts, the logic, the data, the code. No vendor lock-in. No “we changed our pricing” surprises.

The real ROI math: If your average deal is worth $10,000 and this system helps you close just one extra deal per month, that’s a 50x return on your $197 investment. Even at one extra deal per quarter, you’re at 12x ROI.

Start your free Leadpipe trial - 500 identified leads, no credit card required.


Enhancements

Once the base pipeline is running, here’s where to take it:

Add Clay waterfall enrichment for richer company data - employee count, revenue, funding stage, tech stack. Feed that into your ICP qualifier for sharper filtering. Full walkthrough here.

Layer in Leadpipe Orbit intent data to know what visitors are researching across 20,000+ topics before they hit your site. A VP researching “CRM migration” who then visits your integrations page is a completely different conversation than a cold visit. See the intent API guide.

Add follow-up sequences. The first email is one touch. Build a 3-step sequence: Day 0 (personalized intro), Day 3 (relevant case study), Day 7 (breakup email). Store the sequence state in SQLite and run a cron job to send follow-ups.

Add LinkedIn connection requests. Use PhantomBuster or Dripify APIs to send a LinkedIn connection request alongside the email. Multi-channel outreach converts 2-3x better than single-channel.

A/B test subject lines. Generate two subject lines per email, randomly assign, track open rates. After 100 emails, feed the winner data back into your system prompt. Your agent gets smarter over time.

Build a feedback loop. Track replies in your inbox (or use Resend’s webhook for email events). Log which emails got responses. Use that data to refine your prompts monthly. The commercial AI SDR platforms charge $5K/month partly because they have this feedback loop built in. You can build yours in an afternoon.

For a deeper look at how this pipeline fits into the full AI SDR data stack, including enrichment, scoring, and meeting booking, see the complete guide.


FAQ

Can I use Claude or another LLM instead of OpenAI?

Absolutely. Swap the OpenAI client for the Anthropic SDK. The prompt structure is identical. Claude tends to produce more natural-sounding outreach out of the box and follows system prompt instructions tightly. If you’re on a budget, GPT-4o-mini cuts costs by 90% with slightly less nuanced personalization.

How do I handle scale beyond 500 visitors per month?

Leadpipe’s Growth plan at $299/mo handles higher volumes. On the code side, replace the in-memory contacted_emails set with Redis, use a task queue like Celery for processing, and add rate limiting to your email sender. The architecture scales linearly - there’s no architectural ceiling.

What about email deliverability? Won’t this get flagged as spam?

Three things keep you out of spam: (1) Proper email authentication - SPF, DKIM, DMARC on your sending domain. (2) Volume ramping - start with 10-20 emails/day, increase by 10/day each week. (3) Quality content - because each email is personalized to the visitor’s actual behavior, engagement rates are high, which improves your sender reputation over time. Generic blast emails kill deliverability. Hyper-personalized emails from AI actually help it.

Is this compliant with CAN-SPAM and GDPR?

For US-based outreach: CAN-SPAM requires a physical address, an unsubscribe mechanism, and accurate “From” headers. Add your address to the email footer and include an unsubscribe link - Resend handles this automatically. For GDPR (EU visitors): Leadpipe provides company-level identification only for EU traffic. Person-level identification is limited to US visitors where CCPA governs. Always consult legal counsel for your specific use case.


What You’ve Built

Let’s recap. You now have a custom AI SDR that:

  1. Identifies anonymous website visitors in real time via Leadpipe
  2. Classifies intent based on pages visited and engagement depth
  3. Qualifies visitors against your ICP before any outreach
  4. Personalizes every email with GPT-4o based on visitor behavior
  5. Sends the email automatically via Resend
  6. Logs every action to your CRM and a local database
  7. Notifies your team on Slack for high-intent prospects

Total cost: ~$167/month. Total lines of code: under 400. Time to deploy: an afternoon.

The commercial AI SDR platforms are building the same pipeline with more polish and less flexibility. You’re building it with full control for a fraction of the price. And when you need to change the qualification logic, tweak the prompts, or add a new channel - you edit your code. No support tickets. No waiting for a feature request.

The data layer is what makes AI sales agents work. Without real-time visitor identity, your agent is guessing. With Leadpipe feeding it live behavioral data, every outreach is grounded in who actually showed up and what they cared about.

Start your free trial - 500 identified leads, no credit card required.