All Articles

How to Build a Lead Generation Bot With Claude Code (Complete Tutorial)

March 10, 2026
How to Build a Lead Generation Bot With Claude Code (Complete Tutorial)

Most lead generation forms are digital dead ends. A visitor lands on your page, sees a five-field form asking for their name, email, company, phone number, and "how did you hear about us," and quietly closes the tab. You never find out what they actually wanted. They never find out if you could actually help them. Everyone loses.

A well-built lead generation bot changes that equation entirely. Instead of asking cold questions into a void, it holds a real conversation — qualifying prospects, answering objections, and capturing intent signals that a static form could never collect. And thanks to Claude Code, you can build one from scratch in a single sitting, even if you've never shipped a production AI application before.

Limited Event

Master Claude Code in One Day — Live Workshop by Adventure Media

Go from zero coding experience to building real AI-powered tools. Hands-on projects, expert guidance, no fluff.

Register Now — Spots Filling Fast →

This tutorial walks you through the complete build: environment setup, conversation architecture, lead capture logic, CRM integration, and deployment. By the end, you'll have a working lead generation bot powered by Claude's API that you can embed on any website or landing page. We'll move fast, but we won't skip the details that actually matter.

What You'll Build — and Why Claude Code Is the Right Tool for This

Claude Code is Anthropic's agentic coding environment that lets you build, run, and iterate on real software using natural language instructions alongside precise code execution. For a lead generation bot specifically, it's the right choice for three reasons: Claude's conversational intelligence is genuinely strong at holding nuanced qualification conversations, the API is straightforward to integrate, and Claude Code's agentic scaffolding lets you build the surrounding application logic without switching between a dozen different tools.

The bot you'll build in this tutorial will do the following:

  • Greet visitors and open a natural conversation based on page context
  • Qualify leads through a dynamic, branching dialogue (not a linear form)
  • Capture name, email, company, budget range, timeline, and key pain points
  • Score leads based on qualification criteria you define
  • Push captured leads to a Google Sheet or CRM via webhook
  • Send an instant notification email to your sales team
  • Serve a personalized closing message based on the lead's profile

This is not a proof-of-concept demo. Every component we build is production-ready and has been architected to handle real traffic, edge cases, and user behavior that doesn't follow the happy path.

Prerequisites and Tools You'll Need

Before you start, make sure you have the following:

  • An Anthropic API key — sign up at console.anthropic.com if you don't have one yet
  • Node.js 20+ installed on your machine
  • A Google account for Google Sheets integration (or a free HubSpot account if you prefer a CRM)
  • A Vercel account for deployment (free tier works fine)
  • Basic familiarity with JavaScript — you don't need to be an expert, but you should be comfortable reading and modifying code
  • Claude Code installed — run npm install -g @anthropic-ai/claude-code in your terminal

Estimated total build time: 3–4 hours from zero to deployed. Each step below includes a time estimate so you can pace yourself.

Step 1: Set Up Your Project Environment and API Authentication

Estimated time: 20–30 minutes. This first step establishes the foundation everything else runs on. Rushing through environment setup is the single most common reason first builds fail — a misconfigured API key or a missing dependency will cause cryptic errors three steps from now, not immediately. Do this carefully.

Initialize the Project

Open your terminal and run the following commands in sequence:

mkdir leadgen-bot
cd leadgen-bot
npm init -y
npm install @anthropic-ai/sdk express cors dotenv nodemailer googleapis

Here's what each package does:

  • @anthropic-ai/sdk — the official Anthropic SDK for Node.js, handles API calls to Claude
  • express — the web server framework that will serve your bot's API endpoints
  • cors — handles cross-origin requests so your frontend widget can talk to your backend
  • dotenv — manages environment variables so your API keys never end up in your code
  • nodemailer — sends email notifications to your sales team when a qualified lead is captured
  • googleapis — connects to Google Sheets for lead storage

Configure Your Environment Variables

Create a .env file in the root of your project directory. This file must never be committed to version control — it's where your secrets live.

ANTHROPIC_API_KEY=your_anthropic_api_key_here
GOOGLE_SHEETS_ID=your_sheet_id_here
GOOGLE_SERVICE_ACCOUNT_EMAIL=your_service_account@project.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password_here
SALES_NOTIFICATION_EMAIL=sales@yourcompany.com
BOT_NAME=Alex
COMPANY_NAME=Your Company Name
PORT=3000

Immediately add .env to your .gitignore file:

echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore

Verify Your Anthropic API Connection

Create a quick test file called test-connection.js to confirm your API key works before building anything on top of it:

require('dotenv').config();
const Anthropic = require('@anthropic-ai/sdk');

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

async function test() {
  const message = await client.messages.create({
    model: 'claude-opus-4-5',
    max_tokens: 100,
    messages: [{ role: 'user', content: 'Say "Connection successful" and nothing else.' }]
  });
  console.log(message.content[0].text);
}

test().catch(console.error);

Run it with node test-connection.js. If you see "Connection successful" in your terminal, you're ready to build. If you see an authentication error, double-check that your API key in .env doesn't have extra spaces or quotation marks around it.

Common mistake to avoid: Using the wrong model name. As of early 2026, use claude-opus-4-5 for the highest conversational quality, or claude-haiku-4-5 if you want faster responses at lower cost. Check Anthropic's current model documentation for the latest available model IDs — they update periodically.

Step 2: Design Your Lead Qualification Conversation Architecture

Estimated time: 30–45 minutes. This is the most strategically important step, and it's the one most tutorials skip entirely. The conversation architecture — the logic that determines what the bot asks, when, and in what order — determines whether your bot qualifies leads effectively or just annoys people into leaving.

Before writing a single line of code, answer these four questions about your business:

  1. What makes someone a qualified lead for you? (Budget threshold, company size, decision-making authority, geographic location, timeline to purchase)
  2. What's the minimum information you need before a sales conversation makes sense?
  3. What's the most common objection or misunderstanding that kills deals early? (Your bot can address this proactively)
  4. What does a high-intent prospect say or signal in the first 60 seconds?

Build Your System Prompt — This Is Your Bot's Brain

Create a file called prompts/system-prompt.js. The system prompt is the single most powerful lever you have over your bot's behavior. A weak system prompt produces a weak bot, no matter how good the underlying model is.

const getSystemPrompt = (pageContext) => `
You are ${process.env.BOT_NAME}, a friendly and knowledgeable assistant for ${process.env.COMPANY_NAME}.

YOUR PRIMARY GOAL: Have a natural conversation that helps visitors understand if we're the right fit for their needs, while gathering the information our team needs to follow up effectively.

YOUR PERSONALITY:
- Warm and professional, never pushy or salesy
- Ask one question at a time — never stack multiple questions in one message
- Acknowledge what the visitor shares before asking the next question
- Use their name once you have it, but don't overuse it
- Keep responses concise (2-4 sentences max unless explaining something complex)

INFORMATION YOU NEED TO COLLECT (in roughly this order, but adapt to the conversation):
1. Their first name
2. What brought them here / what they're looking for
3. Their company/organization name
4. Their role (are they a decision-maker?)
5. Their timeline (when do they need this?)
6. Their budget range (frame this as "helps us recommend the right approach")
7. Their email address (ask for this AFTER you've built rapport, not at the start)

QUALIFICATION SCORING (internal — do not mention this to the visitor):
- Budget $5,000+/month: high priority
- Decision-maker (owner, VP, director, C-suite): high priority
- Timeline under 3 months: high priority
- Timeline 3-6 months: medium priority
- Timeline 6+ months or "just exploring": low priority

IMPORTANT RULES:
- Never ask for email in your first or second message
- Never say "I'm just an AI" — focus on helping
- If someone is clearly not a fit, be honest and helpful anyway — suggest alternatives
- If someone asks a question outside your scope, say you'll have a specialist follow up
- Page context: ${pageContext}

CLOSING BEHAVIOR:
Once you have all required information, tell the visitor what happens next (e.g., "I'll have one of our specialists reach out to you at [email] within one business day") and thank them warmly.

CAPTURE SIGNAL: When you have collected name, email, company, and at least one qualifying data point, include the exact text "LEAD_CAPTURED" on a new line at the END of your response, followed by a JSON object with the collected data. This is parsed by our system — do not mention it to the visitor.

Format: LEAD_CAPTURED
{"name": "...", "email": "...", "company": "...", "role": "...", "budget": "...", "timeline": "...", "pain_points": "...", "score": "high|medium|low"}
`;

module.exports = { getSystemPrompt };

