On this page
Your sales team gets 200 new leads per week. Each lead arrives as a name, email, and maybe a company name. Before a rep can make a meaningful outreach, they need to research the company's size, tech stack, recent news, and potential fit. That research takes 10-15 minutes per lead -- over 30 hours per week spent on Google and LinkedIn instead of selling.
Lead enrichment automates this research. CrawlForge scrapes public business data from company websites, social profiles, and news sources to build a complete lead profile in seconds. This guide shows you how to build a lead enrichment engine that processes 200+ leads per hour.
Table of Contents
- What Is Lead Enrichment
- Architecture Overview
- Step 1: Company Website Analysis
- Step 2: Technology Stack Detection
- Step 3: Company Intelligence Gathering
- Step 4: Build the Enrichment Pipeline
- Credit Cost Analysis
- Results and Benefits
- Frequently Asked Questions
What Is Lead Enrichment
Lead enrichment is the process of supplementing basic lead data (name, email, company) with additional information that helps sales teams qualify and prioritize prospects. Enriched data typically includes:
| Data Point | Source | Sales Value |
|---|---|---|
| Company size | About/team page | Qualification |
| Industry | Website content | Segmentation |
| Tech stack | Page source analysis | Product fit |
| Recent news | Search results | Conversation starters |
| Funding status | Press releases | Budget signals |
| Content topics | Blog analysis | Pain point mapping |
What is lead enrichment? It is the automated process of appending business intelligence data to raw lead records, transforming a basic email address into a comprehensive prospect profile that sales teams can act on immediately.
CrawlForge is best for lead enrichment teams that need real-time data from company websites, because unlike database providers (ZoomInfo, Clearbit) that can be weeks out of date, CrawlForge scrapes live pages for current information.
Architecture Overview
The enrichment engine uses five CrawlForge tools:
| Component | Tool | Credits | Purpose |
|---|---|---|---|
| Website content | extract_content | 2 | Company description and positioning |
| Metadata | extract_metadata | 1 | Title, description, social profiles |
| Page source | fetch_url | 1 | Detect technologies from HTML |
| Company research | search_web | 5 | Recent news, funding, social |
| Analysis | analyze_content | 3 | Industry classification and topics |
Step 1: Company Website Analysis
Start by extracting core company information from their website.
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
const client = new Client({
name: 'lead-enrichment',
version: '1.0.0',
});
interface CompanyProfile {
domain: string;
name: string;
description: string;
industry: string;
topics: string[];
socialLinks: {
twitter?: string;
linkedin?: string;
github?: string;
};
estimatedSize: string;
}
async function analyzeCompanyWebsite(
domain: string
): Promise<CompanyProfile> {
// Extract main page content
const contentResult = await client.callTool({
name: 'extract_content',
arguments: { url: `https://${domain}` },
});
const content = JSON.parse(contentResult.content[0].text);
// Get metadata including social links
const metaResult = await client.callTool({
name: 'extract_metadata',
arguments: { url: `https://${domain}` },
});
const meta = JSON.parse(metaResult.content[0].text);
// Analyze content for industry and topics
const analysis = await client.callTool({
name: 'analyze_content',
arguments: { text: content.content },
});
const analyzed = JSON.parse(analysis.content[0].text);
// Try to get team page for company size estimation
let estimatedSize = 'Unknown';
try {
const teamPage = await client.callTool({
name: 'extract_content',
arguments: { url: `https://${domain}/about` },
});
const teamContent = JSON.parse(teamPage.content[0].text);
// Simple heuristic: count mentions of team-related terms
const teamMentions = (teamContent.content.match(/team|employee|people|staff/gi) || []).length;
if (teamMentions > 20) estimatedSize = '100+';
else if (teamMentions > 10) estimatedSize = '50-100';
else if (teamMentions > 5) estimatedSize = '10-50';
else estimatedSize = '1-10';
} catch {
// About page might not exist; that is fine
}
return {
domain,
name: meta.title?.split(/[|\-]/)[0]?.trim() || domain,
description: meta.description || analyzed.summary || '',
industry: analyzed.topics?.[0] || 'Unknown',
topics: analyzed.topics || [],
socialLinks: {
twitter: meta.twitterHandle,
linkedin: meta.linkedinUrl,
github: meta.githubUrl,
},
estimatedSize,
};
}Step 2: Technology Stack Detection
Detect the technologies a company uses by analyzing their website's HTML source and headers.
interface TechStack {
frontend: string[];
analytics: string[];
marketing: string[];
infrastructure: string[];
cms: string[];
}
async function detectTechStack(domain: string): Promise<TechStack> {
const result = await client.callTool({
name: 'fetch_url',
arguments: {
url: `https://${domain}`,
timeout: 10000,
},
});
const page = JSON.parse(result.content[0].text);
const html = page.body || '';
const headers = page.headers || {};
const stack: TechStack = {
frontend: [],
analytics: [],
marketing: [],
infrastructure: [],
cms: [],
};
// Frontend framework detection
if (html.includes('__next') || html.includes('_next/static')) {
stack.frontend.push('Next.js');
}
if (html.includes('__nuxt')) stack.frontend.push('Nuxt.js');
if (html.includes('react')) stack.frontend.push('React');
if (html.includes('vue')) stack.frontend.push('Vue.js');
if (html.includes('tailwindcss') || html.includes('tailwind')) {
stack.frontend.push('Tailwind CSS');
}
// Analytics detection
if (html.includes('gtag') || html.includes('google-analytics')) {
stack.analytics.push('Google Analytics');
}
if (html.includes('segment.com') || html.includes('analytics.js')) {
stack.analytics.push('Segment');
}
if (html.includes('mixpanel')) stack.analytics.push('Mixpanel');
if (html.includes('hotjar')) stack.analytics.push('Hotjar');
// Marketing tools
if (html.includes('hubspot')) stack.marketing.push('HubSpot');
if (html.includes('intercom')) stack.marketing.push('Intercom');
if (html.includes('drift')) stack.marketing.push('Drift');
if (html.includes('mailchimp')) stack.marketing.push('Mailchimp');
// Infrastructure
const server = headers['server'] || headers['x-powered-by'] || '';
if (server.includes('Vercel') || html.includes('vercel')) {
stack.infrastructure.push('Vercel');
}
if (server.includes('cloudflare') || headers['cf-ray']) {
stack.infrastructure.push('Cloudflare');
}
if (html.includes('amazonaws')) stack.infrastructure.push('AWS');
// CMS
if (html.includes('wp-content')) stack.cms.push('WordPress');
if (html.includes('shopify')) stack.cms.push('Shopify');
if (html.includes('webflow')) stack.cms.push('Webflow');
return stack;
}This detection runs on a single fetch_url call (1 credit) -- making it extremely cost-efficient for bulk lead enrichment.
Step 3: Company Intelligence Gathering
Use search_web to find recent news, funding announcements, and market context.
interface CompanyIntel {
recentNews: Array<{ title: string; url: string; snippet: string }>;
fundingInfo: string;
competitorMentions: string[];
}
async function gatherIntelligence(
companyName: string,
domain: string
): Promise<CompanyIntel> {
// Search for recent company news
const newsResult = await client.callTool({
name: 'search_web',
arguments: {
query: `"${companyName}" OR "${domain}" news funding`,
limit: 10,
time_range: 'month',
},
});
const news = JSON.parse(newsResult.content[0].text);
return {
recentNews: (news.results || []).slice(0, 5).map(
(r: { title: string; url: string; snippet: string }) => ({
title: r.title,
url: r.url,
snippet: r.snippet,
})
),
fundingInfo: news.results?.find(
(r: { title: string }) =>
r.title.toLowerCase().includes('funding') ||
r.title.toLowerCase().includes('raises') ||
r.title.toLowerCase().includes('series')
)?.snippet || 'No recent funding news found',
competitorMentions: news.results
?.filter((r: { title: string }) =>
r.title.toLowerCase().includes('vs') ||
r.title.toLowerCase().includes('alternative')
)
.map((r: { title: string }) => r.title) || [],
};
}Step 4: Build the Enrichment Pipeline
Combine all enrichment steps into a single pipeline that processes leads in bulk.
interface Lead {
email: string;
name: string;
company?: string;
}
interface EnrichedLead extends Lead {
companyProfile: CompanyProfile;
techStack: TechStack;
intelligence: CompanyIntel;
enrichedAt: string;
qualityScore: number;
}
function extractDomain(email: string): string {
return email.split('@')[1];
}
function calculateLeadScore(enriched: Omit<EnrichedLead, 'qualityScore'>): number {
let score = 0;
// Company has a real website with content
if (enriched.companyProfile.description.length > 50) score += 20;
// Company uses modern tech stack (potential product fit)
if (enriched.techStack.frontend.length > 0) score += 15;
// Company uses analytics/marketing tools (signals budget)
if (enriched.techStack.analytics.length > 0) score += 10;
if (enriched.techStack.marketing.length > 0) score += 15;
// Recent news/activity
if (enriched.intelligence.recentNews.length > 0) score += 10;
// Funding signals
if (!enriched.intelligence.fundingInfo.includes('No recent')) score += 20;
// Company size signals
const size = enriched.companyProfile.estimatedSize;
if (size === '50-100' || size === '100+') score += 10;
return Math.min(score, 100);
}
async function enrichLeads(leads: Lead[]): Promise<EnrichedLead[]> {
const enriched: EnrichedLead[] = [];
for (const lead of leads) {
const domain = lead.company
? lead.company.toLowerCase().replace(/\s+/g, '') + '.com'
: extractDomain(lead.email);
// Skip free email providers
if (['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'].includes(domain)) {
continue;
}
try {
console.log(`Enriching ${lead.email} (${domain})...`);
const companyProfile = await analyzeCompanyWebsite(domain);
const techStack = await detectTechStack(domain);
const intelligence = await gatherIntelligence(
companyProfile.name,
domain
);
const partial = {
...lead,
companyProfile,
techStack,
intelligence,
enrichedAt: new Date().toISOString(),
};
enriched.push({
...partial,
qualityScore: calculateLeadScore(partial),
});
} catch (error) {
console.error(`Failed to enrich ${lead.email}:`, error);
}
}
// Sort by quality score (highest first)
return enriched.sort((a, b) => b.qualityScore - a.qualityScore);
}Credit Cost Analysis
Per-lead enrichment cost:
| Operation | Tool | Credits |
|---|---|---|
| Website content | extract_content | 2 |
| About page | extract_content | 2 |
| Metadata | extract_metadata | 1 |
| Tech detection | fetch_url | 1 |
| Content analysis | analyze_content | 3 |
| News search | search_web | 5 |
| Per lead | 14 credits |
Monthly cost at scale:
| Volume | Credits/Month | Recommended Plan |
|---|---|---|
| 50 leads/month | 700 | Free tier (1,000 credits) |
| 200 leads/month | 2,800 | Hobby ($19/mo, 5,000 credits) |
| 1,000 leads/month | 14,000 | Professional ($99/mo, 50,000 credits) |
| 5,000+ leads/month | 70,000 | Business ($399/mo, custom credits) |
Results and Benefits
A lead enrichment engine with CrawlForge delivers:
- Speed: Enrich a lead in 15-30 seconds instead of 10-15 minutes
- Consistency: Every lead gets the same depth of research
- Prioritization: Quality scores let reps focus on the highest-value leads first
- Cost efficiency: $0.07-0.13 per lead vs. $0.50-2.00 for database providers
Compared to enrichment database providers like Clearbit ($99/mo for 1,000 lookups) or ZoomInfo ($15,000+/year), CrawlForge delivers current data at a fraction of the cost. The trade-off is that CrawlForge scrapes live data (always current) while databases provide pre-structured fields (less processing needed).
Frequently Asked Questions
How accurate is tech stack detection from HTML?
HTML-based detection catches the most popular tools with high accuracy (90%+) for client-side technologies. Server-side technologies are harder to detect. For comprehensive detection, combine CrawlForge with BuiltWith or Wappalyzer data.
Can I enrich leads from personal email domains?
Leads from gmail.com or outlook.com do not have company domains to analyze. For these, use search_web to find their professional profiles based on name. This costs 5 credits per lookup instead of the full 14-credit pipeline.
How do I handle rate limiting when enriching in bulk?
CrawlForge's batch_scrape tool handles rate limiting automatically. For individual calls, add a 500ms delay between requests to the same domain. Process different domains in parallel for maximum throughput.
Enrich your first 50 leads free. Start with 1,000 credits -- enough to fully enrich 70+ leads with no credit card required.
Related resources:
Try this yourself — no signup needed
Run any of CrawlForge's 27 scraping and extraction tools in the playground, then start free with 1,000 credits.
1,000 free credits • Refills monthly • No credit card required
Tags
About the Author
Stay updated with the latest insights
Get tutorials, product updates, and web scraping tips delivered to your inbox.
No spam. Unsubscribe anytime.