You've seen your competitor everywhere. Google Search. YouTube pre-rolls. Display ads on every site you visit. They're spending a fortune ā and you want to know what's working.
Good news: Google's Ad Transparency Center makes every ad public. Bad news: manually searching through it is like drinking from a firehose.
Let's build a Google Ads Competitor Intelligence Tool that:
- Pulls every ad a competitor is running on Google
- Tracks how long they've been running each creative
- Analyzes their messaging strategy
- Monitors for new campaign launches
If they found a winning ad formula, we want to know about it.
Why Google Ads Transparency Is a Goldmine
Since 2023, Google requires all advertisers to verify their identity and makes their ads publicly searchable. This means:
- You can see every single ad any company runs
- You can filter by region, format, and date range
- You can track which ads they keep running (survival = profitable)
Most people don't know this exists. The few who do check it manually once a quarter. We're going to automate it.
The Stack
- Node.js: Runtime
- SociaVault API: Google Ad Library endpoints
- better-sqlite3: Local tracking database
- OpenAI: Ad copy analysis
Step 1: Setup
mkdir google-ad-spy
cd google-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: Set Up Tracking Database
Create db.js:
const Database = require('better-sqlite3');
const db = new Database('google-ads.db');
db.exec(`
CREATE TABLE IF NOT EXISTS advertisers (
id TEXT PRIMARY KEY,
name TEXT,
domain TEXT,
added_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS ads (
id TEXT PRIMARY KEY,
advertiser_id TEXT,
format TEXT,
content 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 (advertiser_id) REFERENCES advertisers(id)
);
`);
module.exports = db;
Step 3: Find Advertisers
First, search for a company to get their advertiser ID:
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 searchAdvertisers(query) {
console.log(`š Searching Google Ad Library for "${query}"...\n`);
const { data } = await axios.get(
`${API_BASE}/v1/scrape/google-ad-library/search-advertisers`,
{ params: { query }, headers }
);
const advertisers = data.data || [];
if (advertisers.length === 0) {
console.log('No advertisers found.');
return [];
}
advertisers.forEach((adv, i) => {
console.log(` ${i + 1}. ${adv.name || adv.advertiserName}`);
console.log(` ID: ${adv.id || adv.advertiserId}`);
if (adv.domain) console.log(` Domain: ${adv.domain}`);
if (adv.verifiedStatus) console.log(` Verified: ${adv.verifiedStatus}`);
console.log();
});
return advertisers;
}
Step 4: Pull All Ads from a Competitor
Fetch every ad they're running with pagination:
async function getCompanyAds(identifier, options = {}) {
const isDomain = identifier.includes('.');
console.log(`š„ Fetching ads for ${identifier}...`);
let allAds = [];
let cursor = null;
let page = 0;
while (page < (options.maxPages || 10)) {
page++;
const params = {
...(isDomain ? { domain: identifier } : { advertiser_id: identifier }),
region: options.region || 'US',
topic: options.topic || 'all',
...(cursor && { cursor })
};
const { data } = await axios.get(
`${API_BASE}/v1/scrape/google-ad-library/company-ads`,
{ params, headers }
);
const ads = data.data?.ads || data.data || [];
allAds = allAds.concat(ads);
console.log(` Page ${page}: ${ads.length} ads`);
cursor = data.data?.cursor || data.data?.nextPageToken;
if (!cursor || ads.length === 0) break;
await new Promise(r => setTimeout(r, 1000));
}
console.log(` Total: ${allAds.length} ads\n`);
// Store in database
const advertiserId = isDomain ? identifier : identifier;
db.prepare('INSERT OR IGNORE INTO advertisers (id, name, domain) VALUES (?, ?, ?)')
.run(advertiserId, options.name || identifier, isDomain ? identifier : '');
const upsertAd = db.prepare(`
INSERT INTO ads (id, advertiser_id, format, content)
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) {
upsertAd.run(
ad.id || ad.creativeId || `gad_${Math.random().toString(36).slice(2)}`,
advertiserId,
ad.format || ad.adType || detectFormat(ad),
JSON.stringify({
headline: ad.headline || ad.title,
description: ad.description || ad.bodyText,
displayUrl: ad.displayUrl,
finalUrl: ad.finalUrl || ad.landingPage,
imageUrl: ad.imageUrl || ad.creative?.imageUrl,
})
);
}
});
tx();
return allAds;
}
function detectFormat(ad) {
if (ad.videoUrl || ad.video) return 'video';
if (ad.imageUrl && ad.headline) return 'display';
if (ad.headline && !ad.imageUrl) return 'search';
return 'unknown';
}
Step 5: Analyze Long-Running Winners
Same principle as Facebook ads ā longevity = profitability:
function getWinningAds(advertiserId, minDays = 14) {
return db.prepare(`
SELECT *, json_extract(content, '$.headline') as headline,
json_extract(content, '$.description') as description,
json_extract(content, '$.finalUrl') as url
FROM ads
WHERE advertiser_id = ? AND days_running >= ?
ORDER BY days_running DESC
`).all(advertiserId, minDays);
}
function printAdReport(advertiserId) {
const allAds = db.prepare(
'SELECT * FROM ads WHERE advertiser_id = ?'
).all(advertiserId);
const winners = getWinningAds(advertiserId, 14);
// Format breakdown
const formats = {};
allAds.forEach(ad => {
formats[ad.format] = (formats[ad.format] || 0) + 1;
});
console.log('š GOOGLE ADS INTELLIGENCE REPORT');
console.log('ā'.repeat(55));
console.log(`\n Total ads tracked: ${allAds.length}`);
console.log(` Long-running (14+ days): ${winners.length}`);
console.log(` Active: ${allAds.filter(a => a.is_active).length}`);
console.log('\n š Format Breakdown:');
Object.entries(formats)
.sort((a, b) => b[1] - a[1])
.forEach(([format, count]) => {
const pct = ((count / allAds.length) * 100).toFixed(0);
console.log(` ${format}: ${count} (${pct}%)`);
});
if (winners.length > 0) {
console.log('\nš WINNING ADS (14+ Days Running):');
console.log('ā'.repeat(55));
winners.slice(0, 10).forEach((ad, i) => {
console.log(`\n ${i + 1}. [${ad.days_running} days] ${ad.format.toUpperCase()}`);
if (ad.headline) console.log(` Headline: ${ad.headline}`);
if (ad.description) console.log(` Desc: ${ad.description?.substring(0, 100)}`);
if (ad.url) console.log(` URL: ${ad.url}`);
});
}
}
Step 6: AI-Powered Ad Copy Analysis
async function analyzeAdStrategy(advertiserId) {
const winners = getWinningAds(advertiserId, 7);
if (winners.length < 3) {
console.log('Need more data. Run spy first.');
return;
}
const adSample = winners.slice(0, 20).map(ad => ({
format: ad.format,
headline: ad.headline,
description: ad.description,
days: ad.days_running,
}));
console.log(`\nš§ Analyzing ${adSample.length} ads with AI...\n`);
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Analyze this company's Google Ads strategy based on their longest-running ads.
Ads data:
${JSON.stringify(adSample, null, 2)}
Return JSON:
{
"overall_strategy": "2-3 sentence summary of their approach",
"target_audience": "who they're targeting based on ad copy",
"key_value_props": ["list of value propositions they emphasize"],
"headline_patterns": ["common headline formulas"],
"cta_patterns": ["call to action styles"],
"emotional_appeals": ["what emotions they target"],
"format_strategy": "how they use different ad formats",
"landing_page_strategy": "what their URLs reveal about funnel structure",
"weaknesses": ["potential gaps or weaknesses in their strategy"],
"counter_strategies": ["3 ways to compete against these ads"]
}`
}],
response_format: { type: 'json_object' }
});
const analysis = JSON.parse(completion.choices[0].message.content);
console.log('šÆ COMPETITIVE AD ANALYSIS');
console.log('ā'.repeat(55));
console.log(`\nš Strategy: ${analysis.overall_strategy}`);
console.log(`\nšÆ Target Audience: ${analysis.target_audience}`);
console.log('\nš Value Props:');
analysis.key_value_props.forEach(v => console.log(` ⢠${v}`));
console.log('\nš° Headline Patterns:');
analysis.headline_patterns.forEach(h => console.log(` ⢠${h}`));
console.log('\nā” Counter-Strategies:');
analysis.counter_strategies.forEach((s, i) => console.log(` ${i+1}. ${s}`));
return analysis;
}
Step 7: Deep Dive on Individual Ads
async function inspectAd(adUrl) {
console.log(`š Inspecting ad...`);
const { data } = await axios.get(
`${API_BASE}/v1/scrape/google-ad-library/ad-details`,
{ params: { url: adUrl }, headers }
);
const ad = data.data;
console.log('\nš AD DETAILS');
console.log('ā'.repeat(50));
console.log(` Advertiser: ${ad.advertiserName || ad.advertiser}`);
console.log(` Format: ${ad.format || ad.adType}`);
console.log(` Region: ${(ad.regions || ad.targetRegions || []).join(', ')}`);
console.log(` Last shown: ${ad.lastShown || ad.lastSeen}`);
if (ad.headline) console.log(`\n Headline: ${ad.headline}`);
if (ad.description) console.log(` Description: ${ad.description}`);
if (ad.displayUrl) console.log(` Display URL: ${ad.displayUrl}`);
return ad;
}
Step 8: CLI Interface
async function main() {
const command = process.argv[2];
const target = process.argv[3];
switch (command) {
case 'search':
await searchAdvertisers(target);
break;
case 'spy':
const advertisers = await searchAdvertisers(target);
if (advertisers.length > 0) {
const adv = advertisers[0];
const id = adv.id || adv.advertiserId;
await getCompanyAds(id, { name: adv.name });
printAdReport(id);
await analyzeAdStrategy(id);
}
break;
case 'domain':
await getCompanyAds(target);
printAdReport(target);
break;
case 'report':
printAdReport(target);
break;
case 'analyze':
await analyzeAdStrategy(target);
break;
case 'ad':
await inspectAd(target);
break;
default:
console.log('Google Ads Spy Tool\n');
console.log('Commands:');
console.log(' node spy.js search "Shopify" - Find an advertiser');
console.log(' node spy.js spy "Shopify" - Full intelligence report');
console.log(' node spy.js domain shopify.com - Spy by domain');
console.log(' node spy.js report <advertiserId> - Print stored report');
console.log(' node spy.js analyze <advertiserId> - AI strategy analysis');
console.log(' node spy.js ad <adUrl> - Inspect specific ad');
}
}
main().catch(console.error);
Running It
# Find a competitor
node spy.js search "Monday.com"
# Full spy analysis
node spy.js spy "Monday.com"
# Spy by domain directly
node spy.js domain monday.com
The Competitive Edge
Most companies check competitor ads once a quarter. With this tool, you can:
- Daily monitoring: See new ads within hours of launch
- Pattern detection: Know exactly what copy formulas work for them
- Counter-positioning: Build ads that directly counter their messaging
- Budget estimation: More ad variations = larger budget
Everyone's landing pages are different, but their ad copy tells you exactly what converts.
Cost Comparison
| Tool | Price | Scope |
|---|---|---|
| Semrush Ad Research | $129/mo | Google ads + estimates |
| SpyFu | $39/mo | Historical data, limited current |
| iSpionage | $59/mo | Google + Bing |
| This tool | ~$0.05/spy session | Full data + AI analysis |
Get Started
- Get your API key at sociavault.com
- Search for your biggest competitor
- See exactly what they're spending money on
Their ad playbook isn't a secret. It's sitting in the Google Ad Library. We're just reading it faster.
The best ad strategy starts with knowing what already works. Your competitors already ran the experiments for you.
Top comments (0)