The LEAD_CAPTURED signal at the end of this prompt is a clever pattern: rather than trying to parse conversational text to determine when a lead has been fully qualified, you instruct Claude to explicitly signal completion with structured data. This makes your backend parsing reliable and simple.

Design the Conversation Flow States

Create src/conversation-manager.js to track conversation state across multiple messages:

class ConversationManager {
  constructor() {
    this.sessions = new Map();
  }

  createSession(sessionId, pageContext) {
    this.sessions.set(sessionId, {
      id: sessionId,
      messages: [],
      leadData: {},
      status: 'active', // active | qualified | closed
      startTime: new Date(),
      pageContext: pageContext || 'general'
    });
    return this.sessions.get(sessionId);
  }

  getSession(sessionId) {
    return this.sessions.get(sessionId);
  }

  addMessage(sessionId, role, content) {
    const session = this.sessions.get(sessionId);
    if (!session) throw new Error('Session not found');
    session.messages.push({ role, content });
    return session;
  }

  updateLeadData(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) throw new Error('Session not found');
    session.leadData = { ...session.leadData, ...data };
    session.status = 'qualified';
    return session;
  }

  cleanupOldSessions() {
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
    for (const [id, session] of this.sessions) {
      if (session.startTime < oneHourAgo) {
        this.sessions.delete(id);
      }
    }
  }
}

module.exports = new ConversationManager();

Pro tip: In production, replace this in-memory session store with Redis. The in-memory approach works perfectly for development and low-traffic deployments, but it won't survive server restarts and won't scale across multiple instances.

Step 3: Build the Express API Server

Estimated time: 45–60 minutes. This step creates the backend that powers all bot interactions. Your frontend widget will call these endpoints, Claude will process the conversation, and the server will handle lead capture, scoring, and notification logic.

Create src/server.js:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const Anthropic = require('@anthropic-ai/sdk');
const conversationManager = require('./conversation-manager');
const { getSystemPrompt } = require('../prompts/system-prompt');
const { saveLead } = require('./integrations/sheets');
const { sendLeadNotification } = require('./integrations/email');

const app = express();
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

app.use(cors());
app.use(express.json());

// Start a new conversation session
app.post('/api/chat/start', (req, res) => {
  const { pageContext, pageUrl } = req.body;
  const sessionId = uuidv4();
  conversationManager.createSession(sessionId, pageContext || 'general website visitor');
  
  res.json({ 
    sessionId,
    message: `Hi there! I'm ${process.env.BOT_NAME} from ${process.env.COMPANY_NAME}. What brings you here today — is there something specific I can help you with?`
  });
});

// Handle an ongoing conversation turn
app.post('/api/chat/message', async (req, res) => {
  const { sessionId, message } = req.body;
  
  if (!sessionId || !message) {
    return res.status(400).json({ error: 'sessionId and message are required' });
  }

  const session = conversationManager.getSession(sessionId);
  if (!session) {
    return res.status(404).json({ error: 'Session not found or expired' });
  }

  // Add user message to history
  conversationManager.addMessage(sessionId, 'user', message);

  try {
    const response = await client.messages.create({
      model: 'claude-opus-4-5',
      max_tokens: 500,
      system: getSystemPrompt(session.pageContext),
      messages: session.messages
    });

    const rawResponse = response.content[0].text;
    
    // Check for lead capture signal
    let botMessage = rawResponse;
    let leadCaptured = false;
    
    if (rawResponse.includes('LEAD_CAPTURED')) {
      const parts = rawResponse.split('LEAD_CAPTURED');
      botMessage = parts[0].trim();
      
      try {
        const jsonMatch = parts[1].match(/\{[\s\S]*\}/);
        if (jsonMatch) {
          const leadData = JSON.parse(jsonMatch[0]);
          leadData.sessionId = sessionId;
          leadData.pageContext = session.pageContext;
          leadData.capturedAt = new Date().toISOString();
          
          // Update session with lead data
          conversationManager.updateLeadData(sessionId, leadData);
          
          // Save to Google Sheets and send notification (async — don't block the response)
          Promise.all([
            saveLead(leadData),
            sendLeadNotification(leadData)
          ]).catch(err => console.error('Lead save/notify error:', err));
          
          leadCaptured = true;
        }
      } catch (parseError) {
        console.error('Failed to parse lead data:', parseError);
      }
    }

    // Add bot response to conversation history
    conversationManager.addMessage(sessionId, 'assistant', botMessage);

    res.json({ 
      message: botMessage,
      leadCaptured,
      sessionId
    });

  } catch (error) {
    console.error('Claude API error:', error);
    res.status(500).json({ 
      error: 'Something went wrong. Please try again.',
      message: "I'm sorry, I ran into a technical issue. Could you try again in a moment?"
    });
  }
});

// Health check endpoint
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Cleanup old sessions every 30 minutes
setInterval(() => conversationManager.cleanupOldSessions(), 30 * 60 * 1000);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Lead gen bot server running on port ${PORT}`));

Important architecture note: Notice that the lead save and email notification are fired asynchronously with Promise.all() but we don't await them before sending the response. This keeps the bot's reply instant even if your Google Sheets API call takes a second or two. The catch handler logs errors without breaking the user experience.

Step 4: Build the Google Sheets and Email Integrations

Estimated time: 30–40 minutes. These integrations are where the rubber meets the road — they transform bot conversations into actual business data your sales team can act on.

Google Sheets Integration

First, set up a Google Cloud service account and give it editor access to your spreadsheet. This is a one-time configuration step. Once you have your service account credentials saved in .env, create src/integrations/sheets.js:

const { google } = require('googleapis');

const auth = new google.auth.JWT(
  process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
  null,
  process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'),
  ['https://www.googleapis.com/auth/spreadsheets']
);

const sheets = google.sheets({ version: 'v4', auth });

async function saveLead(leadData) {
  const row = [
    new Date().toLocaleDateString('en-US'),
    new Date().toLocaleTimeString('en-US'),
    leadData.name || '',
    leadData.email || '',
    leadData.company || '',
    leadData.role || '',
    leadData.budget || '',
    leadData.timeline || '',
    leadData.pain_points || '',
    leadData.score || 'unknown',
    leadData.pageContext || '',
    leadData.sessionId || ''
  ];

  await sheets.spreadsheets.values.append({
    spreadsheetId: process.env.GOOGLE_SHEETS_ID,
    range: 'Leads!A:L',
    valueInputOption: 'USER_ENTERED',
    requestBody: { values: [row] }
  });

  console.log(`Lead saved: ${leadData.email}`);
}

module.exports = { saveLead };

Set up your Google Sheet with these column headers in row 1 of a tab named "Leads": Date, Time, Name, Email, Company, Role, Budget, Timeline, Pain Points, Score, Page Context, Session ID. The integration will append one row per captured lead.

Email Notification Integration

Create src/integrations/email.js:

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransporter({
  host: process.env.SMTP_HOST,
  port: parseInt(process.env.SMTP_PORT),
  secure: false,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS
  }
});

async function sendLeadNotification(leadData) {
  const scoreEmoji = { high: '🔥', medium: '⭐', low: '📋' }[leadData.score] || '📋';
  
  const html = `
    

${scoreEmoji} New ${leadData.score?.toUpperCase()} Priority Lead

Name${leadData.name}
Email${leadData.email}
Company${leadData.company}
Role${leadData.role}
Budget${leadData.budget}
Timeline${leadData.timeline}
Pain Points${leadData.pain_points}
Page${leadData.pageContext}

Captured: ${new Date().toLocaleString('en-US')}

