#!/usr/bin/env node
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import database from './database-pg.js';
import LinkedInAutomation from './linkedin.js';
import AIService from './ai.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
let linkedinBot = null;
let aiService = null;
let servicesInitialized = false;
let campaignWorkers = new Map(); // Track active campaign workers
// Automated Campaign Message Processing Function
async function processCampaignMessages(campaignId, userId) {
try {
if (!linkedinBot || !linkedinBot.isConnected) {
console.error(`Campaign ${campaignId}: LinkedIn browser not connected`);
return { success: false, error: 'LinkedIn browser not connected' };
}
const campaign = await database.getCampaign(campaignId, userId);
if (!campaign || campaign.status !== 'active') {
console.log(`Campaign ${campaignId}: Not active, stopping processing`);
campaignWorkers.delete(campaignId);
return { success: false, error: 'Campaign not active' };
}
const leadsToProcess = await database.getCampaignLeadsForSending(campaignId);
if (leadsToProcess.length === 0) {
return { success: true, processed: 0, message: 'No leads ready to process' };
}
const messageTemplate = campaign.message_template || {};
const followUpSequence = campaign.follow_up_sequence || [];
const results = [];
const session = await database.getSession(userId);
if (session && session.cookies && session.cookies.length > 0) {
await linkedinBot.setupSession(session.cookies[0].value);
}
for (const campaignLead of leadsToProcess.slice(0, 10)) {
try {
const leadResult = await database.query(
'SELECT * FROM leads WHERE profile_url = $1 AND user_id = $2',
[campaignLead.profile_url, userId]
);
const lead = leadResult.rows[0];
if (!lead) continue;
let messageText = '';
let isConnectionRequest = false;
if (campaignLead.sequence_stage === 0) {
messageText = messageTemplate.message || '';
isConnectionRequest = true;
} else {
const followUp = followUpSequence[campaignLead.sequence_stage - 1];
if (followUp) {
messageText = followUp.message || '';
} else {
await database.updateCampaignLead(campaignLead.id, { status: 'completed' });
continue;
}
}
if (!messageText) continue;
if (aiService && messageTemplate.message) {
const personalizedResult = await aiService.generateMessage(lead, {
value_proposition: messageTemplate.message,
message_type: isConnectionRequest ? 'connection' : 'direct'
});
if (personalizedResult.success && personalizedResult.message) {
messageText = personalizedResult.message;
}
}
const sendResult = await linkedinBot.sendMessage(campaignLead.profile_url, messageText, isConnectionRequest);
if (sendResult.success) {
const message = await database.saveMessage({
lead_id: campaignLead.lead_id,
user_id: userId,
profile_url: campaignLead.profile_url,
message_text: messageText,
sent_at: new Date().toISOString(),
sequence_stage: campaignLead.sequence_stage
});
const nextStage = campaignLead.sequence_stage + 1;
const nextFollowUp = followUpSequence[nextStage - 1];
const nextScheduledAt = nextFollowUp
? new Date(Date.now() + (nextFollowUp.days_after_previous || 3) * 24 * 60 * 60 * 1000).toISOString()
: null;
await database.updateCampaignLead(campaignLead.id, {
status: 'message_sent',
sequence_stage: nextStage,
last_message_sent_at: new Date().toISOString(),
next_message_scheduled_at: nextScheduledAt
});
await database.createMessageEvent({
message_id: message.id,
campaign_id: campaignId,
event_type: 'sent',
event_data: { sequence_stage: campaignLead.sequence_stage }
});
const today = new Date().toISOString().split('T')[0];
await database.updateCampaignAnalytics(campaignId, today, {
messages_sent: 1
});
results.push({ success: true, lead: campaignLead.profile_url, message_id: message.id });
await new Promise(resolve => setTimeout(resolve, Math.random() * 5000 + 5000));
} else {
results.push({ success: false, lead: campaignLead.profile_url, error: sendResult.error });
}
} catch (error) {
console.error(`Error processing lead ${campaignLead.profile_url}:`, error);
results.push({ success: false, lead: campaignLead.profile_url, error: error.message });
}
}
console.log(`Campaign ${campaignId}: Processed ${results.length} leads`);
return { success: true, processed: results.length, results };
} catch (error) {
console.error(`Error processing campaign ${campaignId}:`, error);
return { success: false, error: error.message };
}
}
// Background Campaign Worker - Continuously processes active campaigns
async function startCampaignWorker(campaignId, userId) {
if (campaignWorkers.has(campaignId)) {
console.log(`Campaign ${campaignId}: Worker already running`);
return;
}
console.log(`Campaign ${campaignId}: Starting automated worker`);
let intervalId = null;
const processInterval = async () => {
try {
const campaign = await database.getCampaign(campaignId, userId);
if (!campaign || campaign.status !== 'active') {
console.log(`Campaign ${campaignId}: No longer active, stopping worker`);
if (intervalId) clearInterval(intervalId);
campaignWorkers.delete(campaignId);
return;
}
await processCampaignMessages(campaignId, userId);
} catch (error) {
console.error(`Campaign ${campaignId} worker error:`, error);
}
};
await processInterval();
intervalId = setInterval(processInterval, 15 * 60 * 1000);
campaignWorkers.set(campaignId, intervalId);
}
function stopCampaignWorker(campaignId) {
const intervalId = campaignWorkers.get(campaignId);
if (intervalId) {
clearInterval(intervalId);
}
campaignWorkers.delete(campaignId);
console.log(`Campaign ${campaignId}: Worker stopped`);
}
async function initializeServices() {
if (servicesInitialized) return;
try {
await database.init();
// Initialize AI service only if we have the required config
// For Vercel: use VERTEX_AI_API_KEY
// For local: use gcloud CLI or VERTEX_AI_API_KEY
if (process.env.GCP_PROJECT_ID || process.env.VERTEX_AI_API_KEY) {
try {
aiService = new AIService({
projectId: process.env.GCP_PROJECT_ID,
location: process.env.GCP_LOCATION,
modelId: process.env.ANTHROPIC_MODEL_ID
});
} catch (error) {
console.warn('⚠️ AI service initialization failed (non-critical):', error.message);
// Don't throw - allow app to continue without AI features
aiService = null;
}
}
servicesInitialized = true;
} catch (error) {
console.error('Failed to initialize services:', error);
// Don't throw - allow health check to work
servicesInitialized = true;
}
}
// Initialize services on first request (for Vercel serverless)
app.use(async (req, res, next) => {
if (!servicesInitialized) {
try {
await initializeServices();
} catch (error) {
console.error('Failed to initialize services:', error);
return res.status(503).json({
success: false,
error: 'Service initialization failed',
details: error.message
});
}
}
next();
});
// Root route
app.get('/', (req, res) => {
res.json({
name: 'LinkedIn Lead Automation MCP Server',
version: '1.0.0',
status: 'running',
endpoints: {
health: '/health',
api: '/api',
docs: 'https://github.com/vikram-agentic/linkedin-mcp'
}
});
});
// Favicon handler (ignore favicon requests)
app.get('/favicon.ico', (req, res) => {
res.status(204).end();
});
app.get('/health', (req, res) => {
try {
res.json({
status: 'healthy',
service: 'linkedin-lead-automation-api',
database: servicesInitialized && database.initialized ? 'connected' : 'not connected',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
status: 'error',
error: error.message
});
}
});
app.get('/api', (req, res) => {
res.json({
name: 'LinkedIn Lead Automation API',
version: '2.0.0',
status: 'running',
description: 'Comprehensive LinkedIn Lead Automation with AI-Powered Campaign Management',
endpoints: {
// Core Setup
generateKey: 'POST /api/generate-key',
connectBrowser: 'POST /api/browser/connect',
setupSession: 'POST /api/session/setup',
// Product & ICP Management
analyzeProduct: 'POST /api/products/analyze - Deep product/website analysis',
createProduct: 'POST /api/products - Create product',
getProducts: 'GET /api/products?api_key=... - Get all products',
generateICP: 'POST /api/icps/generate - Generate ICP from product analysis',
createICP: 'POST /api/icps - Create ICP manually',
getICPs: 'GET /api/icps?api_key=... - Get all ICPs',
// Lead Discovery & Management
discoverLeads: 'POST /api/campaigns/discover - Discover leads based on ICP',
searchLeads: 'POST /api/leads/search - Search LinkedIn',
analyzeProfile: 'POST /api/leads/analyze - Analyze profile',
scoreLead: 'POST /api/leads/score - Score lead with AI',
getLeads: 'GET /api/leads?api_key=... - Get all leads',
// Campaign Management
createCampaign: 'POST /api/campaigns - Create campaign',
getCampaigns: 'GET /api/campaigns?api_key=...&status=... - Get campaigns',
getCampaign: 'GET /api/campaigns/:id?api_key=... - Get campaign details',
generateSequence: 'POST /api/campaigns/:id/generate-sequence - Generate dynamic sequence',
selectLeads: 'POST /api/campaigns/:id/leads/select - Select leads for campaign',
approveCampaign: 'POST /api/campaigns/:id/approve - Approve campaign',
startCampaign: 'POST /api/campaigns/:id/start - Start automated sending',
processCampaign: 'POST /api/campaigns/:id/process - Process pending messages',
pauseCampaign: 'POST /api/campaigns/:id/pause - Pause campaign',
getCampaignAnalytics: 'GET /api/campaigns/:id/analytics?api_key=... - Get analytics',
// Messaging
generateMessage: 'POST /api/messages/generate - Generate personalized message',
sendMessage: 'POST /api/messages/send - Send message',
createSequence: 'POST /api/sequences/create - Create follow-up sequence',
// Usage & Testing
getUsage: 'GET /api/usage?api_key=... - Get usage stats',
createTestLead: 'POST /api/test/lead - Create test lead (no browser)'
},
workflow: {
step1: 'Analyze product: POST /api/products/analyze',
step2: 'Generate ICP: POST /api/icps/generate',
step3: 'Discover leads: POST /api/campaigns/discover',
step4: 'Create campaign: POST /api/campaigns',
step5: 'Generate sequence: POST /api/campaigns/:id/generate-sequence',
step6: 'Select leads: POST /api/campaigns/:id/leads/select',
step7: 'Approve: POST /api/campaigns/:id/approve',
step8: 'Start: POST /api/campaigns/:id/start',
step9: 'Process: POST /api/campaigns/:id/process (automated)',
step10: 'Analytics: GET /api/campaigns/:id/analytics'
}
});
});
// Test endpoint: Create a mock lead for AI feature testing (no browser required)
app.post('/api/test/lead', async (req, res) => {
try {
const { api_key, profile_url, name, headline, company, title, location } = req.body;
if (!api_key || !profile_url) {
return res.status(400).json({ success: false, error: 'api_key and profile_url are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
// Create mock lead data for AI testing
const mockLead = {
user_id: key.id,
profile_url: profile_url,
name: name || 'Test User',
headline: headline || 'CEO at Test Company',
company: company || 'Test Company Inc',
title: title || 'Chief Executive Officer',
location: location || 'San Francisco, CA',
experience: [{ title: title || 'CEO', company: company || 'Test Company', duration: '5 years' }],
education: [{ school: 'Test University', degree: 'MBA' }],
skills: ['Leadership', 'Strategy', 'Business Development'],
summary: 'Experienced executive with a passion for innovation and growth.',
analyzed_at: new Date().toISOString()
};
const lead = await database.saveLead(mockLead);
res.json({
success: true,
message: 'Test lead created successfully',
lead: {
id: lead.id,
profile_url: lead.profile_url,
name: lead.name,
headline: lead.headline
}
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/generate-key', async (req, res) => {
try {
const { tier = 'starter' } = req.body;
const result = await database.createApiKey(tier);
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/browser/connect', async (req, res) => {
try {
const { cdp_url, use_unblock, use_stealth } = req.body;
// Use provided CDP URL or default from environment variable (for cloud deployments)
const cdpUrl = cdp_url || process.env.CDP_URL;
// Use Browserless.io /unblock API to bypass bot detection (recommended for LinkedIn)
const useUnblock = use_unblock !== undefined ? use_unblock : (cdpUrl && cdpUrl.includes('browserless.io'));
// Use stealth mode for additional bot detection bypass (recommended for LinkedIn)
const useStealth = use_stealth !== undefined ? use_stealth : (cdpUrl && cdpUrl.includes('browserless.io'));
if (!cdpUrl) {
return res.status(400).json({
success: false,
error: 'cdp_url is required in request body or set CDP_URL environment variable'
});
}
linkedinBot = new LinkedInAutomation();
const result = await linkedinBot.connect(cdpUrl, useUnblock, useStealth);
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Test connectivity from Browserless.io to external sites
app.post('/api/browser/test-connectivity', async (req, res) => {
try {
const { cdp_url, test_url } = req.body;
const cdpUrl = cdp_url || process.env.CDP_URL;
const testUrl = test_url || 'https://www.google.com';
if (!cdpUrl) {
return res.status(400).json({
success: false,
error: 'cdp_url is required in request body or set CDP_URL environment variable'
});
}
// Create a temporary browser instance for testing
const testBot = new LinkedInAutomation();
const connectResult = await testBot.connect(cdpUrl);
if (!connectResult.success) {
return res.json({
success: false,
error: 'Failed to connect to browser',
details: connectResult
});
}
try {
// Try navigating to test URL
await testBot.page.goto(testUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
const currentUrl = testBot.page.url();
const pageTitle = await testBot.page.title();
const isErrorPage = currentUrl.startsWith('chrome-error://') ||
currentUrl.startsWith('chrome://error') ||
currentUrl.startsWith('data:');
if (isErrorPage) {
return res.json({
success: false,
error: `Connectivity test failed. Browserless.io cannot reach ${testUrl}`,
debug: {
currentUrl,
suggestion: 'Check Browserless.io network connectivity, DNS resolution, or firewall rules'
}
});
}
// Close test browser
if (testBot.browser) {
await testBot.browser.close();
}
res.json({
success: true,
message: `Successfully reached ${testUrl}`,
details: {
finalUrl: currentUrl,
pageTitle: pageTitle.substring(0, 100)
}
});
} catch (error) {
// Close test browser on error
if (testBot.browser) {
try {
await testBot.browser.close();
} catch (e) {
// Ignore close errors
}
}
res.json({
success: false,
error: `Navigation to ${testUrl} failed: ${error.message}`,
debug: {
currentUrl: testBot.page ? testBot.page.url() : 'unknown',
error: error.message
}
});
}
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/session/setup', async (req, res) => {
try {
const { api_key, li_at_cookie, cdp_url } = req.body;
if (!api_key || !li_at_cookie) {
return res.status(400).json({ success: false, error: 'api_key and li_at_cookie are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
// For serverless (Vercel), browser connection doesn't persist across requests
// Auto-connect if not connected (using provided cdp_url or environment variable)
if (!linkedinBot || !linkedinBot.isConnected) {
const cdpUrl = cdp_url || process.env.CDP_URL;
// Use Browserless.io /unblock API + stealth mode for better bot detection bypass (recommended for LinkedIn)
const useUnblock = cdpUrl && cdpUrl.includes('browserless.io');
const useStealth = cdpUrl && cdpUrl.includes('browserless.io');
if (!cdpUrl) {
return res.status(400).json({
success: false,
error: 'Browser not connected. Provide cdp_url in request body or set CDP_URL environment variable.'
});
}
// Create new browser connection with unblock API and stealth mode
linkedinBot = new LinkedInAutomation();
const connectResult = await linkedinBot.connect(cdpUrl, useUnblock, useStealth);
if (!connectResult.success) {
return res.status(500).json({
success: false,
error: `Failed to connect to browser: ${connectResult.error || connectResult.message}`
});
}
}
const result = await linkedinBot.setupSession(li_at_cookie);
if (result.success) {
await database.saveSession(key.id, li_at_cookie);
}
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/leads/search', async (req, res) => {
try {
const { api_key, keywords, location, limit = 25 } = req.body;
if (!api_key || !keywords) {
return res.status(400).json({ success: false, error: 'api_key and keywords are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(400).json({ success: false, error: 'Browser not connected or session not setup' });
}
const result = await linkedinBot.searchLeads({ keywords, location, limit });
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/leads/analyze', async (req, res) => {
try {
const { api_key, profile_url } = req.body;
if (!api_key || !profile_url) {
return res.status(400).json({ success: false, error: 'api_key and profile_url are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const usage = await database.checkUsageLimit(key.id, 'profile_analysis');
if (!usage.allowed) {
return res.status(403).json({ success: false, error: usage.reason });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(400).json({ success: false, error: 'Browser not connected' });
}
const result = await linkedinBot.analyzeProfile(profile_url);
if (result.success) {
await database.saveLead({
user_id: key.id,
profile_url,
...result.data,
analyzed_at: new Date().toISOString()
});
}
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/leads/score', async (req, res) => {
try {
const { api_key, profile_url } = req.body;
if (!api_key || !profile_url) {
return res.status(400).json({ success: false, error: 'api_key and profile_url are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not configured. Set GCP_PROJECT_ID and ensure gcloud auth is set up.' });
}
const lead = await database.getLead(profile_url);
if (!lead) {
return res.status(404).json({ success: false, error: 'Profile not analyzed yet. Call /api/leads/analyze first.' });
}
const result = await aiService.scoreLead(lead);
if (result.success) {
lead.score = result.score;
lead.score_reasoning = result.reasoning;
await database.saveLead(lead);
}
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/messages/generate', async (req, res) => {
try {
const { api_key, profile_url, value_proposition, message_type } = req.body;
if (!api_key || !profile_url || !value_proposition || !message_type) {
return res.status(400).json({ success: false, error: 'api_key, profile_url, value_proposition, and message_type are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not configured. Set GCP_PROJECT_ID and ensure gcloud auth is set up.' });
}
const lead = await database.getLead(profile_url);
if (!lead) {
return res.status(404).json({ success: false, error: 'Profile not analyzed yet' });
}
const previousMessages = await database.getMessages(lead.id);
const result = await aiService.generateMessage(lead, {
valueProposition: value_proposition,
messageType: message_type,
previousMessages
});
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/messages/send', async (req, res) => {
try {
const { api_key, profile_url, message, is_connection_request = false } = req.body;
if (!api_key || !profile_url || !message) {
return res.status(400).json({ success: false, error: 'api_key, profile_url, and message are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const usage = await database.checkUsageLimit(key.id, 'message_send');
if (!usage.allowed) {
return res.status(403).json({ success: false, error: usage.reason });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(400).json({ success: false, error: 'Browser not connected' });
}
const result = await linkedinBot.sendMessage(profile_url, message, is_connection_request);
if (result.success) {
const lead = await database.getLead(profile_url);
if (lead) {
await database.saveMessage({
lead_id: lead.id,
user_id: key.id,
profile_url,
message_text: message,
sent_at: new Date().toISOString(),
sequence_stage: 0,
response_received: false
});
}
}
res.json(result);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/sequences/create', async (req, res) => {
try {
const { api_key, profile_url, initial_message, num_followups = 3 } = req.body;
if (!api_key || !profile_url || !initial_message) {
return res.status(400).json({ success: false, error: 'api_key, profile_url, and initial_message are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const usage = await database.checkUsageLimit(key.id, 'sequence_create');
if (!usage.allowed) {
return res.status(403).json({ success: false, error: usage.reason });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not configured. Set GCP_PROJECT_ID and ensure gcloud auth is set up.' });
}
const lead = await database.getLead(profile_url);
if (!lead) {
return res.status(404).json({ success: false, error: 'Profile not analyzed yet' });
}
const sequenceResult = await aiService.generateFollowUpSequence(
lead,
initial_message,
num_followups
);
if (sequenceResult.success) {
await database.createSequence({
lead_id: lead.id,
user_id: key.id,
profile_url,
messages: sequenceResult.sequence,
is_active: true
});
}
res.json(sequenceResult);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/leads', async (req, res) => {
try {
const { api_key } = req.query;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const leads = await database.getLeads({ user_id: key.id });
res.json({ success: true, leads });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/usage', async (req, res) => {
try {
const { api_key } = req.query;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const limits = await database.getTierLimits(key.tier);
const currentMonth = new Date().toISOString().slice(0, 7);
// Get usage from database using the query method
const usageResult = await database.query(
'SELECT * FROM usage WHERE user_id = $1 AND month = $2',
[key.id, currentMonth]
);
const userUsage = usageResult.rows.length > 0 ? usageResult.rows[0] : {
profiles: 0,
messages: 0,
sequences: 0
};
res.json({
success: true,
usage: {
tier: key.tier,
limits,
current: {
profiles: userUsage.profiles || 0,
messages: userUsage.messages || 0,
sequences: userUsage.sequences || 0
},
remaining: {
profiles: limits.profiles === -1 ? -1 : limits.profiles - (userUsage.profiles || 0),
messages: limits.messages === -1 ? -1 : limits.messages - (userUsage.messages || 0),
sequences: limits.sequences === -1 ? -1 : limits.sequences - (userUsage.sequences || 0)
}
}
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// ============= COMPREHENSIVE CAMPAIGN MANAGEMENT ENDPOINTS =============
// Product Analysis & Management
app.post('/api/products/analyze', async (req, res) => {
try {
const { api_key, name, website_url, description, value_proposition, target_audience } = req.body;
if (!api_key || !name) {
return res.status(400).json({ success: false, error: 'api_key and name are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not available. Configure Vertex AI credentials.' });
}
const productInfo = { name, website_url, description, value_proposition, target_audience };
// Deep product analysis using AI
const analysisResult = await aiService.analyzeProduct(productInfo);
if (!analysisResult.success) {
return res.status(500).json(analysisResult);
}
// Save product with analysis
const product = await database.createProduct({
user_id: key.id,
name,
website_url,
description,
value_proposition,
target_audience,
analysis_data: analysisResult
});
res.json({ success: true, product, analysis: analysisResult });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/products', async (req, res) => {
try {
const { api_key, name, website_url, description, value_proposition, target_audience, analysis_data } = req.body;
if (!api_key || !name) {
return res.status(400).json({ success: false, error: 'api_key and name are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const product = await database.createProduct({
user_id: key.id,
name,
website_url,
description,
value_proposition,
target_audience,
analysis_data
});
res.json({ success: true, product });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/products', async (req, res) => {
try {
const { api_key } = req.query;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const products = await database.getProducts(key.id);
res.json({ success: true, products });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// ICP Generation & Management
app.post('/api/icps/generate', async (req, res) => {
try {
const { api_key, product_id, product_analysis } = req.body;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
// If product_analysis is not provided but product_id is, fetch it from database
let analysisData = product_analysis;
if (!analysisData && product_id) {
const product = await database.getProduct(product_id, key.id);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
if (product.analysis_data) {
analysisData = product.analysis_data;
} else {
return res.status(400).json({ success: false, error: 'Product has no analysis data. Please analyze the product first using /api/products/analyze' });
}
}
if (!analysisData) {
return res.status(400).json({ success: false, error: 'product_analysis is required (or provide product_id to fetch from database)' });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not available. Configure AI credentials (OpenAI or Vertex AI).' });
}
// Generate ICP based on product analysis and current trends
const icpResult = await aiService.generateICP(analysisData);
if (!icpResult.success) {
return res.status(500).json(icpResult);
}
// Save ICP
const icp = await database.createICP({
user_id: key.id,
product_id: product_id || null,
name: icpResult.icp_name,
description: icpResult.description,
search_criteria: icpResult.search_criteria,
target_characteristics: icpResult.target_characteristics
});
res.json({ success: true, icp, icp_data: icpResult });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/icps', async (req, res) => {
try {
const { api_key, product_id, name, description, search_criteria, target_characteristics } = req.body;
if (!api_key || !name || !search_criteria) {
return res.status(400).json({ success: false, error: 'api_key, name, and search_criteria are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const icp = await database.createICP({
user_id: key.id,
product_id,
name,
description,
search_criteria,
target_characteristics
});
res.json({ success: true, icp });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/icps', async (req, res) => {
try {
const { api_key, product_id } = req.query;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const icps = await database.getICPs(key.id, product_id || null);
res.json({ success: true, icps });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Campaign Management - Lead Discovery Based on ICP
app.post('/api/campaigns/discover', async (req, res) => {
try {
const { api_key, icp_id, icp_search_criteria, limit = 50 } = req.body;
if (!api_key || !icp_search_criteria) {
return res.status(400).json({ success: false, error: 'api_key and icp_search_criteria are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(503).json({ success: false, error: 'LinkedIn browser not connected. Call /api/browser/connect first.' });
}
// Extract search criteria from ICP
const { keywords, locations, industries, job_titles } = icp_search_criteria;
// Build LinkedIn search query
const searchParams = {
keywords: keywords || '',
location: Array.isArray(locations) ? locations[0] : locations || '',
limit: Math.min(limit, 100)
};
// Search for leads
const searchResult = await linkedinBot.searchLeads(searchParams);
if (!searchResult.success) {
return res.status(500).json(searchResult);
}
const leads = searchResult.data || [];
// Analyze and score each lead
const analyzedLeads = [];
for (const lead of leads.slice(0, limit)) {
try {
const analyzeResult = await linkedinBot.analyzeProfile(lead.profile_url);
if (analyzeResult.success && analyzeResult.data) {
const leadData = analyzeResult.data;
// Save lead to database
const savedLead = await database.saveLead({
user_id: key.id,
profile_url: lead.profile_url,
name: leadData.name || lead.name,
title: leadData.headline || leadData.experience?.[0]?.title || '',
company: leadData.experience?.[0]?.company || '',
location: leadData.location || '',
experience: leadData.experience || [],
education: leadData.education || [],
skills: leadData.skills || [],
summary: leadData.about || ''
});
// Score the lead with AI
let scoreResult = null;
if (aiService) {
scoreResult = await aiService.scoreLead(leadData);
if (scoreResult.success && scoreResult.score) {
await database.updateLead(savedLead.id, {
score: scoreResult.score,
score_reasoning: scoreResult.reasoning
});
}
}
analyzedLeads.push({
...savedLead,
score: scoreResult?.score || null,
score_reasoning: scoreResult?.reasoning || null,
strengths: scoreResult?.strengths || [],
weaknesses: scoreResult?.weaknesses || []
});
}
} catch (error) {
console.error(`Error analyzing lead ${lead.profile_url}:`, error.message);
// Continue with other leads
}
}
res.json({
success: true,
leads: analyzedLeads,
total_discovered: leads.length,
total_analyzed: analyzedLeads.length
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Campaign Creation & Management
app.post('/api/campaigns', async (req, res) => {
try {
const { api_key, product_id, icp_id, name, description, leads_selected, message_template, follow_up_sequence, settings } = req.body;
if (!api_key || !name) {
return res.status(400).json({ success: false, error: 'api_key and name are required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.createCampaign({
user_id: key.id,
product_id,
icp_id,
name,
description,
leads_selected: leads_selected || [],
message_template: message_template || {},
follow_up_sequence: follow_up_sequence || [],
settings: settings || {},
status: 'draft'
});
res.json({ success: true, campaign });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/campaigns', async (req, res) => {
try {
const { api_key, status } = req.query;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaigns = await database.getCampaigns(key.id, status || null);
res.json({ success: true, campaigns });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/campaigns/:id', async (req, res) => {
try {
const { api_key } = req.query;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
// Get campaign leads
const leads = await database.getCampaignLeads(id);
res.json({ success: true, campaign, leads });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Generate Campaign Sequence (Dynamic, Not Template-Based)
app.post('/api/campaigns/:id/generate-sequence', async (req, res) => {
try {
const { api_key } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
if (!aiService) {
return res.status(503).json({ success: false, error: 'AI service not available' });
}
// Get product analysis
let productAnalysis = null;
if (campaign.product_id) {
const product = await database.getProduct(campaign.product_id, key.id);
if (product && product.analysis_data) {
productAnalysis = product.analysis_data;
}
}
// Get ICP data
let icpData = null;
if (campaign.icp_id) {
const icp = await database.getICP(campaign.icp_id, key.id);
if (icp) {
icpData = {
search_criteria: icp.search_criteria,
target_characteristics: icp.target_characteristics
};
}
}
if (!productAnalysis || !icpData) {
return res.status(400).json({ success: false, error: 'Campaign must have product and ICP to generate sequence' });
}
// Generate dynamic sequence
const sequenceResult = await aiService.generateCampaignSequence(productAnalysis, icpData, 3);
if (!sequenceResult.success) {
return res.status(500).json(sequenceResult);
}
// Update campaign with sequence
const updatedCampaign = await database.updateCampaign(id, key.id, {
message_template: sequenceResult.initial_message,
follow_up_sequence: sequenceResult.follow_up_sequence
});
res.json({ success: true, campaign: updatedCampaign, sequence: sequenceResult });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Select Leads for Campaign (User Approval Workflow)
app.post('/api/campaigns/:id/leads/select', async (req, res) => {
try {
const { api_key, lead_ids, profile_urls } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
const selectedLeads = [];
const leadsToSelect = lead_ids || [];
const profileUrlsToSelect = profile_urls || [];
// Add leads by ID
for (const leadId of leadsToSelect) {
const lead = await database.query('SELECT * FROM leads WHERE id = $1 AND user_id = $2', [leadId, key.id]);
if (lead.rows.length > 0) {
const campaignLead = await database.addCampaignLead({
campaign_id: id,
lead_id: leadId,
profile_url: lead.rows[0].profile_url,
status: 'selected'
});
selectedLeads.push(campaignLead);
}
}
// Add leads by profile URL
for (const profileUrl of profileUrlsToSelect) {
const lead = await database.getLead(profileUrl);
if (lead && lead.user_id === key.id) {
const campaignLead = await database.addCampaignLead({
campaign_id: id,
lead_id: lead.id,
profile_url: profileUrl,
status: 'selected'
});
selectedLeads.push(campaignLead);
}
}
// Update campaign with selected leads
const currentLeads = campaign.leads_selected || [];
const newLeads = [...new Set([...currentLeads, ...profileUrlsToSelect])];
await database.updateCampaign(id, key.id, {
leads_selected: newLeads,
status: 'pending_approval'
});
res.json({ success: true, selected_leads: selectedLeads, campaign_status: 'pending_approval' });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Approve Campaign (Ready to start)
app.post('/api/campaigns/:id/approve', async (req, res) => {
try {
const { api_key } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.updateCampaign(id, key.id, {
status: 'approved'
});
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
res.json({ success: true, campaign, message: 'Campaign approved and ready to start' });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Start Campaign (Begin automated sending)
app.post('/api/campaigns/:id/start', async (req, res) => {
try {
const { api_key } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
if (campaign.status !== 'approved') {
return res.status(400).json({ success: false, error: 'Campaign must be approved before starting' });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(503).json({ success: false, error: 'LinkedIn browser not connected' });
}
// Update campaign status
await database.updateCampaign(id, key.id, {
status: 'active',
started_at: new Date().toISOString()
});
// Start automated background worker for continuous message processing
startCampaignWorker(id, key.id).catch(error => {
console.error(`Error starting campaign worker ${id}:`, error);
});
// Immediately process initial batch of messages
processCampaignMessages(id, key.id).catch(error => {
console.error(`Error processing campaign ${id} messages:`, error);
});
res.json({ success: true, message: 'Campaign started. Automated message processing is now active.', campaign_id: id });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Process Campaign Messages (Send pending messages)
app.post('/api/campaigns/:id/process', async (req, res) => {
try {
const { api_key } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign || campaign.status !== 'active') {
return res.status(400).json({ success: false, error: 'Campaign not active' });
}
if (!linkedinBot || !linkedinBot.isConnected) {
return res.status(503).json({ success: false, error: 'LinkedIn browser not connected' });
}
// Get leads ready to receive messages
const leadsToProcess = await database.getCampaignLeadsForSending(id);
const results = [];
const messageTemplate = campaign.message_template || {};
const followUpSequence = campaign.follow_up_sequence || [];
for (const campaignLead of leadsToProcess.slice(0, 10)) { // Process max 10 at a time
try {
const leadResult = await database.query(
'SELECT * FROM leads WHERE profile_url = $1 AND user_id = $2',
[campaignLead.profile_url, key.id]
);
const lead = leadResult.rows[0];
if (!lead) continue;
// Determine which message to send
let messageText = '';
let isConnectionRequest = false;
if (campaignLead.sequence_stage === 0) {
// Initial message
messageText = messageTemplate.message || '';
isConnectionRequest = true;
} else {
// Follow-up message
const followUp = followUpSequence[campaignLead.sequence_stage - 1];
if (followUp) {
messageText = followUp.message || '';
} else {
// No more follow-ups
await database.updateCampaignLead(campaignLead.id, { status: 'completed' });
continue;
}
}
if (!messageText) {
continue;
}
// Generate personalized message if AI is available
if (aiService && messageTemplate.message) {
const personalizedResult = await aiService.generateMessage(lead, {
value_proposition: messageTemplate.message,
message_type: isConnectionRequest ? 'connection' : 'direct'
});
if (personalizedResult.success && personalizedResult.message) {
messageText = personalizedResult.message;
}
}
// Send message
const sendResult = await linkedinBot.sendMessage(campaignLead.profile_url, messageText, isConnectionRequest);
if (sendResult.success) {
// Save message
const message = await database.saveMessage({
lead_id: campaignLead.lead_id,
user_id: key.id,
profile_url: campaignLead.profile_url,
message_text: messageText,
sent_at: new Date().toISOString(),
sequence_stage: campaignLead.sequence_stage
});
// Update campaign lead
const nextStage = campaignLead.sequence_stage + 1;
const nextFollowUp = followUpSequence[nextStage - 1];
const nextScheduledAt = nextFollowUp
? new Date(Date.now() + (nextFollowUp.days_after_previous || 3) * 24 * 60 * 60 * 1000).toISOString()
: null;
await database.updateCampaignLead(campaignLead.id, {
status: 'message_sent',
sequence_stage: nextStage,
last_message_sent_at: new Date().toISOString(),
next_message_scheduled_at: nextScheduledAt
});
// Track message event
await database.createMessageEvent({
message_id: message.id,
campaign_id: id,
event_type: 'sent',
event_data: { sequence_stage: campaignLead.sequence_stage }
});
results.push({ success: true, lead: campaignLead.profile_url, message_id: message.id });
} else {
results.push({ success: false, lead: campaignLead.profile_url, error: sendResult.error });
}
} catch (error) {
console.error(`Error processing lead ${campaignLead.profile_url}:`, error);
results.push({ success: false, lead: campaignLead.profile_url, error: error.message });
}
}
res.json({ success: true, processed: results.length, results });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Pause Campaign
app.post('/api/campaigns/:id/pause', async (req, res) => {
try {
const { api_key } = req.body;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.updateCampaign(id, key.id, {
status: 'paused'
});
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
// Stop automated worker
stopCampaignWorker(id);
res.json({ success: true, campaign, message: 'Campaign paused. Automated processing stopped.' });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Campaign Analytics
app.get('/api/campaigns/:id/analytics', async (req, res) => {
try {
const { api_key, start_date, end_date } = req.query;
const { id } = req.params;
if (!api_key) {
return res.status(400).json({ success: false, error: 'api_key is required' });
}
const key = await database.validateApiKey(api_key);
if (!key) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
const campaign = await database.getCampaign(id, key.id);
if (!campaign) {
return res.status(404).json({ success: false, error: 'Campaign not found' });
}
// Get analytics
const analytics = await database.getCampaignAnalytics(id, start_date || null, end_date || null);
// Get campaign leads stats
const allLeads = await database.getCampaignLeads(id);
const leadsStats = {
total: allLeads.length,
selected: allLeads.filter(l => l.status === 'selected').length,
message_sent: allLeads.filter(l => l.status === 'message_sent').length,
replied: allLeads.filter(l => l.status === 'replied').length,
connected: allLeads.filter(l => l.status === 'connected').length,
not_interested: allLeads.filter(l => l.status === 'not_interested').length
};
// Get message events
const eventsResult = await database.query(
`SELECT event_type, COUNT(*) as count
FROM message_events
WHERE campaign_id = $1
GROUP BY event_type`,
[id]
);
const eventsStats = {};
for (const row of eventsResult.rows) {
eventsStats[row.event_type] = parseInt(row.count);
}
res.json({
success: true,
campaign,
analytics,
leads_stats: leadsStats,
events_stats: eventsStats
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Export for Vercel serverless
export default app;
// Start server if running directly (not on Vercel)
if (process.env.VERCEL !== '1') {
async function startServer() {
await initializeServices();
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`🚀 LinkedIn Lead Automation HTTP API running on http://localhost:${PORT}`);
console.log(`📚 Health check: http://localhost:${PORT}/health`);
console.log(`📖 API Documentation available at http://localhost:${PORT}/health`);
});
}
startServer().catch(console.error);
}