
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.
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.
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:
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.
Before you start, make sure you have the following:
npm install -g @anthropic-ai/claude-code in your terminalEstimated total build time: 3–4 hours from zero to deployed. Each step below includes a time estimate so you can pace yourself.
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.
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:
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
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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"
}
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 *.
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:
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.
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.
Add a simple analytics endpoint to your server that aggregates session data:
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:
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.
Once your baseline bot is running and capturing leads, these additions will meaningfully improve its performance:
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.
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.
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."
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
Before you start, make sure you have the following:
npm install -g @anthropic-ai/claude-code in your terminalEstimated total build time: 3–4 hours from zero to deployed. Each step below includes a time estimate so you can pace yourself.
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.
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:
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
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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"
}
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 *.
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:
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.
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.
Add a simple analytics endpoint to your server that aggregates session data:
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:
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.
Once your baseline bot is running and capturing leads, these additions will meaningfully improve its performance:
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.
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.
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."
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.

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.
New York
1074 Broadway
Woodmere, NY
Philadelphia
1429 Walnut Street
Philadelphia, PA
Florida
433 Plaza Real
Boca Raton, FL
info@adventureppc.com
(516) 218-3722
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.
Named one of the most important advertising books of all time.
buy on amazon


Over ten hours of lectures and workshops from our DOLAH Conference, themed: "Marketing Solutions for the AI Revolution"
check out dolah
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.
Over 100 hours of video training and 60+ downloadable resources
view bundles →