`; await transporter.sendMail({ from: `"${process.env.COMPANY_NAME} Lead Bot" <${process.env.SMTP_USER}>`, to: process.env.SALES_NOTIFICATION_EMAIL, subject: `${scoreEmoji} New Lead: ${leadData.name} from ${leadData.company}`, html }); } module.exports = { sendLeadNotification };

Gmail users: You need to generate an App Password, not use your regular Gmail password. Go to your Google Account → Security → 2-Step Verification → App passwords to generate one. Using your regular password will result in authentication errors.

Step 5: Build the Frontend Chat Widget

Estimated time: 45–60 minutes. The frontend widget is what your visitors actually see and interact with. We'll build a clean, embeddable chat bubble that works on any website — no framework dependencies, pure JavaScript and CSS.

Create public/widget.js. This is a self-contained script that injects the entire chat UI into any page it's added to:

(function() {
  const BOT_API = 'https://your-deployed-api.vercel.app'; // Update after deployment
  
  // Inject styles
  const style = document.createElement('style');
  style.textContent = `
    #lgb-container {
      position: fixed;
      bottom: 24px;
      right: 24px;
      z-index: 9999;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    #lgb-bubble {
      width: 56px;
      height: 56px;
      border-radius: 50%;
      background: #2563eb;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 4px 16px rgba(37, 99, 235, 0.4);
      transition: transform 0.2s;
    }
    #lgb-bubble:hover { transform: scale(1.1); }
    #lgb-window {
      display: none;
      flex-direction: column;
      width: 360px;
      height: 520px;
      background: white;
      border-radius: 16px;
      box-shadow: 0 8px 32px rgba(0,0,0,0.15);
      margin-bottom: 12px;
      overflow: hidden;
    }
    #lgb-window.open { display: flex; }
    #lgb-header {
      background: #2563eb;
      color: white;
      padding: 16px;
      display: flex;
      align-items: center;
      gap: 12px;
    }
    #lgb-avatar {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
    }
    #lgb-messages {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .lgb-msg {
      max-width: 80%;
      padding: 10px 14px;
      border-radius: 12px;
      font-size: 14px;
      line-height: 1.5;
    }
    .lgb-msg.bot {
      background: #f1f5f9;
      align-self: flex-start;
      border-bottom-left-radius: 4px;
    }
    .lgb-msg.user {
      background: #2563eb;
      color: white;
      align-self: flex-end;
      border-bottom-right-radius: 4px;
    }
    #lgb-input-area {
      padding: 12px;
      border-top: 1px solid #e2e8f0;
      display: flex;
      gap: 8px;
    }
    #lgb-input {
      flex: 1;
      padding: 10px 14px;
      border: 1px solid #e2e8f0;
      border-radius: 24px;
      font-size: 14px;
      outline: none;
    }
    #lgb-input:focus { border-color: #2563eb; }
    #lgb-send {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background: #2563eb;
      border: none;
      cursor: pointer;
      color: white;
      font-size: 18px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .lgb-typing { display: flex; gap: 4px; padding: 10px 14px; }
    .lgb-typing span {
      width: 8px; height: 8px;
      background: #94a3b8;
      border-radius: 50%;
      animation: lgb-bounce 1.2s infinite;
    }
    .lgb-typing span:nth-child(2) { animation-delay: 0.2s; }
    .lgb-typing span:nth-child(3) { animation-delay: 0.4s; }
    @keyframes lgb-bounce {
      0%, 60%, 100% { transform: translateY(0); }
      30% { transform: translateY(-6px); }
    }
  `;
  document.head.appendChild(style);

  // Build HTML
  const container = document.createElement('div');
  container.id = 'lgb-container';
  container.innerHTML = `
    
