Skip to main content

Ad Verification & Compliance

Verify that ads are displaying correctly across different regions, detect fraud, and ensure brand safety using geo-distributed stealth browsers.

The Challenge

Ad verification requires:
  • Seeing pages as real users in specific locations see them
  • Detecting malicious ads, redirects, and cloaking
  • Screenshotting actual ad placements for proof
  • Checking across dozens of geos simultaneously

Multi-Geo Ad Verification

import { Meshbrow } from '@meshbrow/sdk';
import { chromium, Page } from 'playwright';
import * as fs from 'fs';

const client = new Meshbrow({ apiKey: process.env.MESHBROW_API_KEY! });

interface AdVerification {
  url: string;
  country: string;
  screenshot: string; // file path
  ads: AdPlacement[];
  redirects: string[];
  loadTime: number;
  brandSafe: boolean;
}

interface AdPlacement {
  selector: string;
  dimensions: { width: number; height: number };
  visible: boolean;
  src?: string;
  advertiser?: string;
}

async function verifyAds(
  urls: string[],
  countries: string[]
): Promise<AdVerification[]> {
  const results: AdVerification[] = [];

  // Launch geo-distributed fleet
  const fleet = await client.fleet.create({
    name: `ad-verify-${Date.now()}`,
    count: countries.length,
    config: {
      stealth: 'max',
      timeout: 300,
      recording: { screenshots: true, har: true },
    },
    distribution: {
      countries,
      proxyTypes: ['residential'],
    },
  });

  const ready = await client.fleet.waitReady(fleet.id);

  const tasks = ready.sessions.map(async (session, i) => {
    const country = countries[i];
    const browser = await chromium.connectOverCDP(session.cdpUrl);
    const page = browser.contexts()[0].pages()[0];

    try {
      for (const url of urls) {
        const result = await verifyPageAds(page, url, country);
        results.push(result);
        await page.waitForTimeout(2000 + Math.random() * 3000);
      }
    } finally {
      await browser.close();
    }
  });

  await Promise.all(tasks);
  await client.fleet.destroy(fleet.id);

  return results;
}

async function verifyPageAds(
  page: Page,
  url: string,
  country: string
): Promise<AdVerification> {
  const redirects: string[] = [];
  const startTime = Date.now();

  // Track redirects
  page.on('response', (response) => {
    if (response.status() >= 300 && response.status() < 400) {
      redirects.push(response.headers()['location'] || response.url());
    }
  });

  await page.goto(url, { waitUntil: 'networkidle' });
  const loadTime = Date.now() - startTime;

  // Wait for ads to render
  await page.waitForTimeout(3000);

  // Take full-page screenshot
  const screenshotPath = `screenshots/ad-verify-${country}-${Date.now()}.png`;
  await page.screenshot({ path: screenshotPath, fullPage: true });

  // Find all ad elements
  const ads = await page.evaluate(() => {
    const adSelectors = [
      'iframe[id*="google_ads"]',
      'iframe[src*="doubleclick"]',
      'div[id*="ad-"]',
      'div[class*="ad-slot"]',
      'div[data-ad]',
      'ins.adsbygoogle',
    ];

    const results: Array<{
      selector: string;
      dimensions: { width: number; height: number };
      visible: boolean;
      src?: string;
    }> = [];

    for (const selector of adSelectors) {
      const elements = document.querySelectorAll(selector);
      elements.forEach((el) => {
        const rect = el.getBoundingClientRect();
        results.push({
          selector,
          dimensions: { width: rect.width, height: rect.height },
          visible: rect.width > 0 && rect.height > 0,
          src: (el as HTMLIFrameElement).src || undefined,
        });
      });
    }

    return results;
  });

  // Brand safety check - look for inappropriate content
  const brandSafe = await page.evaluate(() => {
    const text = document.body.innerText.toLowerCase();
    const unsafeKeywords = ['gambling', 'adult', 'weapons', 'drugs'];
    return !unsafeKeywords.some((kw) => text.includes(kw));
  });

  return {
    url,
    country,
    screenshot: screenshotPath,
    ads,
    redirects,
    loadTime,
    brandSafe,
  };
}

Ad Fraud Detection

async function detectAdFraud(page: Page): Promise<{
  cloaking: boolean;
  hiddenAds: number;
  suspiciousRedirects: string[];
  maliciousScripts: string[];
}> {
  // Detect cloaking (different content for bots vs humans)
  const cloaking = await page.evaluate(() => {
    // Check if content changes based on visibility
    const hiddenContent = document.querySelectorAll('[style*="display:none"]');
    const visibleText = document.body.innerText.length;
    return hiddenContent.length > 20 || visibleText < 100;
  });

  // Detect hidden/invisible ads (ad fraud indicator)
  const hiddenAds = await page.evaluate(() => {
    const adFrames = document.querySelectorAll('iframe[src*="ad"]');
    let hidden = 0;
    adFrames.forEach((frame) => {
      const rect = frame.getBoundingClientRect();
      const style = window.getComputedStyle(frame);
      if (
        rect.width < 2 || rect.height < 2 ||
        style.opacity === '0' ||
        style.visibility === 'hidden'
      ) {
        hidden++;
      }
    });
    return hidden;
  });

  // Check for suspicious redirect chains
  const suspiciousRedirects: string[] = [];
  page.on('response', (response) => {
    const url = response.url();
    if (
      response.status() >= 300 &&
      response.status() < 400 &&
      (url.includes('click') || url.includes('redirect') || url.includes('track'))
    ) {
      suspiciousRedirects.push(url);
    }
  });

  // Detect malicious scripts
  const maliciousScripts = await page.evaluate(() => {
    const scripts = Array.from(document.querySelectorAll('script[src]'));
    const suspicious = scripts
      .map((s) => (s as HTMLScriptElement).src)
      .filter((src) =>
        src.includes('crypto') ||
        src.includes('miner') ||
        src.includes('inject')
      );
    return suspicious;
  });

  return { cloaking, hiddenAds, suspiciousRedirects, maliciousScripts };
}

Scheduled Verification Report

async function generateVerificationReport(
  publisherUrls: string[],
  geos: string[]
) {
  const verifications = await verifyAds(publisherUrls, geos);

  const report = {
    timestamp: new Date().toISOString(),
    totalChecks: verifications.length,
    summary: {
      adsFound: verifications.reduce((acc, v) => acc + v.ads.length, 0),
      hiddenAds: verifications.reduce(
        (acc, v) => acc + v.ads.filter((a) => !a.visible).length, 0
      ),
      avgLoadTime: verifications.reduce((acc, v) => acc + v.loadTime, 0) / verifications.length,
      brandUnsafe: verifications.filter((v) => !v.brandSafe).length,
      withRedirects: verifications.filter((v) => v.redirects.length > 0).length,
    },
    details: verifications,
  };

  fs.writeFileSync(
    `reports/ad-verification-${Date.now()}.json`,
    JSON.stringify(report, null, 2)
  );

  return report;
}

Key Takeaways

  • Residential proxies only — Ad networks block datacenter IPs
  • Multi-geo fleet — Verify ads appear correctly in all target markets
  • Wait for ad rendering — Ads load asynchronously, wait 3-5s after page load
  • Screenshot everything — Visual proof for compliance reporting
  • Detect fraud patterns — Hidden iframes, cloaking, suspicious redirects
  • HAR recording — Capture full network traffic for audit trails