If you sell to businesses, your competitors are running LinkedIn ads right now. And unlike Facebook or Google, LinkedIn ads target job titles, company sizes, and industries. Every ad in their library tells you exactly who they think their buyer is.
LinkedIn quietly launched their Ad Library, and almost nobody in the B2B space is using it systematically. Let's fix that.
Here's what we're building:
- Search every ad a competitor is running on LinkedIn
- Track ad longevity to find proven winners
- Analyze messaging themes across B2B competitors
- Spot positioning shifts before your sales team does
Why LinkedIn Ads Are Different
Facebook ads reach consumers. Google ads catch intent. But LinkedIn ads reveal B2B strategy in a way nothing else does:
- The copy tells you what value props they're betting on
- The targeting (inferred from copy) tells you what buyer persona they're after
- The creative format tells you what stage of the funnel they're investing in
- The longevity tells you what's actually generating pipeline
A LinkedIn ad that's been running for 3 months? That's generating demos at an acceptable CAC. Period.
The Stack
- Node.js: Runtime
- SociaVault API: LinkedIn Ad Library + Company endpoints
- better-sqlite3: Ad tracking database
- OpenAI: Strategic analysis
Step 1: Setup
mkdir linkedin-ad-spy
cd linkedin-ad-spy
npm init -y
npm install axios better-sqlite3 openai dotenv
Create .env:
SOCIAVAULT_API_KEY=your_key_here
OPENAI_API_KEY=your_openai_key
Step 2: Tracking Database
Create db.js:
const Database = require('better-sqlite3');
const db = new Database('linkedin-ads.db');
db.exec(`
CREATE TABLE IF NOT EXISTS companies (
id TEXT PRIMARY KEY,
name TEXT,
industry TEXT,
employee_count TEXT,
tagline TEXT,
added_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS ads (
id TEXT PRIMARY KEY,
company_id TEXT,
content TEXT,
format TEXT,
first_seen TEXT DEFAULT (datetime('now')),
last_seen TEXT DEFAULT (datetime('now')),
is_active BOOLEAN DEFAULT 1,
days_running INTEGER DEFAULT 0,
FOREIGN KEY (company_id) REFERENCES companies(id)
);
`);
module.exports = db;
Step 3: Get Company Intelligence
Before analyzing ads, pull the company profile for context:
Create spy.js:
require('dotenv').config();
const axios = require('axios');
const db = require('./db');
const OpenAI = require('openai');
const API_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function getCompanyProfile(companyUrl) {
console.log(`š¢ Pulling LinkedIn company profile...\n`);
const { data } = await axios.get(
`${API_BASE}/v1/scrape/linkedin/company`,
{ params: { url: companyUrl }, headers }
);
const company = data.data;
console.log(` Company: ${company.name}`);
console.log(` Industry: ${company.industry || 'N/A'}`);
console.log(` Size: ${company.employeeCount || company.staffCount || 'N/A'}`);
console.log(` Tagline: ${company.tagline || company.description?.substring(0, 100) || 'N/A'}`);
console.log();
// Store company
db.prepare(`
INSERT OR REPLACE INTO companies (id, name, industry, employee_count, tagline)
VALUES (?, ?, ?, ?, ?)
`).run(
company.universalName || company.id || companyUrl,
company.name,
company.industry,
company.employeeCount || company.staffCount,
company.tagline
);
return company;
}
Step 4: Pull All LinkedIn Ads
Search the LinkedIn Ad Library for a company's ads:
async function searchCompanyAds(companyName, options = {}) {
console.log(`š„ Searching LinkedIn ads for "${companyName}"...\n`);
let allAds = [];
let page = 0;
while (page < (options.maxPages || 5)) {
const params = {
query: companyName,
...(options.country && { country: options.country }),
...(page > 0 && { offset: page * 20 })
};
const { data } = await axios.get(
`${API_BASE}/v1/scrape/linkedin-ad-library/search`,
{ params, headers }
);
const ads = data.data?.ads || data.data || [];
allAds = allAds.concat(ads);
console.log(` Page ${page + 1}: ${ads.length} ads found`);
if (ads.length < 20) break;
page++;
await new Promise(r => setTimeout(r, 1000));
}
console.log(` Total: ${allAds.length} ads\n`);
// Store ads
const upsert = db.prepare(`
INSERT INTO ads (id, company_id, content, format)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
last_seen = datetime('now'),
is_active = 1,
days_running = CAST(
(julianday('now') - julianday(COALESCE(ads.first_seen, datetime('now')))) AS INTEGER
)
`);
const tx = db.transaction(() => {
for (const ad of allAds) {
upsert.run(
ad.id || ad.adId || `lad_${Math.random().toString(36).slice(2)}`,
companyName.toLowerCase().replace(/\s+/g, '-'),
JSON.stringify({
headline: ad.headline || ad.title,
body: ad.body || ad.description || ad.introText,
ctaText: ad.ctaText || ad.callToAction,
imageUrl: ad.imageUrl,
videoUrl: ad.videoUrl,
advertiserName: ad.advertiserName || ad.companyName,
startDate: ad.startDate || ad.impressionStart,
}),
ad.format || (ad.videoUrl ? 'video' : 'image')
);
}
});
tx();
return allAds;
}
Step 5: Get Deep Detail on Individual Ads
async function getAdDetails(adUrl) {
console.log(`š Fetching ad details...\n`);
const { data } = await axios.get(
`${API_BASE}/v1/scrape/linkedin-ad-library/ad-details`,
{ params: { url: adUrl }, headers }
);
const ad = data.data;
console.log(' š AD DETAILS');
console.log(' ' + 'ā'.repeat(50));
if (ad.advertiserName) console.log(` Company: ${ad.advertiserName}`);
if (ad.headline) console.log(` Headline: ${ad.headline}`);
if (ad.body) console.log(` Body: ${ad.body?.substring(0, 200)}`);
if (ad.ctaText) console.log(` CTA: ${ad.ctaText}`);
if (ad.format) console.log(` Format: ${ad.format}`);
if (ad.startDate) console.log(` Running since: ${ad.startDate}`);
return ad;
}
Step 6: B2B Ad Strategy Analyzer
This is where it gets interesting. B2B ad copy reveals positioning strategy:
async function analyzeB2BStrategy(companyName) {
const companyId = companyName.toLowerCase().replace(/\s+/g, '-');
const ads = db.prepare(`
SELECT *,
json_extract(content, '$.headline') as headline,
json_extract(content, '$.body') as body,
json_extract(content, '$.ctaText') as cta
FROM ads
WHERE company_id = ?
ORDER BY days_running DESC
`).all(companyId);
if (ads.length < 2) {
console.log('Need more ad data. Run spy first.');
return;
}
const adSample = ads.slice(0, 25).map(ad => ({
headline: ad.headline,
body: ad.body?.substring(0, 200),
cta: ad.cta,
format: ad.format,
daysRunning: ad.days_running,
}));
console.log(`\nš§ Analyzing B2B strategy from ${adSample.length} ads...\n`);
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Analyze this B2B company's LinkedIn ad strategy:
Company: ${companyName}
Ads: ${JSON.stringify(adSample, null, 2)}
Return JSON:
{
"positioning": "How they position themselves in 2-3 sentences",
"target_persona": {
"job_titles": ["likely target job titles based on language"],
"seniority": "C-suite / VP / Director / Manager / IC",
"company_size": "Enterprise / Mid-market / SMB",
"pain_points_targeted": ["specific problems they address"]
},
"messaging_themes": ["recurring themes in their copy"],
"value_propositions": ["specific claims they make"],
"funnel_stages": {
"awareness": "% of ads focused on awareness",
"consideration": "% focused on consideration",
"conversion": "% focused on direct conversion"
},
"competitive_angles": ["how they differentiate from alternatives"],
"creative_strategy": "video vs image vs carousel breakdown and strategy",
"cta_patterns": ["call to action types used"],
"weaknesses": ["gaps or weaknesses in their ad strategy"],
"counter_playbook": [
"How to position against them",
"What messaging would counter theirs",
"What audience segments they're missing"
]
}`
}],
response_format: { type: 'json_object' }
});
const analysis = JSON.parse(completion.choices[0].message.content);
console.log('šÆ B2B AD STRATEGY ANALYSIS');
console.log('ā'.repeat(55));
console.log(`\nš Positioning: ${analysis.positioning}`);
console.log(`\nš¤ Target Buyer:`);
console.log(` Titles: ${analysis.target_persona.job_titles.join(', ')}`);
console.log(` Seniority: ${analysis.target_persona.seniority}`);
console.log(` Company: ${analysis.target_persona.company_size}`);
console.log(`\nšÆ Pain Points They Target:`);
analysis.target_persona.pain_points_targeted.forEach(p => console.log(` ⢠${p}`));
console.log(`\nš Value Props:`);
analysis.value_propositions.forEach(v => console.log(` ⢠${v}`));
console.log(`\nš Funnel Split:`);
console.log(` Awareness: ${analysis.funnel_stages.awareness}`);
console.log(` Consideration: ${analysis.funnel_stages.consideration}`);
console.log(` Conversion: ${analysis.funnel_stages.conversion}`);
console.log(`\nāļø Counter-Playbook:`);
analysis.counter_playbook.forEach((c, i) => console.log(` ${i+1}. ${c}`));
return analysis;
}
Step 7: Multi-Competitor Comparison
The real power ā compare positioning across an entire competitive landscape:
async function compareCompetitors(companyNames) {
console.log(`\nš Comparing ${companyNames.length} competitors...\n`);
const analyses = {};
for (const name of companyNames) {
console.log(`āā Analyzing ${name}...`);
await searchCompanyAds(name);
analyses[name] = await analyzeB2BStrategy(name);
await new Promise(r => setTimeout(r, 2000));
}
// Summary comparison
console.log('\n\nš COMPETITIVE LANDSCAPE SUMMARY');
console.log('ā'.repeat(60));
for (const [name, analysis] of Object.entries(analyses)) {
if (!analysis) continue;
console.log(`\nš¢ ${name}`);
console.log(` Position: ${analysis.positioning?.substring(0, 100)}`);
console.log(` Target: ${analysis.target_persona?.seniority} at ${analysis.target_persona?.company_size}`);
console.log(` Key prop: ${analysis.value_propositions?.[0]}`);
console.log(` Weakness: ${analysis.weaknesses?.[0]}`);
}
}
Step 8: Monitor for New Ad Launches
async function monitorNewAds(companies) {
console.log('š Checking for new ads...\n');
for (const company of companies) {
const existingIds = new Set(
db.prepare('SELECT id FROM ads WHERE company_id = ?')
.all(company.toLowerCase().replace(/\s+/g, '-'))
.map(r => r.id)
);
const currentAds = await searchCompanyAds(company, { maxPages: 2 });
const newAds = currentAds.filter(
ad => !existingIds.has(ad.id || ad.adId)
);
if (newAds.length > 0) {
console.log(` š ${company}: ${newAds.length} NEW ADS!`);
newAds.forEach(ad => {
console.log(` ā ${ad.headline || ad.title || 'Untitled'}`);
});
} else {
console.log(` ā ${company}: No new ads`);
}
await new Promise(r => setTimeout(r, 1500));
}
}
Step 9: CLI
async function main() {
const command = process.argv[2];
const target = process.argv.slice(3).join(' ');
switch (command) {
case 'company':
await getCompanyProfile(target);
break;
case 'spy':
await searchCompanyAds(target);
await analyzeB2BStrategy(target);
break;
case 'ad':
await getAdDetails(target);
break;
case 'compare':
const companies = target.split(',').map(c => c.trim());
await compareCompetitors(companies);
break;
case 'monitor':
const watchlist = target.split(',').map(c => c.trim());
await monitorNewAds(watchlist);
break;
default:
console.log('LinkedIn Ad Spy Tool\n');
console.log('Commands:');
console.log(' node spy.js company <linkedin-url> - Get company profile');
console.log(' node spy.js spy "HubSpot" - Full ad intelligence');
console.log(' node spy.js ad <ad-url> - Inspect specific ad');
console.log(' node spy.js compare "A, B, C" - Compare competitors');
console.log(' node spy.js monitor "A, B, C" - Check for new ads');
}
}
main().catch(console.error);
Running It
# Get company info first
node spy.js company "https://linkedin.com/company/hubspot"
# Full spy analysis
node spy.js spy "HubSpot"
# Compare your main competitors
node spy.js compare "HubSpot, Salesforce, Pipedrive"
# Daily monitoring
node spy.js monitor "HubSpot, Salesforce, Pipedrive"
What This Reveals That Nothing Else Does
LinkedIn ads are uniquely valuable for B2B intelligence because:
- Buyer persona: The language reveals exactly who they're targeting
- Funnel investment: Demo CTAs vs. content downloads show funnel maturity
- Positioning shifts: New messaging themes = new strategic direction
- Budget priorities: More ad variations on a topic = they're doubling down
If a competitor suddenly launches 15 new ads about "AI-powered analytics" when they used to talk about "reporting" ā that's a strategy shift your sales team needs to know about.
Cost Comparison
| Tool | Monthly Cost | LinkedIn Ads |
|---|---|---|
| Semrush | $249/mo | No LinkedIn support |
| AdClarity | $169/mo | Limited LinkedIn data |
| Pathmatics | Enterprise pricing | Requires demo |
| This tool | ~$0.05/analysis | Full ad library + AI analysis |
Get Started
- Get your API key at sociavault.com
- Pick your top 3 B2B competitors
- Run the comparison and share with your marketing team
Their LinkedIn ad budget is basically a public strategy document. Read it.
In B2B, your competitor's LinkedIn ads tell you who they're selling to, what they're promising, and what's actually working. That's not espionage ā it's the Ad Library.
Top comments (0)