A
Alex
● Online now
`; document.body.appendChild(container); // State let sessionId = null; let isOpen = false; let isLoading = false; const messagesEl = document.getElementById('lgb-messages'); const inputEl = document.getElementById('lgb-input'); const windowEl = document.getElementById('lgb-window'); const bubbleEl = document.getElementById('lgb-bubble'); function addMessage(text, sender) { const div = document.createElement('div'); div.className = `lgb-msg ${sender}`; div.textContent = text; messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; return div; } function showTyping() { const div = document.createElement('div'); div.className = 'lgb-msg bot lgb-typing-indicator'; div.innerHTML = '
'; messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; return div; } async function startSession() { const typing = showTyping(); const res = await fetch(`${BOT_API}/api/chat/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pageContext: document.title, pageUrl: window.location.href }) }); const data = await res.json(); typing.remove(); sessionId = data.sessionId; addMessage(data.message, 'bot'); } async function sendMessage() { const text = inputEl.value.trim(); if (!text || isLoading) return; inputEl.value = ''; addMessage(text, 'user'); isLoading = true; const typing = showTyping(); try { const res = await fetch(`${BOT_API}/api/chat/message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, message: text }) }); const data = await res.json(); typing.remove(); addMessage(data.message, 'bot'); if (data.leadCaptured) { // Fire a custom event for analytics tracking window.dispatchEvent(new CustomEvent('lgb:lead_captured', { detail: { sessionId } })); } } catch (err) { typing.remove(); addMessage("Sorry, I'm having trouble connecting. Please try again.", 'bot'); } isLoading = false; } bubbleEl.addEventListener('click', () => { isOpen = !isOpen; windowEl.classList.toggle('open', isOpen); if (isOpen && !sessionId) startSession(); if (isOpen) setTimeout(() => inputEl.focus(), 100); }); document.getElementById('lgb-send').addEventListener('click', sendMessage); inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendMessage(); }); })();

To embed this widget on any website, simply add this one line before the closing </body> tag:

<script src="https://your-deployed-api.vercel.app/widget.js"></script>

The lgb:lead_captured custom event is a key detail. When you listen for this event in your analytics setup, you can fire Google Analytics goals, Meta pixel events, or any other conversion tracking without modifying the widget code. It's a clean separation of concerns that makes the widget analytics-agnostic.

Step 6: Deploy to Vercel in Under 10 Minutes

Estimated time: 20–30 minutes including DNS configuration. Vercel is the fastest path from local development to a production URL. The free tier handles thousands of API calls per month, which is more than enough for most lead generation deployments.

Prepare for Deployment

Create a vercel.json configuration file in your project root:

{
  "version": 2,
  "builds": [
    { "src": "src/server.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/widget.js", "dest": "public/widget.js" },
    { "src": "/api/(.*)", "dest": "src/server.js" }
  ]
}

Update your package.json to add a start script:

"scripts": {
  "start": "node src/server.js",
  "dev": "nodemon src/server.js"
}

Deploy with the Vercel CLI

npm install -g vercel
vercel login
vercel --prod

Vercel will walk you through a series of prompts. Accept the defaults for most questions. When it asks about your build settings, confirm that the output directory is public and the build command is empty (we're not using a build step).

After deployment, you'll get a URL like https://leadgen-bot-abc123.vercel.app. Now go back and add your environment variables to Vercel:

vercel env add ANTHROPIC_API_KEY production
vercel env add GOOGLE_SHEETS_ID production
# Add all other .env variables one by one
vercel --prod # Redeploy to pick up the new env vars

Finally, update the BOT_API constant in public/widget.js with your actual Vercel URL and redeploy one more time. Then test your deployed bot by visiting your Vercel URL and opening the widget.

Warning: If you're getting CORS errors in the browser console, double-check that your Express CORS configuration allows requests from your website's domain. Update the CORS configuration to specify your allowed origins in production rather than using the wildcard *.

Step 7: Connect to Claude Code for Ongoing Iteration

Estimated time: 15–20 minutes. Now that you have a working bot, this is where Claude Code itself becomes your development superpower. Rather than manually editing files to tweak conversation logic, you can use Claude Code to rapidly iterate on your system prompt, add new qualification criteria, or extend the bot's capabilities.

Launch Claude Code in your project directory:

claude

From here, you can give Claude Code natural language instructions to modify your bot. For example:

  • "Update the system prompt to add a qualification question about the prospect's current technology stack"
  • "Add a webhook integration to HubSpot that fires when a high-priority lead is captured"
  • "Modify the lead scoring logic to treat any mention of a specific competitor as a high-priority signal"
  • "Add rate limiting to the chat API endpoints — max 10 messages per session, 100 sessions per IP per hour"

Claude Code will read your existing files, understand the architecture, write the modifications, and execute any necessary shell commands. This feedback loop — describe what you want, review the changes, test, iterate — is where Claude Code genuinely accelerates development. What would take an experienced developer 30 minutes of focused coding, Claude Code can often accomplish in under 5 minutes.

If you want to go deeper with this kind of development workflow — moving from understanding the tool to shipping complete projects — Adventure Media is running a hands-on Claude Code workshop called "Master Claude Code in One Day" where participants build real, production-ready projects from scratch. It's designed for marketers, entrepreneurs, and non-traditional developers who want to harness AI coding without a computer science background.

Measuring Your Bot's Performance: The Metrics That Actually Matter

Building the bot is only half the work. The metrics you track determine whether your lead gen bot becomes a core business asset or an expensive experiment. Set up these measurements from day one.

Conversation Analytics

Add a simple analytics endpoint to your server that aggregates session data:

  • Session start rate: How many visitors who see the widget open it? Healthy bots typically see meaningful engagement from visitors who are already considering a purchase.
  • Conversation completion rate: Of sessions started, how many reach at least 3 exchanges? Drop-off in the first message usually indicates a weak opening question.
  • Lead capture rate: Of conversations started, how many result in a LEAD_CAPTURED event? This is your primary optimization metric.
  • Lead-to-meeting rate: Of captured leads, how many book a sales call? Track this in your CRM. If it's low, your qualification criteria may be too loose.
  • Score distribution: Are most of your captured leads high, medium, or low priority? A healthy distribution is weighted toward high and medium.

Revenue Attribution

Use the lgb:lead_captured custom event to fire Google Analytics 4 conversion events. Tag each bot-sourced lead in your CRM with a "LeadBot" source. After 60–90 days, you'll have enough data to calculate:

  • Cost per qualified lead (CPL) from the bot vs. your other channels
  • Average deal size for bot-sourced leads
  • Sales cycle length for bot-sourced leads vs. form submissions

Industry experience consistently shows that conversational leads close faster than form-sourced leads, because the qualification conversation itself builds trust and surfaces objections that would otherwise emerge (and potentially kill deals) later in the sales cycle.

Advanced Enhancements: Where to Take This Next

Once your baseline bot is running and capturing leads, these additions will meaningfully improve its performance:

Context-Aware Opening Messages

Instead of a generic greeting, use the page URL to customize the bot's opening line. A visitor on your pricing page should get a different opener than a visitor reading a blog post. Pass the full page URL to your /api/chat/start endpoint and include page-specific context in your system prompt.

Proactive Chat Triggers

Add JavaScript on your website pages to automatically open the chat widget based on behavioral signals: time on page (over 45 seconds), scroll depth (past 70%), or exit intent (mouse moving toward browser chrome). These triggers can significantly increase session start rates on high-intent pages.

Multi-Language Support

Claude handles over 100 languages natively. Add a language detection step at the start of each session and include a language instruction in your system prompt: "The visitor's preferred language appears to be [detected language]. Respond entirely in that language."

CRM Sync with HubSpot

Replace or augment the Google Sheets integration with direct HubSpot CRM creation via the HubSpot Contacts API. This lets your sales team see bot-captured leads in their existing workflow without any manual data transfer.

Frequently Asked Questions

How much does running this bot cost per month?

Costs depend on your conversation volume and the Claude model you choose. At typical lead generation conversation lengths (8–15 exchanges), using Claude claude-haiku-4-5 keeps costs very low even at hundreds of conversations per day. Claude claude-opus-4-5 produces better qualification conversations but costs more per token. Most small to mid-size deployments run well under $50/month in API costs. Start with claude-opus-4-5 for quality, then optimize with claude-haiku-4-5 if costs become a concern.

Can I use this bot on a WordPress site?

Yes — any website that allows you to add a script tag can embed this widget. In WordPress, you can add the widget script via a plugin like "Insert Headers and Footers" or directly in your theme's functions.php file. The widget is completely self-contained and doesn't depend on any WordPress-specific functionality.

What happens when someone tries to abuse the bot or enter nonsense?

Claude handles this gracefully by default — it will politely redirect nonsensical inputs back toward the qualification conversation. For more aggressive abuse protection, add rate limiting middleware (the express-rate-limit package is simple to add) and implement a message length cap of 500 characters to prevent prompt injection attempts through the chat input.

How do I prevent the bot from making promises I can't keep?

This is controlled entirely through your system prompt. Add explicit instructions like "Never commit to specific pricing, timelines, or guarantees — always say a specialist will confirm these details." Claude follows these constraints reliably. Review your conversation logs weekly for the first month to catch any edge cases where the bot's responses don't align with your business commitments.

Can the bot handle scheduling — like booking a call directly?

Yes, with an additional integration. Calendly offers a simple API that lets you generate a booking link dynamically. Once a lead is qualified, your bot can say "I'd love to have one of our specialists connect with you — here's a link to book a 20-minute call at your convenience: [generated Calendly link]." This removes a full step from your sales process.

Is this GDPR and CCPA compliant?

Your bot's compliance depends on how you configure it and your legal disclosures — not the technology itself. At minimum: add a disclosure in the chat widget UI that conversations may be recorded for business purposes, include a privacy policy link, and ensure your data storage (Google Sheets, CRM) complies with your obligations. For B2B lead generation in the US, the compliance bar is generally lower than B2C, but consult with a privacy attorney for your specific situation.

How do I train the bot to know more about my specific products and services?

Add your product and service details directly to the system prompt. There's no separate training step required — Claude learns from context in real time. Include your pricing tiers, key differentiators, ideal customer profiles, and common FAQ answers in the system prompt. Keep this section organized and specific. For very large knowledge bases (detailed product catalogs, extensive documentation), consider implementing Retrieval-Augmented Generation (RAG) to pull relevant context dynamically rather than stuffing everything into the system prompt.

What's the difference between this and using a no-code chatbot platform?

The primary differences are flexibility, intelligence, and cost at scale. No-code platforms like Intercom, Drift, or Tidio offer faster initial setup and built-in CRM integrations, but they constrain your conversation logic to their template structures and charge significant monthly fees as conversation volume grows. The custom build in this tutorial gives you complete control over conversation logic, costs a fraction of the price at scale, and produces more natural conversations because you're using a frontier AI model without the platform's guardrails limiting its intelligence. The trade-off is the upfront build time and ongoing maintenance responsibility.

Can I run multiple different bots for different pages or products?

Absolutely — this is actually a recommended approach. Create separate system prompts for different page contexts (pricing page, product page, blog, etc.) and use the pageContext parameter to route to the appropriate prompt. Each "bot persona" is just a different system prompt — your infrastructure stays the same, and costs don't increase significantly.

How do I handle leads that come in outside business hours?

The bot itself operates 24/7 without any configuration changes. For the follow-up experience, update your closing message to set expectations: "Our team will reach out during business hours — typically within one business day." You can also implement time-based logic in your email notification to add urgency flags for leads captured during business hours vs. overnight.

What if a user abandons mid-conversation without leaving their email?

Partial lead data is still valuable. Log all conversations to your database regardless of whether they reach the LEAD_CAPTURED state. Partial sessions often contain pain point information and intent signals that can inform your content strategy and product development, even without a contact email. Consider implementing a session cookie so that if the same visitor returns, their conversation can continue from where they left off.

How often should I review and update the system prompt?

Review monthly for the first three months, then quarterly once the bot is performing consistently. Each review should look at: questions users ask that the bot struggles to answer, leads that passed the bot's qualification but didn't convert in sales (sign of loose criteria), and high-value deals that the bot scored as low-priority (sign of criteria needing recalibration). Your system prompt is a living document — treat it like your best-performing sales script and refine it continuously.

You've Built Something That Works While You Sleep

The bot you've built in this tutorial isn't a novelty — it's a genuine business asset. Every visitor who opens that chat widget and has a real conversation with your bot is a prospect being qualified, educated, and moved through your pipeline without any human intervention required. At 2 AM on a Tuesday. While you're focused on the work that actually requires your expertise.

The architecture you've built is also intentionally extensible. The session manager, the system prompt design pattern, the LEAD_CAPTURED signal — these are patterns you can reuse for customer support bots, onboarding assistants, or internal knowledge bases. You're not just shipping a lead gen bot; you're learning a development approach that scales.

What separates the bots that quietly drive revenue from the ones that get disabled after two weeks is continuous refinement. Watch your conversations. Read what prospects actually say when they're not filling out a form. Let that inform your system prompt, your qualification criteria, and ultimately your sales process. The bot gives you data that a contact form never could.

If you want to take this further — learning how to build more sophisticated AI tools, automate more of your business operations, or go deeper into Claude Code's capabilities — the fastest path is hands-on instruction. Adventure Media's "Master Claude Code in One Day" workshop is designed exactly for this: structured, project-based learning where you leave with working software, not just notes. Adventure Media has been at the forefront of AI-powered marketing since before it was mainstream, and their practical approach to AI tools reflects real production experience rather than theoretical instruction.

Your lead generation bot is live. Now go check your Google Sheet — there might already be someone in there.

Ready to Master Claude Code?

Stop reading tutorials and start building. Adventure Media's "Master Claude Code in One Day" workshop takes you from zero to building real, functional AI tools — in a single day. Hands-on projects. Expert guidance. No coding experience required.

Reserve Your Spot — Seats Are Limited

Most lead generation forms are digital dead ends. A visitor lands on your page, sees a five-field form asking for their name, email, company, phone number, and "how did you hear about us," and quietly closes the tab. You never find out what they actually wanted. They never find out if you could actually help them. Everyone loses.

A well-built lead generation bot changes that equation entirely. Instead of asking cold questions into a void, it holds a real conversation — qualifying prospects, answering objections, and capturing intent signals that a static form could never collect. And thanks to Claude Code, you can build one from scratch in a single sitting, even if you've never shipped a production AI application before.

Limited Event

Master Claude Code in One Day — Live Workshop by Adventure Media

Go from zero coding experience to building real AI-powered tools. Hands-on projects, expert guidance, no fluff.

Register Now — Spots Filling Fast →

This tutorial walks you through the complete build: environment setup, conversation architecture, lead capture logic, CRM integration, and deployment. By the end, you'll have a working lead generation bot powered by Claude's API that you can embed on any website or landing page. We'll move fast, but we won't skip the details that actually matter.

What You'll Build — and Why Claude Code Is the Right Tool for This

Claude Code is Anthropic's agentic coding environment that lets you build, run, and iterate on real software using natural language instructions alongside precise code execution. For a lead generation bot specifically, it's the right choice for three reasons: Claude's conversational intelligence is genuinely strong at holding nuanced qualification conversations, the API is straightforward to integrate, and Claude Code's agentic scaffolding lets you build the surrounding application logic without switching between a dozen different tools.

The bot you'll build in this tutorial will do the following:

  • Greet visitors and open a natural conversation based on page context
  • Qualify leads through a dynamic, branching dialogue (not a linear form)
  • Capture name, email, company, budget range, timeline, and key pain points
  • Score leads based on qualification criteria you define
  • Push captured leads to a Google Sheet or CRM via webhook
  • Send an instant notification email to your sales team
  • Serve a personalized closing message based on the lead's profile

This is not a proof-of-concept demo. Every component we build is production-ready and has been architected to handle real traffic, edge cases, and user behavior that doesn't follow the happy path.

Prerequisites and Tools You'll Need

Before you start, make sure you have the following:

  • An Anthropic API key — sign up at console.anthropic.com if you don't have one yet
  • Node.js 20+ installed on your machine
  • A Google account for Google Sheets integration (or a free HubSpot account if you prefer a CRM)
  • A Vercel account for deployment (free tier works fine)
  • Basic familiarity with JavaScript — you don't need to be an expert, but you should be comfortable reading and modifying code
  • Claude Code installed — run npm install -g @anthropic-ai/claude-code in your terminal

Estimated total build time: 3–4 hours from zero to deployed. Each step below includes a time estimate so you can pace yourself.

Step 1: Set Up Your Project Environment and API Authentication

Estimated time: 20–30 minutes. This first step establishes the foundation everything else runs on. Rushing through environment setup is the single most common reason first builds fail — a misconfigured API key or a missing dependency will cause cryptic errors three steps from now, not immediately. Do this carefully.

Initialize the Project

Open your terminal and run the following commands in sequence:

mkdir leadgen-bot
cd leadgen-bot
npm init -y
npm install @anthropic-ai/sdk express cors dotenv nodemailer googleapis

Here's what each package does:

  • @anthropic-ai/sdk — the official Anthropic SDK for Node.js, handles API calls to Claude
  • express — the web server framework that will serve your bot's API endpoints
  • cors — handles cross-origin requests so your frontend widget can talk to your backend
  • dotenv — manages environment variables so your API keys never end up in your code
  • nodemailer — sends email notifications to your sales team when a qualified lead is captured
  • googleapis — connects to Google Sheets for lead storage

Configure Your Environment Variables

Create a .env file in the root of your project directory. This file must never be committed to version control — it's where your secrets live.

ANTHROPIC_API_KEY=your_anthropic_api_key_here
GOOGLE_SHEETS_ID=your_sheet_id_here
GOOGLE_SERVICE_ACCOUNT_EMAIL=your_service_account@project.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password_here
SALES_NOTIFICATION_EMAIL=sales@yourcompany.com
BOT_NAME=Alex
COMPANY_NAME=Your Company Name
PORT=3000

Immediately add .env to your .gitignore file:

echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore

Verify Your Anthropic API Connection

Create a quick test file called test-connection.js to confirm your API key works before building anything on top of it:

require('dotenv').config();
const Anthropic = require('@anthropic-ai/sdk');

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

async function test() {
  const message = await client.messages.create({
    model: 'claude-opus-4-5',
    max_tokens: 100,
    messages: [{ role: 'user', content: 'Say "Connection successful" and nothing else.' }]
  });
  console.log(message.content[0].text);
}

test().catch(console.error);

Run it with node test-connection.js. If you see "Connection successful" in your terminal, you're ready to build. If you see an authentication error, double-check that your API key in .env doesn't have extra spaces or quotation marks around it.

Common mistake to avoid: Using the wrong model name. As of early 2026, use claude-opus-4-5 for the highest conversational quality, or claude-haiku-4-5 if you want faster responses at lower cost. Check Anthropic's current model documentation for the latest available model IDs — they update periodically.

Step 2: Design Your Lead Qualification Conversation Architecture

Estimated time: 30–45 minutes. This is the most strategically important step, and it's the one most tutorials skip entirely. The conversation architecture — the logic that determines what the bot asks, when, and in what order — determines whether your bot qualifies leads effectively or just annoys people into leaving.

Before writing a single line of code, answer these four questions about your business:

  1. What makes someone a qualified lead for you? (Budget threshold, company size, decision-making authority, geographic location, timeline to purchase)
  2. What's the minimum information you need before a sales conversation makes sense?
  3. What's the most common objection or misunderstanding that kills deals early? (Your bot can address this proactively)
  4. What does a high-intent prospect say or signal in the first 60 seconds?

Build Your System Prompt — This Is Your Bot's Brain

Create a file called prompts/system-prompt.js. The system prompt is the single most powerful lever you have over your bot's behavior. A weak system prompt produces a weak bot, no matter how good the underlying model is.

const getSystemPrompt = (pageContext) => `
You are ${process.env.BOT_NAME}, a friendly and knowledgeable assistant for ${process.env.COMPANY_NAME}.

YOUR PRIMARY GOAL: Have a natural conversation that helps visitors understand if we're the right fit for their needs, while gathering the information our team needs to follow up effectively.

YOUR PERSONALITY:
- Warm and professional, never pushy or salesy
- Ask one question at a time — never stack multiple questions in one message
- Acknowledge what the visitor shares before asking the next question
- Use their name once you have it, but don't overuse it
- Keep responses concise (2-4 sentences max unless explaining something complex)

INFORMATION YOU NEED TO COLLECT (in roughly this order, but adapt to the conversation):
1. Their first name
2. What brought them here / what they're looking for
3. Their company/organization name
4. Their role (are they a decision-maker?)
5. Their timeline (when do they need this?)
6. Their budget range (frame this as "helps us recommend the right approach")
7. Their email address (ask for this AFTER you've built rapport, not at the start)

QUALIFICATION SCORING (internal — do not mention this to the visitor):
- Budget $5,000+/month: high priority
- Decision-maker (owner, VP, director, C-suite): high priority
- Timeline under 3 months: high priority
- Timeline 3-6 months: medium priority
- Timeline 6+ months or "just exploring": low priority

IMPORTANT RULES:
- Never ask for email in your first or second message
- Never say "I'm just an AI" — focus on helping
- If someone is clearly not a fit, be honest and helpful anyway — suggest alternatives
- If someone asks a question outside your scope, say you'll have a specialist follow up
- Page context: ${pageContext}

CLOSING BEHAVIOR:
Once you have all required information, tell the visitor what happens next (e.g., "I'll have one of our specialists reach out to you at [email] within one business day") and thank them warmly.

CAPTURE SIGNAL: When you have collected name, email, company, and at least one qualifying data point, include the exact text "LEAD_CAPTURED" on a new line at the END of your response, followed by a JSON object with the collected data. This is parsed by our system — do not mention it to the visitor.

Format: LEAD_CAPTURED
{"name": "...", "email": "...", "company": "...", "role": "...", "budget": "...", "timeline": "...", "pain_points": "...", "score": "high|medium|low"}
`;

module.exports = { getSystemPrompt };

The LEAD_CAPTURED signal at the end of this prompt is a clever pattern: rather than trying to parse conversational text to determine when a lead has been fully qualified, you instruct Claude to explicitly signal completion with structured data. This makes your backend parsing reliable and simple.

Design the Conversation Flow States

Create src/conversation-manager.js to track conversation state across multiple messages:

class ConversationManager {
  constructor() {
    this.sessions = new Map();
  }

  createSession(sessionId, pageContext) {
    this.sessions.set(sessionId, {
      id: sessionId,
      messages: [],
      leadData: {},
      status: 'active', // active | qualified | closed
      startTime: new Date(),
      pageContext: pageContext || 'general'
    });
    return this.sessions.get(sessionId);
  }

  getSession(sessionId) {
    return this.sessions.get(sessionId);
  }

  addMessage(sessionId, role, content) {
    const session = this.sessions.get(sessionId);
    if (!session) throw new Error('Session not found');
    session.messages.push({ role, content });
    return session;
  }

  updateLeadData(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) throw new Error('Session not found');
    session.leadData = { ...session.leadData, ...data };
    session.status = 'qualified';
    return session;
  }

  cleanupOldSessions() {
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
    for (const [id, session] of this.sessions) {
      if (session.startTime < oneHourAgo) {
        this.sessions.delete(id);
      }
    }
  }
}

module.exports = new ConversationManager();

Pro tip: In production, replace this in-memory session store with Redis. The in-memory approach works perfectly for development and low-traffic deployments, but it won't survive server restarts and won't scale across multiple instances.

Step 3: Build the Express API Server

Estimated time: 45–60 minutes. This step creates the backend that powers all bot interactions. Your frontend widget will call these endpoints, Claude will process the conversation, and the server will handle lead capture, scoring, and notification logic.

Create src/server.js:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const Anthropic = require('@anthropic-ai/sdk');
const conversationManager = require('./conversation-manager');
const { getSystemPrompt } = require('../prompts/system-prompt');
const { saveLead } = require('./integrations/sheets');
const { sendLeadNotification } = require('./integrations/email');

const app = express();
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

app.use(cors());
app.use(express.json());

// Start a new conversation session
app.post('/api/chat/start', (req, res) => {
  const { pageContext, pageUrl } = req.body;
  const sessionId = uuidv4();
  conversationManager.createSession(sessionId, pageContext || 'general website visitor');
  
  res.json({ 
    sessionId,
    message: `Hi there! I'm ${process.env.BOT_NAME} from ${process.env.COMPANY_NAME}. What brings you here today — is there something specific I can help you with?`
  });
});

