Your competitor just 10x'd their revenue. You saw it on Twitter. Their founder's bragging about it.
What changed? Their ads.
Every Facebook ad ever run is public. Meta's Ad Library exists. But scraping it manually is painful ā you're clicking through pages, can't filter properly, and there's no way to track changes over time.
So let's build a tool that does it for you. A Facebook Ad Spy that:
- Finds every ad a company is running
- Tracks how long they've been running (longer = profitable)
- Analyzes ad copy patterns and formats
- Alerts you when competitors launch new campaigns
Why Ad Longevity Matters
Here's the thing most people miss: if an ad has been running for 30+ days, it's profitable.
No sane media buyer keeps a losing ad running for a month. So the simplest competitive intelligence hack is: find ads that have been running the longest.
That's it. No fancy AI. No complex analysis. Just time.
Facebook's Ad Library lets you see this data. We're going to automate extracting it.
The Stack
- Node.js: Runtime
- SociaVault API: Facebook Ad Library endpoints
- OpenAI: Ad copy analysis
- SQLite (via better-sqlite3): Local storage for tracking
Step 1: Setup
mkdir fb-ad-spy
cd fb-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: Initialize the Database
We need to track ads over time to detect new launches and long-running winners.
Create db.js:
const Database = require('better-sqlite3');
const path = require('path');
const db = new Database(path.join(__dirname, 'ad-spy.db'));
db.exec(`
CREATE TABLE IF NOT EXISTS companies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
page_id TEXT UNIQUE,
added_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS ads (
id TEXT PRIMARY KEY,
company_id INTEGER,
headline TEXT,
body TEXT,
link_url TEXT,
media_type TEXT,
status TEXT,
start_date TEXT,
first_seen TEXT DEFAULT (datetime('now')),
last_seen TEXT DEFAULT (datetime('now')),
days_running INTEGER DEFAULT 0,
FOREIGN KEY (company_id) REFERENCES companies(id)
);
CREATE TABLE IF NOT EXISTS snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ad_id TEXT,
snapshot_date TEXT DEFAULT (datetime('now')),
status TEXT,
FOREIGN KEY (ad_id) REFERENCES ads(id)
);
`);
module.exports = db;
Step 3: Find Companies in the Ad Library
First, we need to search for a company and get their page ID:
Create spy.js:
require('dotenv').config();
const axios = require('axios');
const db = require('./db');
const API_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
async function searchCompany(query) {
console.log(`š Searching for "${query}"...`);
const { data } = await axios.get(`${API_BASE}/v1/scrape/facebook-ad-library/search-companies`, {
params: { query },
headers
});
const companies = data.data || [];
if (companies.length === 0) {
console.log('No companies found.');
return null;
}
console.log(`\nFound ${companies.length} companies:\n`);
companies.forEach((c, i) => {
console.log(` ${i + 1}. ${c.name || c.pageName} (${c.pageId || c.id})`);
if (c.categories) console.log(` Categories: ${c.categories.join(', ')}`);
});
return companies;
}
Step 4: Pull All Ads from a Company
This is the meat of it. We pull every ad, handle pagination, and store them:
async function getCompanyAds(companyName, pageId, options = {}) {
console.log(`\nš„ Fetching ads for ${companyName}...`);
// Save company to db
const insertCompany = db.prepare(
'INSERT OR IGNORE INTO companies (name, page_id) VALUES (?, ?)'
);
insertCompany.run(companyName, pageId);
const company = db.prepare('SELECT * FROM companies WHERE page_id = ?').get(pageId);
let allAds = [];
let cursor = null;
let page = 0;
while (true) {
page++;
console.log(` Page ${page}...`);
const params = {
pageId,
country: options.country || 'US',
status: options.status || 'active',
...(options.media_type && { media_type: options.media_type }),
...(cursor && { cursor })
};
const { data } = await axios.get(
`${API_BASE}/v1/scrape/facebook-ad-library/company-ads`,
{ params, headers }
);
const ads = data.data?.ads || data.data || [];
allAds = allAds.concat(ads);
// Check for pagination
cursor = data.data?.cursor || data.data?.paging?.next;
if (!cursor || ads.length === 0) break;
// Don't hammer the API
await new Promise(r => setTimeout(r, 1000));
}
console.log(` Found ${allAds.length} ads total`);
// Store ads
const insertAd = db.prepare(`
INSERT INTO ads (id, company_id, headline, body, link_url, media_type, status, start_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
last_seen = datetime('now'),
status = excluded.status,
days_running = CAST(
(julianday('now') - julianday(COALESCE(ads.start_date, ads.first_seen))) AS INTEGER
)
`);
const insertSnapshot = db.prepare(
'INSERT INTO snapshots (ad_id, status) VALUES (?, ?)'
);
const transaction = db.transaction((ads) => {
for (const ad of ads) {
const adId = ad.id || ad.adArchiveID;
insertAd.run(
adId,
company.id,
ad.headline || ad.title || '',
ad.body || ad.bodyText || '',
ad.linkUrl || ad.link || '',
ad.mediaType || detectMediaType(ad),
ad.isActive ? 'active' : 'inactive',
ad.startDate || ad.startedRunningOn || null
);
insertSnapshot.run(adId, ad.isActive ? 'active' : 'inactive');
}
});
transaction(allAds);
return allAds;
}
function detectMediaType(ad) {
if (ad.videos?.length > 0 || ad.video) return 'video';
if (ad.images?.length > 1 || ad.carousel) return 'carousel';
return 'image';
}
Step 5: Identify Long-Running Winners
This is the money feature. Ads that run the longest are the profitable ones:
function getLongRunningAds(pageId, minDays = 14) {
const company = db.prepare('SELECT * FROM companies WHERE page_id = ?').get(pageId);
if (!company) return [];
const ads = db.prepare(`
SELECT * FROM ads
WHERE company_id = ? AND days_running >= ?
ORDER BY days_running DESC
`).all(company.id, minDays);
return ads;
}
function printWinningAds(pageId) {
console.log('\nš WINNING ADS (Running 14+ Days)');
console.log('ā'.repeat(60));
const winners = getLongRunningAds(pageId, 14);
if (winners.length === 0) {
console.log('No long-running ads found yet. Run the spy again in a few days.');
return;
}
winners.forEach((ad, i) => {
console.log(`\n${i + 1}. [${ad.days_running} days] ${ad.media_type.toUpperCase()}`);
console.log(` Headline: ${ad.headline || '(none)'}`);
console.log(` Body: ${(ad.body || '').substring(0, 120)}...`);
if (ad.link_url) console.log(` URL: ${ad.link_url}`);
console.log(` Status: ${ad.status}`);
});
console.log(`\nTotal winners: ${winners.length}`);
}
Step 6: Analyze Ad Copy Patterns with AI
What are the winning ads actually saying? Let's find the patterns:
const OpenAI = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function analyzeAdPatterns(pageId) {
const winners = getLongRunningAds(pageId, 7);
if (winners.length < 3) {
console.log('Need at least 3 ads to find patterns.');
return;
}
console.log(`\nš§ Analyzing patterns across ${winners.length} ads...\n`);
const adData = winners.map(ad => ({
headline: ad.headline,
body: ad.body,
type: ad.media_type,
days: ad.days_running
}));
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Analyze these Facebook ads from the same company and find patterns.
These are their longest-running (most profitable) ads:
${JSON.stringify(adData, null, 2)}
Return JSON:
{
"hooks": ["list of hook formulas they use repeatedly"],
"ctas": ["list of call-to-action patterns"],
"copyFormulas": ["list of copy frameworks (PAS, AIDA, etc)"],
"emotionalTriggers": ["urgency, FOMO, social proof, etc"],
"mediaStrategy": "what types of media dominate their winners",
"angleBreakdown": {
"pain_point_ads": "percentage and description",
"benefit_ads": "percentage and description",
"social_proof_ads": "percentage and description"
},
"recommendations": ["3 specific ads you could create based on their patterns"]
}`
}],
response_format: { type: 'json_object' }
});
const analysis = JSON.parse(completion.choices[0].message.content);
console.log('š AD COPY ANALYSIS');
console.log('ā'.repeat(60));
console.log('\nš£ Top Hooks:');
analysis.hooks.forEach(h => console.log(` ⢠${h}`));
console.log('\nš¢ CTAs:');
analysis.ctas.forEach(c => console.log(` ⢠${c}`));
console.log('\nš Copy Formulas:');
analysis.copyFormulas.forEach(f => console.log(` ⢠${f}`));
console.log('\nš¤ Emotional Triggers:');
analysis.emotionalTriggers.forEach(t => console.log(` ⢠${t}`));
console.log(`\nšø Media Strategy: ${analysis.mediaStrategy}`);
console.log('\nš” Ads You Could Create:');
analysis.recommendations.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
return analysis;
}
Step 7: Get Deep Details on Any Ad
Want to look at a specific ad more closely? Get the full creative:
async function getAdDetails(adId) {
console.log(`\nš Fetching ad details...`);
const { data } = await axios.get(
`${API_BASE}/v1/scrape/facebook-ad-library/ad-details`,
{ params: { id: adId, get_transcript: true }, headers }
);
const ad = data.data;
console.log('\nš AD DETAILS');
console.log('ā'.repeat(50));
console.log(` ID: ${ad.id || adId}`);
console.log(` Page: ${ad.pageName || ad.page_name}`);
console.log(` Status: ${ad.isActive ? 'Active' : 'Inactive'}`);
console.log(` Started: ${ad.startDate || ad.startedRunningOn}`);
console.log(` Platforms: ${(ad.platforms || []).join(', ')}`);
if (ad.headline) console.log(`\n Headline: ${ad.headline}`);
if (ad.body) console.log(` Body: ${ad.body}`);
if (ad.linkUrl) console.log(` Link: ${ad.linkUrl}`);
if (ad.cta) console.log(` CTA: ${ad.cta}`);
if (ad.transcript) {
console.log(`\n š Video Transcript:`);
console.log(` ${ad.transcript.substring(0, 300)}...`);
}
return ad;
}
Step 8: Search Ads by Keyword
Don't know who your competitors are? Search the entire ad library:
async function searchAds(query, options = {}) {
console.log(`\nš Searching ads for "${query}"...`);
let allAds = [];
let cursor = null;
let pages = 0;
const maxPages = options.maxPages || 3;
while (pages < maxPages) {
pages++;
const params = {
query,
country: options.country || 'US',
status: options.status || 'active',
sort_by: options.sort_by || 'relevance',
...(options.media_type && { media_type: options.media_type }),
...(cursor && { cursor })
};
const { data } = await axios.get(
`${API_BASE}/v1/scrape/facebook-ad-library/search`,
{ params, headers }
);
const ads = data.data?.ads || data.data || [];
allAds = allAds.concat(ads);
cursor = data.data?.cursor || data.data?.paging?.next;
if (!cursor || ads.length === 0) break;
await new Promise(r => setTimeout(r, 1000));
}
console.log(`Found ${allAds.length} ads\n`);
// Group by company
const byCompany = {};
allAds.forEach(ad => {
const name = ad.pageName || ad.page_name || 'Unknown';
if (!byCompany[name]) byCompany[name] = [];
byCompany[name].push(ad);
});
console.log('š Ads by Company:');
Object.entries(byCompany)
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 10)
.forEach(([name, ads]) => {
console.log(` ${name}: ${ads.length} ads`);
});
return { ads: allAds, byCompany };
}
Step 9: New Ad Alert System
Set this up as a cron job and get notified when competitors launch new ads:
async function checkForNewAds(pageId) {
const company = db.prepare('SELECT * FROM companies WHERE page_id = ?').get(pageId);
if (!company) return [];
const existingIds = new Set(
db.prepare('SELECT id FROM ads WHERE company_id = ?')
.all(company.id)
.map(a => a.id)
);
// Fetch current ads
const currentAds = await getCompanyAds(company.name, pageId);
// Find new ones
const newAds = currentAds.filter(ad => {
const adId = ad.id || ad.adArchiveID;
return !existingIds.has(adId);
});
if (newAds.length > 0) {
console.log(`\nšØ ${newAds.length} NEW ADS DETECTED!`);
console.log('ā'.repeat(50));
newAds.forEach((ad, i) => {
console.log(`\n${i + 1}. NEW: ${ad.headline || ad.title || '(no headline)'}`);
console.log(` ${(ad.body || ad.bodyText || '').substring(0, 100)}`);
console.log(` Type: ${ad.mediaType || detectMediaType(ad)}`);
});
} else {
console.log('No new ads since last check.');
}
return newAds;
}
Step 10: Tie It All Together
async function main() {
const command = process.argv[2];
const target = process.argv[3];
switch (command) {
case 'search':
await searchCompany(target);
break;
case 'spy':
// spy on a company by name or page ID
const companies = await searchCompany(target);
if (companies && companies.length > 0) {
const company = companies[0];
const id = company.pageId || company.id;
await getCompanyAds(company.name || company.pageName, id);
printWinningAds(id);
await analyzeAdPatterns(id);
}
break;
case 'search-ads':
await searchAds(target);
break;
case 'winners':
printWinningAds(target);
break;
case 'check':
await checkForNewAds(target);
break;
case 'detail':
await getAdDetails(target);
break;
default:
console.log('Usage:');
console.log(' node spy.js search "Nike" - Find a company');
console.log(' node spy.js spy "Nike" - Full spy analysis');
console.log(' node spy.js search-ads "protein" - Search ads by keyword');
console.log(' node spy.js winners <pageId> - Show winning ads');
console.log(' node spy.js check <pageId> - Check for new ads');
console.log(' node spy.js detail <adId> - Get ad details');
}
}
main().catch(console.error);
Running It
# Find a competitor
node spy.js search "Gymshark"
# Full spy session
node spy.js spy "Gymshark"
# Search entire ad library for your niche
node spy.js search-ads "meal prep delivery"
# Check for new ads (run daily via cron)
node spy.js check 123456789
Sample Output
š Searching for "Gymshark"...
Found 3 companies:
1. Gymshark (1234567890)
Categories: Clothing Brand, Sports & Recreation
š„ Fetching ads for Gymshark...
Page 1...
Page 2...
Page 3...
Found 147 ads total
š WINNING ADS (Running 14+ Days)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
1. [87 days] VIDEO
Headline: This is the workout split that changed everything
Body: 3 days. Full body. No excuses. Our athletes swear by this...
Status: active
2. [54 days] CAROUSEL
Headline: The only leggings that passed the squat test
Body: See-through leggings? Never again. We tested 247 fabrics...
Status: active
3. [41 days] IMAGE
Headline: Free shipping on orders over $50
Body: New arrivals just dropped. 200+ new styles...
Status: active
š AD COPY ANALYSIS
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š£ Top Hooks:
⢠Transformation/before-after framing
⢠Problem-solution with specific numbers
⢠Social proof from athletes
š¢ CTAs:
⢠"Shop Now" (72% of ads)
⢠"See Collection" for new drops
⢠Free shipping threshold urgency
š” Ads You Could Create:
1. "I tested 50 gym shorts in 30 days. Here's the one I kept."
2. Video showing fabric stretch test with competitors
3. Carousel of "What I ordered vs what I got" positive reveals
What This Replaces
Manual competitor ad research tools charge a lot:
| Tool | Price | What You Get |
|---|---|---|
| AdSpy | $149/mo | Facebook + IG ads |
| BigSpy | $99/mo | Multi-platform |
| PowerAdSpy | $49/mo | Limited searches |
| This tool | ~$0.02/spy session | Everything above + AI analysis |
The difference: you own the data and can customize the analysis.
Get Your API Key
The SociaVault Facebook Ad Library endpoints make this embarrassingly simple. No scraping headless browsers. No proxy rotations. Just clean API calls.
- Get your key at sociavault.com
- Each ad library request costs 1 credit
- Start spying on your competitors today
Your competitors' winning ads are public. Not looking at them is leaving money on the table.
Top comments (0)