DEV Community

Cover image for Build a Google Ads Competitor Intelligence Tool with Node.js
Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Google Ads Competitor Intelligence Tool with Node.js

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:

  1. Pulls every ad a competitor is running on Google
  2. Tracks how long they've been running each creative
  3. Analyzes their messaging strategy
  4. 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
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_key_here
OPENAI_API_KEY=your_openai_key
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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}`);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Competitive Edge

Most companies check competitor ads once a quarter. With this tool, you can:

  1. Daily monitoring: See new ads within hours of launch
  2. Pattern detection: Know exactly what copy formulas work for them
  3. Counter-positioning: Build ads that directly counter their messaging
  4. 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

  1. Get your API key at sociavault.com
  2. Search for your biggest competitor
  3. 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.

javascript #googleads #marketing #webdev

Top comments (0)