// Handle an ongoing conversation turn
app.post('/api/chat/message', async (req, res) => {
  const { sessionId, message } = req.body;
  
  if (!sessionId || !message) {
    return res.status(400).json({ error: 'sessionId and message are required' });
  }

  const session = conversationManager.getSession(sessionId);
  if (!session) {
    return res.status(404).json({ error: 'Session not found or expired' });
  }

  // Add user message to history
  conversationManager.addMessage(sessionId, 'user', message);

  try {
    const response = await client.messages.create({
      model: 'claude-opus-4-5',
      max_tokens: 500,
      system: getSystemPrompt(session.pageContext),
      messages: session.messages
    });

    const rawResponse = response.content[0].text;
    
    // Check for lead capture signal
    let botMessage = rawResponse;
    let leadCaptured = false;
    
    if (rawResponse.includes('LEAD_CAPTURED')) {
      const parts = rawResponse.split('LEAD_CAPTURED');
      botMessage = parts[0].trim();
      
      try {
        const jsonMatch = parts[1].match(/\{[\s\S]*\}/);
        if (jsonMatch) {
          const leadData = JSON.parse(jsonMatch[0]);
          leadData.sessionId = sessionId;
          leadData.pageContext = session.pageContext;
          leadData.capturedAt = new Date().toISOString();
          
          // Update session with lead data
          conversationManager.updateLeadData(sessionId, leadData);
          
          // Save to Google Sheets and send notification (async — don't block the response)
          Promise.all([
            saveLead(leadData),
            sendLeadNotification(leadData)
          ]).catch(err => console.error('Lead save/notify error:', err));
          
          leadCaptured = true;
        }
      } catch (parseError) {
        console.error('Failed to parse lead data:', parseError);
      }
    }

    // Add bot response to conversation history
    conversationManager.addMessage(sessionId, 'assistant', botMessage);

    res.json({ 
      message: botMessage,
      leadCaptured,
      sessionId
    });

  } catch (error) {
    console.error('Claude API error:', error);
    res.status(500).json({ 
      error: 'Something went wrong. Please try again.',
      message: "I'm sorry, I ran into a technical issue. Could you try again in a moment?"
    });
  }
});

// Health check endpoint
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Cleanup old sessions every 30 minutes
setInterval(() => conversationManager.cleanupOldSessions(), 30 * 60 * 1000);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Lead gen bot server running on port ${PORT}`));

Important architecture note: Notice that the lead save and email notification are fired asynchronously with Promise.all() but we don't await them before sending the response. This keeps the bot's reply instant even if your Google Sheets API call takes a second or two. The catch handler logs errors without breaking the user experience.

Step 4: Build the Google Sheets and Email Integrations

Estimated time: 30–40 minutes. These integrations are where the rubber meets the road — they transform bot conversations into actual business data your sales team can act on.

Google Sheets Integration

First, set up a Google Cloud service account and give it editor access to your spreadsheet. This is a one-time configuration step. Once you have your service account credentials saved in .env, create src/integrations/sheets.js:

const { google } = require('googleapis');

const auth = new google.auth.JWT(
  process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
  null,
  process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'),
  ['https://www.googleapis.com/auth/spreadsheets']
);

const sheets = google.sheets({ version: 'v4', auth });

async function saveLead(leadData) {
  const row = [
    new Date().toLocaleDateString('en-US'),
    new Date().toLocaleTimeString('en-US'),
    leadData.name || '',
    leadData.email || '',
    leadData.company || '',
    leadData.role || '',
    leadData.budget || '',
    leadData.timeline || '',
    leadData.pain_points || '',
    leadData.score || 'unknown',
    leadData.pageContext || '',
    leadData.sessionId || ''
  ];

  await sheets.spreadsheets.values.append({
    spreadsheetId: process.env.GOOGLE_SHEETS_ID,
    range: 'Leads!A:L',
    valueInputOption: 'USER_ENTERED',
    requestBody: { values: [row] }
  });

  console.log(`Lead saved: ${leadData.email}`);
}

module.exports = { saveLead };

Set up your Google Sheet with these column headers in row 1 of a tab named "Leads": Date, Time, Name, Email, Company, Role, Budget, Timeline, Pain Points, Score, Page Context, Session ID. The integration will append one row per captured lead.

Email Notification Integration

Create src/integrations/email.js:

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransporter({
  host: process.env.SMTP_HOST,
  port: parseInt(process.env.SMTP_PORT),
  secure: false,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS
  }
});

async function sendLeadNotification(leadData) {
  const scoreEmoji = { high: '🔥', medium: '⭐', low: '📋' }[leadData.score] || '📋';
  
  const html = `
    

${scoreEmoji} New ${leadData.score?.toUpperCase()} Priority Lead

Name${leadData.name}
Email${leadData.email}
Company${leadData.company}
Role${leadData.role}
Budget${leadData.budget}
Timeline${leadData.timeline}
Pain Points${leadData.pain_points}
Page${leadData.pageContext}

Captured: ${new Date().toLocaleString('en-US')}

`; await transporter.sendMail({ from: `"${process.env.COMPANY_NAME} Lead Bot" <${process.env.SMTP_USER}>`, to: process.env.SALES_NOTIFICATION_EMAIL, subject: `${scoreEmoji} New Lead: ${leadData.name} from ${leadData.company}`, html }); } module.exports = { sendLeadNotification };

Gmail users: You need to generate an App Password, not use your regular Gmail password. Go to your Google Account → Security → 2-Step Verification → App passwords to generate one. Using your regular password will result in authentication errors.

Step 5: Build the Frontend Chat Widget

Estimated time: 45–60 minutes. The frontend widget is what your visitors actually see and interact with. We'll build a clean, embeddable chat bubble that works on any website — no framework dependencies, pure JavaScript and CSS.

Create public/widget.js. This is a self-contained script that injects the entire chat UI into any page it's added to:

(function() {
  const BOT_API = 'https://your-deployed-api.vercel.app'; // Update after deployment
  
  // Inject styles
  const style = document.createElement('style');
  style.textContent = `
    #lgb-container {
      position: fixed;
      bottom: 24px;
      right: 24px;
      z-index: 9999;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    #lgb-bubble {
      width: 56px;
      height: 56px;
      border-radius: 50%;
      background: #2563eb;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 4px 16px rgba(37, 99, 235, 0.4);
      transition: transform 0.2s;
    }
    #lgb-bubble:hover { transform: scale(1.1); }
    #lgb-window {
      display: none;
      flex-direction: column;
      width: 360px;
      height: 520px;
      background: white;
      border-radius: 16px;
      box-shadow: 0 8px 32px rgba(0,0,0,0.15);
      margin-bottom: 12px;
      overflow: hidden;
    }
    #lgb-window.open { display: flex; }
    #lgb-header {
      background: #2563eb;
      color: white;
      padding: 16px;
      display: flex;
      align-items: center;
      gap: 12px;
    }
    #lgb-avatar {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
    }
    #lgb-messages {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .lgb-msg {
      max-width: 80%;
      padding: 10px 14px;
      border-radius: 12px;
      font-size: 14px;
      line-height: 1.5;
    }
    .lgb-msg.bot {
      background: #f1f5f9;
      align-self: flex-start;
      border-bottom-left-radius: 4px;
    }
    .lgb-msg.user {
      background: #2563eb;
      color: white;
      align-self: flex-end;
      border-bottom-right-radius: 4px;
    }
    #lgb-input-area {
      padding: 12px;
      border-top: 1px solid #e2e8f0;
      display: flex;
      gap: 8px;
    }
    #lgb-input {
      flex: 1;
      padding: 10px 14px;
      border: 1px solid #e2e8f0;
      border-radius: 24px;
      font-size: 14px;
      outline: none;
    }
    #lgb-input:focus { border-color: #2563eb; }
    #lgb-send {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background: #2563eb;
      border: none;
      cursor: pointer;
      color: white;
      font-size: 18px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .lgb-typing { display: flex; gap: 4px; padding: 10px 14px; }
    .lgb-typing span {
      width: 8px; height: 8px;
      background: #94a3b8;
      border-radius: 50%;
      animation: lgb-bounce 1.2s infinite;
    }
    .lgb-typing span:nth-child(2) { animation-delay: 0.2s; }
    .lgb-typing span:nth-child(3) { animation-delay: 0.4s; }
    @keyframes lgb-bounce {
      0%, 60%, 100% { transform: translateY(0); }
      30% { transform: translateY(-6px); }
    }
  `;
  document.head.appendChild(style);

  // Build HTML
  const container = document.createElement('div');
  container.id = 'lgb-container';
  container.innerHTML = `
    
A
Alex
● Online now
`; document.body.appendChild(container); // State let sessionId = null; let isOpen = false; let isLoading = false; const messagesEl = document.getElementById('lgb-messages'); const inputEl = document.getElementById('lgb-input'); const windowEl = document.getElementById('lgb-window'); const bubbleEl = document.getElementById('lgb-bubble'); function addMessage(text, sender) { const div = document.createElement('div'); div.className = `lgb-msg ${sender}`; div.textContent = text; messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; return div; } function showTyping() { const div = document.createElement('div'); div.className = 'lgb-msg bot lgb-typing-indicator'; div.innerHTML = '
'; messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; return div; } async function startSession() { const typing = showTyping(); const res = await fetch(`${BOT_API}/api/chat/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pageContext: document.title, pageUrl: window.location.href }) }); const data = await res.json(); typing.remove(); sessionId = data.sessionId; addMessage(data.message, 'bot'); } async function sendMessage() { const text = inputEl.value.trim(); if (!text || isLoading) return; inputEl.value = ''; addMessage(text, 'user'); isLoading = true; const typing = showTyping(); try { const res = await fetch(`${BOT_API}/api/chat/message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, message: text }) }); const data = await res.json(); typing.remove(); addMessage(data.message, 'bot'); if (data.leadCaptured) { // Fire a custom event for analytics tracking window.dispatchEvent(new CustomEvent('lgb:lead_captured', { detail: { sessionId } })); } } catch (err) { typing.remove(); addMessage("Sorry, I'm having trouble connecting. Please try again.", 'bot'); } isLoading = false; } bubbleEl.addEventListener('click', () => { isOpen = !isOpen; windowEl.classList.toggle('open', isOpen); if (isOpen && !sessionId) startSession(); if (isOpen) setTimeout(() => inputEl.focus(), 100); }); document.getElementById('lgb-send').addEventListener('click', sendMessage); inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendMessage(); }); })();

To embed this widget on any website, simply add this one line before the closing </body> tag:

<script src="https://your-deployed-api.vercel.app/widget.js"></script>

The lgb:lead_captured custom event is a key detail. When you listen for this event in your analytics setup, you can fire Google Analytics goals, Meta pixel events, or any other conversion tracking without modifying the widget code. It's a clean separation of concerns that makes the widget analytics-agnostic.

Step 6: Deploy to Vercel in Under 10 Minutes

Estimated time: 20–30 minutes including DNS configuration. Vercel is the fastest path from local development to a production URL. The free tier handles thousands of API calls per month, which is more than enough for most lead generation deployments.

Prepare for Deployment

Create a vercel.json configuration file in your project root:

{
  "version": 2,
  "builds": [
    { "src": "src/server.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/widget.js", "dest": "public/widget.js" },
    { "src": "/api/(.*)", "dest": "src/server.js" }
  ]
}

Update your package.json to add a start script:

"scripts": {
  "start": "node src/server.js",
  "dev": "nodemon src/server.js"
}

Deploy with the Vercel CLI

npm install -g vercel
vercel login
vercel --prod

Vercel will walk you through a series of prompts. Accept the defaults for most questions. When it asks about your build settings, confirm that the output directory is public and the build command is empty (we're not using a build step).

After deployment, you'll get a URL like https://leadgen-bot-abc123.vercel.app. Now go back and add your environment variables to Vercel:

vercel env add ANTHROPIC_API_KEY production
vercel env add GOOGLE_SHEETS_ID production
# Add all other .env variables one by one
vercel --prod # Redeploy to pick up the new env vars

Finally, update the BOT_API constant in public/widget.js with your actual Vercel URL and redeploy one more time. Then test your deployed bot by visiting your Vercel URL and opening the widget.

Warning: If you're getting CORS errors in the browser console, double-check that your Express CORS configuration allows requests from your website's domain. Update the CORS configuration to specify your allowed origins in production rather than using the wildcard *.

Step 7: Connect to Claude Code for Ongoing Iteration

Estimated time: 15–20 minutes. Now that you have a working bot, this is where Claude Code itself becomes your development superpower. Rather than manually editing files to tweak conversation logic, you can use Claude Code to rapidly iterate on your system prompt, add new qualification criteria, or extend the bot's capabilities.

Launch Claude Code in your project directory:

claude

From here, you can give Claude Code natural language instructions to modify your bot. For example:

  • "Update the system prompt to add a qualification question about the prospect's current technology stack"
  • "Add a webhook integration to HubSpot that fires when a high-priority lead is captured"
  • "Modify the lead scoring logic to treat any mention of a specific competitor as a high-priority signal"
  • "Add rate limiting to the chat API endpoints — max 10 messages per session, 100 sessions per IP per hour"

Claude Code will read your existing files, understand the architecture, write the modifications, and execute any necessary shell commands. This feedback loop — describe what you want, review the changes, test, iterate — is where Claude Code genuinely accelerates development. What would take an experienced developer 30 minutes of focused coding, Claude Code can often accomplish in under 5 minutes.

If you want to go deeper with this kind of development workflow — moving from understanding the tool to shipping complete projects — Adventure Media is running a hands-on Claude Code workshop called "Master Claude Code in One Day" where participants build real, production-ready projects from scratch. It's designed for marketers, entrepreneurs, and non-traditional developers who want to harness AI coding without a computer science background.

Measuring Your Bot's Performance: The Metrics That Actually Matter

Building the bot is only half the work. The metrics you track determine whether your lead gen bot becomes a core business asset or an expensive experiment. Set up these measurements from day one.

Conversation Analytics

Add a simple analytics endpoint to your server that aggregates session data:

  • Session start rate: How many visitors who see the widget open it? Healthy bots typically see meaningful engagement from visitors who are already considering a purchase.
  • Conversation completion rate: Of sessions started, how many reach at least 3 exchanges? Drop-off in the first message usually indicates a weak opening question.
  • Lead capture rate: Of conversations started, how many result in a LEAD_CAPTURED event? This is your primary optimization metric.
  • Lead-to-meeting rate: Of captured leads, how many book a sales call? Track this in your CRM. If it's low, your qualification criteria may be too loose.
  • Score distribution: Are most of your captured leads high, medium, or low priority? A healthy distribution is weighted toward high and medium.

Revenue Attribution

Use the lgb:lead_captured custom event to fire Google Analytics 4 conversion events. Tag each bot-sourced lead in your CRM with a "LeadBot" source. After 60–90 days, you'll have enough data to calculate:

  • Cost per qualified lead (CPL) from the bot vs. your other channels
  • Average deal size for bot-sourced leads
  • Sales cycle length for bot-sourced leads vs. form submissions

Industry experience consistently shows that conversational leads close faster than form-sourced leads, because the qualification conversation itself builds trust and surfaces objections that would otherwise emerge (and potentially kill deals) later in the sales cycle.

Advanced Enhancements: Where to Take This Next

Once your baseline bot is running and capturing leads, these additions will meaningfully improve its performance:

Context-Aware Opening Messages

Instead of a generic greeting, use the page URL to customize the bot's opening line. A visitor on your pricing page should get a different opener than a visitor reading a blog post. Pass the full page URL to your /api/chat/start endpoint and include page-specific context in your system prompt.

Proactive Chat Triggers

Add JavaScript on your website pages to automatically open the chat widget based on behavioral signals: time on page (over 45 seconds), scroll depth (past 70%), or exit intent (mouse moving toward browser chrome). These triggers can significantly increase session start rates on high-intent pages.

Multi-Language Support

Claude handles over 100 languages natively. Add a language detection step at the start of each session and include a language instruction in your system prompt: "The visitor's preferred language appears to be [detected language]. Respond entirely in that language."

CRM Sync with HubSpot

Replace or augment the Google Sheets integration with direct HubSpot CRM creation via the HubSpot Contacts API. This lets your sales team see bot-captured leads in their existing workflow without any manual data transfer.

Frequently Asked Questions

How much does running this bot cost per month?

Costs depend on your conversation volume and the Claude model you choose. At typical lead generation conversation lengths (8–15 exchanges), using Claude claude-haiku-4-5 keeps costs very low even at hundreds of conversations per day. Claude claude-opus-4-5 produces better qualification conversations but costs more per token. Most small to mid-size deployments run well under $50/month in API costs. Start with claude-opus-4-5 for quality, then optimize with claude-haiku-4-5 if costs become a concern.

Can I use this bot on a WordPress site?

Yes — any website that allows you to add a script tag can embed this widget. In WordPress, you can add the widget script via a plugin like "Insert Headers and Footers" or directly in your theme's functions.php file. The widget is completely self-contained and doesn't depend on any WordPress-specific functionality.

What happens when someone tries to abuse the bot or enter nonsense?

Claude handles this gracefully by default — it will politely redirect nonsensical inputs back toward the qualification conversation. For more aggressive abuse protection, add rate limiting middleware (the express-rate-limit package is simple to add) and implement a message length cap of 500 characters to prevent prompt injection attempts through the chat input.

How do I prevent the bot from making promises I can't keep?

This is controlled entirely through your system prompt. Add explicit instructions like "Never commit to specific pricing, timelines, or guarantees — always say a specialist will confirm these details." Claude follows these constraints reliably. Review your conversation logs weekly for the first month to catch any edge cases where the bot's responses don't align with your business commitments.

Can the bot handle scheduling — like booking a call directly?

Yes, with an additional integration. Calendly offers a simple API that lets you generate a booking link dynamically. Once a lead is qualified, your bot can say "I'd love to have one of our specialists connect with you — here's a link to book a 20-minute call at your convenience: [generated Calendly link]." This removes a full step from your sales process.

Is this GDPR and CCPA compliant?

Your bot's compliance depends on how you configure it and your legal disclosures — not the technology itself. At minimum: add a disclosure in the chat widget UI that conversations may be recorded for business purposes, include a privacy policy link, and ensure your data storage (Google Sheets, CRM) complies with your obligations. For B2B lead generation in the US, the compliance bar is generally lower than B2C, but consult with a privacy attorney for your specific situation.

How do I train the bot to know more about my specific products and services?

Add your product and service details directly to the system prompt. There's no separate training step required — Claude learns from context in real time. Include your pricing tiers, key differentiators, ideal customer profiles, and common FAQ answers in the system prompt. Keep this section organized and specific. For very large knowledge bases (detailed product catalogs, extensive documentation), consider implementing Retrieval-Augmented Generation (RAG) to pull relevant context dynamically rather than stuffing everything into the system prompt.

What's the difference between this and using a no-code chatbot platform?

The primary differences are flexibility, intelligence, and cost at scale. No-code platforms like Intercom, Drift, or Tidio offer faster initial setup and built-in CRM integrations, but they constrain your conversation logic to their template structures and charge significant monthly fees as conversation volume grows. The custom build in this tutorial gives you complete control over conversation logic, costs a fraction of the price at scale, and produces more natural conversations because you're using a frontier AI model without the platform's guardrails limiting its intelligence. The trade-off is the upfront build time and ongoing maintenance responsibility.

Can I run multiple different bots for different pages or products?

Absolutely — this is actually a recommended approach. Create separate system prompts for different page contexts (pricing page, product page, blog, etc.) and use the pageContext parameter to route to the appropriate prompt. Each "bot persona" is just a different system prompt — your infrastructure stays the same, and costs don't increase significantly.

How do I handle leads that come in outside business hours?

The bot itself operates 24/7 without any configuration changes. For the follow-up experience, update your closing message to set expectations: "Our team will reach out during business hours — typically within one business day." You can also implement time-based logic in your email notification to add urgency flags for leads captured during business hours vs. overnight.

What if a user abandons mid-conversation without leaving their email?

Partial lead data is still valuable. Log all conversations to your database regardless of whether they reach the LEAD_CAPTURED state. Partial sessions often contain pain point information and intent signals that can inform your content strategy and product development, even without a contact email. Consider implementing a session cookie so that if the same visitor returns, their conversation can continue from where they left off.

How often should I review and update the system prompt?

Review monthly for the first three months, then quarterly once the bot is performing consistently. Each review should look at: questions users ask that the bot struggles to answer, leads that passed the bot's qualification but didn't convert in sales (sign of loose criteria), and high-value deals that the bot scored as low-priority (sign of criteria needing recalibration). Your system prompt is a living document — treat it like your best-performing sales script and refine it continuously.

You've Built Something That Works While You Sleep

The bot you've built in this tutorial isn't a novelty — it's a genuine business asset. Every visitor who opens that chat widget and has a real conversation with your bot is a prospect being qualified, educated, and moved through your pipeline without any human intervention required. At 2 AM on a Tuesday. While you're focused on the work that actually requires your expertise.

The architecture you've built is also intentionally extensible. The session manager, the system prompt design pattern, the LEAD_CAPTURED signal — these are patterns you can reuse for customer support bots, onboarding assistants, or internal knowledge bases. You're not just shipping a lead gen bot; you're learning a development approach that scales.

What separates the bots that quietly drive revenue from the ones that get disabled after two weeks is continuous refinement. Watch your conversations. Read what prospects actually say when they're not filling out a form. Let that inform your system prompt, your qualification criteria, and ultimately your sales process. The bot gives you data that a contact form never could.

If you want to take this further — learning how to build more sophisticated AI tools, automate more of your business operations, or go deeper into Claude Code's capabilities — the fastest path is hands-on instruction. Adventure Media's "Master Claude Code in One Day" workshop is designed exactly for this: structured, project-based learning where you leave with working software, not just notes. Adventure Media has been at the forefront of AI-powered marketing since before it was mainstream, and their practical approach to AI tools reflects real production experience rather than theoretical instruction.

Your lead generation bot is live. Now go check your Google Sheet — there might already be someone in there.

Ready to Master Claude Code?

Stop reading tutorials and start building. Adventure Media's "Master Claude Code in One Day" workshop takes you from zero to building real, functional AI tools — in a single day. Hands-on projects. Expert guidance. No coding experience required.

Reserve Your Spot — Seats Are Limited

Request A Marketing Proposal

We'll get back to you within a day to schedule a quick strategy call. We can also communicate over email if that's easier for you.

Visit Us

New York
1074 Broadway
Woodmere, NY

Philadelphia
1429 Walnut Street
Philadelphia, PA

Florida
433 Plaza Real
Boca Raton, FL

General Inquiries

info@adventureppc.com
(516) 218-3722

AdVenture Education

Over 300,000 marketers from around the world have leveled up their skillset with AdVenture premium and free resources. Whether you're a CMO or a new student of digital marketing, there's something here for you.

OUR BOOK

We wrote the #1 bestselling book on performance advertising

Named one of the most important advertising books of all time.

buy on amazon
join or die bookjoin or die bookjoin or die book
OUR EVENT

DOLAH '24.
Stream Now
.

Over ten hours of lectures and workshops from our DOLAH Conference, themed: "Marketing Solutions for the AI Revolution"

check out dolah
city scape

The AdVenture Academy

Resources, guides, and courses for digital marketers, CMOs, and students. Brought to you by the agency chosen by Google to train Google's top Premier Partner Agencies.

Bundles & All Access Pass

Over 100 hours of video training and 60+ downloadable resources

Adventure resources imageview bundles →

Downloadable Guides

60+ resources, calculators, and templates to up your game.

adventure academic resourcesview guides →