Skip to main content
Free Resource

The Definitive GDN Exclusion List (2026)

228+ bad placements, two auto-exclusion scripts, and a free download. Stop click fraud from draining your Google Display Network budget.

By PPC strategistsUpdated

The Bottom Line

21% of programmatic ad impressions land on Made For Advertising sites. The average Google Ads account loses ~15% of Display clicks to fraud. This page gives you everything you need to fix that: a 228+ placement exclusion list, two Google Ads Scripts for automated protection, and the full breakdown of what to block and why.

How Big Is the GDN Click Fraud Problem?

21%
of programmatic impressions land on MFA sites
Source: ANA
$13B
generated by MFA sites globally per year
Source: ANA/Jounce Media
~15%
of GDN clicks are fraudulent on average
Source: CHEQ
38%
YoY rise in active MFA domains
Source: Jounce Media

If you run Google Display Network campaigns without a placement exclusion list, a significant chunk of your budget is going to sites that exist purely to extract ad revenue. These are not edge cases. The Association of National Advertisers found that one in five programmatic impressions lands on a Made For Advertising site. Seer Interactive found that up to 30% of Search Partner spend was fraudulent traffic. Adalytics discovered over 80,000 sites carrying Google Search Partner embed code, many on piracy, adult, and sanctioned-country domains.

The problem is structural. Google's Display Network includes millions of websites and apps, and the default targeting is "optimise for conversions" across all of them. Without exclusions, your ads will inevitably appear on low-quality inventory where clicks are accidental, automated, or from users with zero buying intent.

What Is in Our Exclusion List

Our list contains 228+ placements organised into 19 categories. Every placement is individually sourced from fraud detection reports, agency placement audits, and industry research. Here is what it covers:

  • Mobile app nuclear exclusion (mobileappcategory::69500) that blocks all app inventory in one line
  • 30+ granular app category exclusions for games, kids, comics, entertainment, personalization, and utilities
  • Known fraudulent apps from Cheetah Mobile (click injection), DO Global (ad fraud), and Kika Tech
  • 48 confirmed MFA domains sourced from Pixalate, Jounce Media, and FraudBlocker
  • 15 parked/spam domains including meta-search engines and AdSense arbitrage sites
  • 25 piracy and torrent sites (1337x, Pirate Bay, FMovies, and streaming clones)
  • 20 dating and gambling sites (irrelevant for most B2B/B2C advertisers)
  • 12 quiz and clickbait platforms (BuzzFeed, Playbuzz, personality test sites)
  • 14 file converter and proxy spam sites
  • 9 lyrics, wallpaper, and ringtone sites (ad-dense, zero purchase intent)
  • 17 children's gaming sites (accidental clicks from toddlers)
  • 14 incentivised traffic/reward sites (Swagbucks, InboxDollars: users click for points, not interest)
  • 7 fake news and misinformation domains
  • 10 high-bounce aggregators (MSN, AOL, tabloids)

Download the Full Exclusion List

Google Sheet with 228+ placements, ready to upload. Click "Make a copy" to add it to your own Google Drive, then paste into Google Ads exclusion lists.

Copy to Your Google Drive

What Are Made For Advertising (MFA) Sites?

MFA sites are the biggest source of wasted Display spend. They are websites built purely for ad arbitrage: they buy cheap traffic from content recommendation widgets (Taboola, Outbrain, ZergNet, Revcontent) and monetise visitors with extremely dense ad placements.

You can identify MFA sites by their characteristics:

  • 10+ ad units visible on a single page
  • Slideshow/gallery format that forces multiple page loads per article
  • Generic, aggregated, or AI-generated content with no original reporting
  • Domains with words like "buzz," "viral," "top," "best," "facts"
  • Traffic primarily from content recommendation widgets rather than search or direct
  • Cheap TLDs: .xyz, .buzz, .one, .cloud, .info

Jounce Media, which reassesses every RTB-traded website daily, tracks over 1,000 active MFA domains and reported a 38% year-over-year rise in active MFA sites. Pixalate flagged msn.com as the top MFA domain by programmatic ad revenue.

Why You Should Exclude All Mobile Apps

Mobile apps are the single largest source of invalid clicks on the Google Display Network. The problem is structural:

  • Children's games generate enormous click volumes because toddlers tap ads accidentally
  • Utility apps (flashlights, battery savers, phone cleaners) were the category most frequently banned from the Play Store for ad fraud. Cheetah Mobile's entire portfolio was removed for click injection, and DO Global had 46 apps banned for fraudulent clicking.
  • Free apps with ads have financial incentives to generate clicks regardless of quality
  • In-app ad placements are often positioned to trigger accidental taps during normal app usage

The fix is simple. Add mobileappcategory::69500 as a placement exclusion. This is the parent category that covers all mobile apps across Google Play and the App Store. Unless you are specifically running app install campaigns, there is no reason for your Display ads to appear inside mobile apps.

TLDs with the Highest Fraud Rates

Certain top-level domains have disproportionately high rates of MFA sites, parked domains, and spam. Free TLDs are especially problematic because they attract bulk domain registration for ad fraud schemes.

Highest-Risk TLDs (45+)

.xyz.buzz.one.cloud.top.click.link.gdn.bid.win.download.stream.racing.date.faith.review.science.party.trade.accountant.cricket.loan.men.work.icu.fun.site.online.space.website.host.press.pw.tk.ml.ga.cf.gq.cam.rest.fit.kim.guru.ninja.rocks.live.world.today.zone

The .gdn TLD is literally named after Google Display Network and is almost exclusively used for ad fraud. Free TLDs (.tk, .ml, .ga, .cf, .gq) are provided by Freenom and attract massive volumes of parked and spam domains.

Our TLD Auto-Exclusion Script (below) automatically scans your placement reports and excludes any placement matching these TLDs, so you are protected as new fraudulent domains appear.

Two Free Google Ads Scripts for Automated Protection

Static exclusion lists are a strong foundation, but fraudulent placements change constantly. These two Google Ads Scripts provide automated, ongoing protection at scale.

Script 1: MCC Placement Excluder

Reads your Google Sheet of bad placements and applies them as exclusions across every child account in your MCC. Uses executeInParallel for speed. Deduplicates against existing exclusions. Sends email alerts with per-account breakdown.

MCC-levelDisplay and Video campaign exclusionsSchedule daily
View full script code
/**
 * MCC-Level GDN Placement Exclusion Script
 * PPC Chief | ppcchief.com/blog/gdn-exclusion-list
 *
 * Reads bad placements from a Google Sheet and applies them as
 * exclusions across all child accounts' Display and Video campaigns.
 * Performance Max campaigns require account-level exclusions and are not
 * handled by campaign-level placement exclusions in this script.
 *
 * SETUP:
 * 1. Create a Google Sheet with placements in column A (one per row, no header)
 * 2. Set SPREADSHEET_URL below to your sheet URL
 * 3. Set SHEET_NAME to the tab name containing placements
 * 4. Deploy this script at the MCC level in Google Ads Scripts
 * 5. Schedule to run daily
 */

var CONFIG = {
  SPREADSHEET_URL: 'YOUR_SHEET_URL_HERE',
  SHEET_NAME: 'Exclusions',
  // Campaign-level placement exclusions only support Display and Video.
  // Performance Max exclusions must be applied at the account level.
  CAMPAIGN_TYPES: ['DISPLAY', 'VIDEO'],
  ACCOUNT_LABEL: '',
  MAX_ACCOUNTS_PER_RUN: 50,
  LOG_LEVEL: 'SUMMARY',
  ALERT_EMAIL: '',
  DRY_RUN: false
};

function main() {
  var badPlacements = loadPlacementsFromSheet();
  if (badPlacements.length === 0) {
    Logger.log('ERROR: No placements found in spreadsheet.');
    return;
  }
  Logger.log('Loaded ' + badPlacements.length + ' bad placements from spreadsheet.');

  var accountIterator = getAccountIterator();
  var accounts = [];
  var accountCount = 0;

  while (accountIterator.hasNext() && accountCount < CONFIG.MAX_ACCOUNTS_PER_RUN) {
    accounts.push(accountIterator.next());
    accountCount++;
  }

  Logger.log('Processing ' + accounts.length + ' accounts...');

  AdsManagerApp.accounts()
    .withIds(accounts.map(function(a) { return a.getCustomerId(); }))
    .executeInParallel('processAccount', 'afterAllAccounts',
      JSON.stringify({ placements: badPlacements, dryRun: CONFIG.DRY_RUN }));
}

function processAccount(serializedConfig) {
  var config = JSON.parse(serializedConfig);
  var placements = config.placements;
  var dryRun = config.dryRun;
  var account = AdsApp.currentAccount();
  var accountName = account.getName();
  var accountId = account.getCustomerId();

  var result = {
    accountName: accountName,
    accountId: accountId,
    added: 0,
    alreadyExcluded: 0,
    campaignsProcessed: 0,
    unsupportedSkipped: 0,
    errors: []
  };

  try {
    var existingExclusions = getExistingExclusions();
    var newPlacements = [];
    for (var i = 0; i < placements.length; i++) {
      if (existingExclusions.indexOf(placements[i].toLowerCase()) === -1) {
        newPlacements.push(placements[i]);
      } else {
        result.alreadyExcluded++;
      }
    }

    if (newPlacements.length === 0) {
      Logger.log('[' + accountId + '] ' + accountName + ': All placements already excluded.');
      return JSON.stringify(result);
    }

    // Collect eligible campaigns using explicit Display/Video selectors
    var targetCampaigns = [];

    // Fetch Display campaigns
    var displayCampaigns = AdsApp.campaigns()
      .withCondition("campaign.advertising_channel_type = 'DISPLAY'")
      .withCondition("campaign.status != 'REMOVED'")
      .get();
    while (displayCampaigns.hasNext()) {
      targetCampaigns.push({
        campaign: displayCampaigns.next(),
        type: "DISPLAY"
      });
    }

    // Fetch Video campaigns
    var videoCampaigns = AdsApp.videoCampaigns()
      .withCondition("campaign.status != 'REMOVED'")
      .get();
    while (videoCampaigns.hasNext()) {
      targetCampaigns.push({
        campaign: videoCampaigns.next(),
        type: "VIDEO"
      });
    }

    // Process exclusions on targeted campaigns
    for (var c = 0; c < targetCampaigns.length; c++) {
      var item = targetCampaigns[c];
      var campaign = item.campaign;
      result.campaignsProcessed++;

      for (var j = 0; j < newPlacements.length; j++) {
        var placement = newPlacements[j];
        if (!isSupportedCampaignPlacement(item.type, placement)) {
          result.unsupportedSkipped++;
          continue;
        }
        try {
          if (!dryRun) {
            applyPlacementExclusionSafe(campaign, placement);
          }
          result.added++;
        } catch (e) {
          if (e.message && e.message.indexOf('already exists') !== -1) {
            result.alreadyExcluded++;
          } else {
            result.errors.push(campaign.getName() + ' (' + item.type + ') -> ' + placement + ': ' + e.message);
          }
        }
      }
    }

    Logger.log('[' + accountId + '] ' + accountName +
      ': +' + result.added + ' exclusions applied, ' +
      result.alreadyExcluded + ' existing skipped, across ' +
      result.campaignsProcessed + ' campaigns, ' +
      result.unsupportedSkipped + ' unsupported skipped.');

  } catch (e) {
    result.errors.push('Account runtime error: ' + e.message);
  }

  return JSON.stringify(result);
}

function afterAllAccounts(results) {
  var totalAdded = 0;
  var totalAlreadyExcluded = 0;
  var totalUnsupportedSkipped = 0;
  var allErrors = [];
  var accountSummaries = [];

  for (var i = 0; i < results.length; i++) {
    var result = JSON.parse(results[i].getReturnValue());
    totalAdded += result.added;
    totalAlreadyExcluded += result.alreadyExcluded;
    totalUnsupportedSkipped += result.unsupportedSkipped || 0;
    allErrors = allErrors.concat(result.errors);
    accountSummaries.push(result.accountName + ' (' + result.accountId + '): +' +
      result.added + ' new, ' + result.alreadyExcluded + ' existing');
  }

  Logger.log('');
  Logger.log('=== EXCLUSION RUN COMPLETE ===');
  Logger.log('Accounts processed: ' + results.length);
  Logger.log('New exclusions added: ' + totalAdded);
  Logger.log('Already excluded: ' + totalAlreadyExcluded);
  Logger.log('Unsupported skipped: ' + totalUnsupportedSkipped);
  Logger.log('Errors: ' + allErrors.length);

  if (CONFIG.ALERT_EMAIL) {
    var subject = 'GDN Exclusion Script: +' + totalAdded +
      ' exclusions across ' + results.length + ' accounts';
    var body = 'Run completed at ' + new Date().toISOString() + '\n\n' +
      'New exclusions: ' + totalAdded + '\n' +
      'Already excluded: ' + totalAlreadyExcluded + '\n' +
      'Unsupported skipped: ' + totalUnsupportedSkipped + '\n' +
      'Errors: ' + allErrors.length + '\n\n' +
      'Account breakdown:\n' + accountSummaries.join('\n');
    MailApp.sendEmail(CONFIG.ALERT_EMAIL, subject, body);
  }
}

function loadPlacementsFromSheet() {
  var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
  var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) { return []; }
  var data = sheet.getRange('A1:A' + sheet.getLastRow()).getValues();
  var placements = [];
  for (var i = 0; i < data.length; i++) {
    var val = String(data[i][0]).trim();
    if (val && val.length > 0 && val.indexOf('#') !== 0) {
      placements.push(val);
    }
  }
  return placements;
}

function getExistingExclusions() {
  var exclusions = [];
  try {
    var query = 'SELECT shared_set.name, shared_criterion.type, shared_criterion.placement.url ' +
      'FROM shared_criterion WHERE shared_set.type = "NEGATIVE_PLACEMENTS"';
    var report = AdsApp.report(query);
    var rows = report.rows();
    while (rows.hasNext()) {
      var row = rows.next();
      exclusions.push(String(row['shared_criterion.placement.url'] || '').toLowerCase());
    }
  } catch (e) { }
  return exclusions;
}

function getAccountIterator() {
  var selector = AdsManagerApp.accounts();
  if (CONFIG.ACCOUNT_LABEL) {
    selector = selector.withCondition('LabelNames CONTAINS "' + CONFIG.ACCOUNT_LABEL + '"');
  }
  return selector.get();
}

function isSupportedCampaignPlacement(campaignType, placement) {
  var lower = String(placement || "").trim().toLowerCase();
  var isAppCategory = lower.indexOf("mobileappcategory::") === 0;
  var isMobileApp =
    lower.indexOf("mobileapp::") === 0 ||
    lower.indexOf("com.") === 0 ||
    lower.indexOf("org.") === 0 ||
    /^[12]-/.test(lower);

  if (!isAppCategory && !isMobileApp) {
    return true;
  }

  return campaignType === "VIDEO";
}

function applyPlacementExclusionSafe(campaign, placement) {
  var lower = String(placement || "").trim().toLowerCase();

  // 1. Mobile app category
  if (lower.indexOf("mobileappcategory::") === 0) {
    var catId = lower.split("::")[1];
    if (!catId) {
      throw new Error("Missing mobile app category ID.");
    }
    if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
      var vt = campaign.videoTargeting();
      if (vt.newMobileAppCategoryBuilder && typeof vt.newMobileAppCategoryBuilder === "function") {
        vt.newMobileAppCategoryBuilder().withMobileAppCategoryId(catId).exclude();
        return;
      }
    }
    throw new Error("Mobile app category exclusions are not supported for Display/PMax campaigns at campaign level. Exclude them at the Account Level under Content Suitability.");
  }

  // 2. Mobile application
  var isApp = (lower.indexOf("mobileapp::") === 0 || lower.indexOf("com.") === 0 || lower.indexOf("org.") === 0 || /^[12]-/.test(lower));
  if (isApp) {
    var appId = normalizeMobileAppId(lower);
    if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
      var vt2 = campaign.videoTargeting();
      if (vt2.newMobileApplicationBuilder && typeof vt2.newMobileApplicationBuilder === "function") {
        vt2.newMobileApplicationBuilder().withAppId(appId).exclude();
        return;
      }
    }
    throw new Error("Mobile app exclusions are not supported for Display/PMax campaigns at campaign level. Exclude them at the Account Level under Content Suitability.");
  }

  // 3. Website placements
  if (campaign.display && typeof campaign.display === "function") {
    var d = campaign.display();
    if (d.newPlacementBuilder && typeof d.newPlacementBuilder === "function") {
      d.newPlacementBuilder().withUrl(placement).exclude();
      return;
    }
  }

  if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
    var vt3 = campaign.videoTargeting();
    if (vt3.newPlacementBuilder && typeof vt3.newPlacementBuilder === "function") {
      vt3.newPlacementBuilder().withUrl(placement).exclude();
      return;
    }
  }

  throw new Error("Campaign type not supported for placement exclusions via script.");
}

function normalizeMobileAppId(appId) {
  if (appId.indexOf("mobileapp::") === 0) {
    appId = appId.replace(/^mobileapp::/, "");
  }

  if (appId.indexOf("com.") === 0 || appId.indexOf("org.") === 0) {
    return "2-" + appId;
  }

  return appId;
}
Known issue & fix
Some environments returned runtime errors like "newExcludedPlacementBuilder is not a function". The gist has been updated to use safer builder detection, generic campaign filtering by channel type, and the helperapplyPlacementExclusionSafe(). Preview the script first (Dry Run) and run against 1-3 accounts before scaling.
Embed (client-side):
<script src="https://gist.github.com/PKBibi/656e4314f309e533735eec8e7560fd5e.js"></script>

Script 2: TLD Auto-Exclusion Script

Scans every child account's placement reports via executeInParallel. Identifies placements matching 45+ bad TLDs, 40+ keyword patterns (torrent, pirate, quiz, wallpaper, converter, proxy, etc.), and zero-conversion placements above a configurable spend threshold. Logs everything to a Google Sheet audit trail.

MCC-level45+ bad TLDs40+ keyword patternsPerformance-based exclusions
View full script code
/**
 * TLD & Pattern Auto-Exclusion Script (MCC-Level)
 * PPC Chief | ppcchief.com/blog/gdn-exclusion-list
 *
 * Runs at MCC level. Fans out to every child account via executeInParallel,
 * scans each account's placement reports, and auto-excludes placements
 * matching bad TLDs, keyword patterns, or zero-conversion spend thresholds.
 *
 * SETUP:
 * 1. Deploy at MCC level: Tools > Bulk Actions > Scripts > New Script
 * 2. Configure BAD_TLDS, BAD_KEYWORDS, and CONFIG below
 * 3. Authorise and run preview first
 * 4. Schedule to run daily
 */

var CONFIG = {
  LOOKBACK_DAYS: 30,
  ZERO_CONV_SPEND_THRESHOLD: 10,
  MIN_CLICKS_THRESHOLD: 5,
  ACCOUNT_LABEL: '',
  AUDIT_SHEET_URL: '',
  AUDIT_SHEET_NAME: 'TLD Exclusion Log',
  DRY_RUN: false,
  ALERT_EMAIL: ''
};

var BAD_TLDS = [
  '.xyz', '.buzz', '.one', '.cloud', '.top', '.click', '.link',
  '.gdn', '.bid', '.win', '.download', '.stream', '.racing',
  '.date', '.faith', '.review', '.science', '.party', '.trade',
  '.accountant', '.cricket', '.loan', '.men', '.work', '.icu',
  '.fun', '.site', '.online', '.space', '.website', '.host',
  '.press', '.pw', '.tk', '.ml', '.ga', '.cf', '.gq',
  '.cam', '.rest', '.fit', '.kim', '.guru', '.ninja',
  '.rocks', '.live', '.world', '.today', '.zone'
];

var BAD_KEYWORDS = [
  'torrent', 'pirate', 'crack', 'hack', 'cheat', 'keygen',
  'warez', 'nulled', 'cracked',
  'freevpn', 'freeproxy', 'freedownload', 'freestreaming',
  'wallpaper', 'ringtone', 'screensaver', 'emoji',
  'quiz', 'personality-test', 'whatcharacter',
  'lyrics', 'chords', 'tabs',
  'proxy', 'unblock', 'mirror', 'bypass',
  'converter', 'convertfree', 'onlineconvert',
  'flashgame', 'browsergame', 'playgame', 'freegame',
  'earnmoney', 'makemoney', 'getpaid', 'cashback', 'rewardpoint',
  'clickbait', 'viralstory', 'shocking', 'unbelievable',
  'adultfriend', 'hookup', 'datenight', 'singlemeet',
  'casinoonline', 'slotmachine', 'betfree', 'gamblingfree',
  'spyware', 'malware', 'cleanpc', 'boostpc', 'fixpc',
  'fakeid', 'buyfollower', 'buylikes'
];

function main() {
  var selector = AdsManagerApp.accounts();
  if (CONFIG.ACCOUNT_LABEL) {
    selector = selector.withCondition(
      'LabelNames CONTAINS "' + CONFIG.ACCOUNT_LABEL + '"');
  }

  var childConfig = JSON.stringify({
    lookbackDays: CONFIG.LOOKBACK_DAYS,
    zeroConvSpendThreshold: CONFIG.ZERO_CONV_SPEND_THRESHOLD,
    minClicksThreshold: CONFIG.MIN_CLICKS_THRESHOLD,
    auditSheetUrl: CONFIG.AUDIT_SHEET_URL,
    auditSheetName: CONFIG.AUDIT_SHEET_NAME,
    dryRun: CONFIG.DRY_RUN,
    badTlds: BAD_TLDS,
    badKeywords: BAD_KEYWORDS
  });

  selector.executeInParallel(
    'processChildAccount', 'onAllComplete', childConfig);
}

function processChildAccount(serializedConfig) {
  var cfg = JSON.parse(serializedConfig);
  var account = AdsApp.currentAccount();
  var accountName = account.getName();
  var accountId = account.getCustomerId();

  var result = {
    accountName: accountName, accountId: accountId,
    scanned: 0, byTld: 0, byKeyword: 0, byPerformance: 0,
    applied: 0, alreadyExcluded: 0, unsupportedSkipped: 0,
    errors: [], topExclusions: []
  };

  var today = new Date();
  var lookbackDate = new Date(
    today.getTime() - (cfg.lookbackDays * 24 * 60 * 60 * 1000));
  var dateFrom = fmtDate(lookbackDate);
  var dateTo = fmtDate(today);

  var query =
    'SELECT ' +
    '  detail_placement_view.display_name, ' +
    '  detail_placement_view.target_url, ' +
    '  detail_placement_view.placement_type, ' +
    '  campaign.name, campaign.id, ' +
    '  campaign.advertising_channel_type, ' +
    '  metrics.clicks, metrics.impressions, ' +
    '  metrics.cost_micros, metrics.conversions ' +
    'FROM detail_placement_view ' +
    'WHERE segments.date BETWEEN "' + dateFrom + '" AND "' + dateTo + '" ' +
    '  AND campaign.advertising_channel_type IN ("DISPLAY", "VIDEO") ' +
    '  AND campaign.status != "REMOVED" ' +
    '  AND metrics.impressions > 0 ' +
    'ORDER BY metrics.cost_micros DESC';

  var report;
  try {
    report = AdsApp.report(query);
  } catch (e) {
    try {
      report = AdsApp.report(
        'SELECT Criteria, DisplayName, CampaignName, CampaignId, ' +
        'Clicks, Impressions, Cost, Conversions ' +
        'FROM PLACEMENT_PERFORMANCE_REPORT WHERE Impressions > 0 ' +
        'DURING ' + dateFrom.replace(/-/g, '') + ',' +
        dateTo.replace(/-/g, ''));
    } catch (e2) {
      result.errors.push('Report failed: ' + e2.message);
      return JSON.stringify(result);
    }
  }

  var rows = report.rows();
  var exclusionQueue = [];
  var seenPlacements = {};

  while (rows.hasNext()) {
    var row = rows.next();
    result.scanned++;

    var placement = String(
      row['detail_placement_view.target_url'] ||
      row['detail_placement_view.display_name'] ||
      row['Criteria'] || ''
    ).trim().toLowerCase();

    var campaignId = row['campaign.id'] || row['CampaignId'];
    var campaignName = row['campaign.name'] || row['CampaignName'];
    var campaignType = row['campaign.advertising_channel_type'] || '';
    var clicks = parseInt(row['metrics.clicks'] || row['Clicks'] || 0);
    var costMicros = parseInt(row['metrics.cost_micros'] || 0);
    var cost = costMicros > 0
      ? costMicros / 1000000
      : parseFloat(row['Cost'] || 0);
    var conversions = parseFloat(
      row['metrics.conversions'] || row['Conversions'] || 0);

    if (!placement || placement === '(not set)') continue;
    var placementKey = placement + '|' + campaignId;
    if (seenPlacements[placementKey]) continue;

    var excludeReason = null;

    for (var t = 0; t < cfg.badTlds.length; t++) {
      if (endsWithTld(placement, cfg.badTlds[t])) {
        excludeReason = 'BAD_TLD: ' + cfg.badTlds[t];
        result.byTld++;
        break;
      }
    }

    if (!excludeReason) {
      for (var k = 0; k < cfg.badKeywords.length; k++) {
        if (placement.indexOf(cfg.badKeywords[k]) !== -1) {
          excludeReason = 'BAD_KEYWORD: ' + cfg.badKeywords[k];
          result.byKeyword++;
          break;
        }
      }
    }

    if (!excludeReason &&
        clicks >= cfg.minClicksThreshold &&
        cost >= cfg.zeroConvSpendThreshold &&
        conversions === 0) {
      excludeReason = 'ZERO_CONV: ' + cost.toFixed(2) +
        ' spent, ' + clicks + ' clicks, 0 conv';
      result.byPerformance++;
    }

    if (excludeReason) {
      seenPlacements[placementKey] = true;
      exclusionQueue.push({
        placement: placement, campaignId: campaignId,
        campaignName: campaignName, campaignType: campaignType,
        reason: excludeReason,
        spend: cost, clicks: clicks, conversions: conversions
      });
    }
  }

  // Apply exclusions safely
  for (var i = 0; i < exclusionQueue.length; i++) {
    var item = exclusionQueue[i];
    if (item.campaignType && !isSupportedCampaignPlacement(item.campaignType, item.placement)) {
      result.unsupportedSkipped++;
      continue;
    }
    try {
      if (!cfg.dryRun) {
        var campaign = AdsApp.campaigns().withIds([item.campaignId]).get();
        var camp;
        if (campaign.hasNext()) {
          camp = campaign.next();
        } else {
          var videoCampaign = AdsApp.videoCampaigns().withIds([item.campaignId]).get();
          if (videoCampaign.hasNext()) {
            camp = videoCampaign.next();
          }
        }

        if (camp) {
          applyPlacementExclusionSafe(camp, item.placement);
        } else {
          result.errors.push(item.placement + ": campaign not found");
          continue;
        }
      }
      result.applied++;
    } catch (e) {
      if (e.message && e.message.indexOf('already exists') !== -1) {
        result.alreadyExcluded++;
      } else {
        result.errors.push(item.placement + ': ' + e.message);
      }
    }
  }

  Logger.log('[' + accountId + '] ' + accountName +
    ': scanned ' + result.scanned +
    ', excluded ' + result.applied +
    ' (TLD:' + result.byTld +
    ' KW:' + result.byKeyword +
    ' PERF:' + result.byPerformance + ')');

  return JSON.stringify(result);
}

function onAllComplete(results) {
  var totals = {
    accounts: 0, scanned: 0, applied: 0,
    byTld: 0, byKeyword: 0, byPerformance: 0,
    alreadyExcluded: 0, unsupportedSkipped: 0, errors: 0
  };
  var summaryLines = [];

  for (var i = 0; i < results.length; i++) {
    var r = JSON.parse(results[i].getReturnValue());
    totals.accounts++;
    totals.scanned += r.scanned;
    totals.applied += r.applied;
    totals.byTld += r.byTld;
    totals.byKeyword += r.byKeyword;
    totals.byPerformance += r.byPerformance;
    totals.alreadyExcluded += r.alreadyExcluded;
    totals.unsupportedSkipped += r.unsupportedSkipped || 0;
    totals.errors += r.errors.length;

    if (r.applied > 0 || r.errors.length > 0) {
      summaryLines.push(
        r.accountName + ' (' + r.accountId + '): +' +
        r.applied + ' excluded, ' + r.alreadyExcluded + ' existing' +
        (r.errors.length > 0
          ? ', ' + r.errors.length + ' errors' : ''));
    }
  }

  Logger.log('=== TLD AUTO-EXCLUSION COMPLETE ===');
  Logger.log('Accounts: ' + totals.accounts);
  Logger.log('Scanned: ' + totals.scanned);
  Logger.log('Excluded: ' + totals.applied);
  Logger.log('Already excluded: ' + totals.alreadyExcluded);
  Logger.log('Unsupported skipped: ' + totals.unsupportedSkipped);

  if (CONFIG.ALERT_EMAIL && totals.applied > 0) {
    MailApp.sendEmail(CONFIG.ALERT_EMAIL,
      'TLD Auto-Exclusion: +' + totals.applied +
        ' across ' + totals.accounts + ' accounts',
      'Run: ' + new Date().toISOString() + '\n' +
      'Excluded: ' + totals.applied + '\n' +
      'Unsupported skipped: ' + totals.unsupportedSkipped + '\n' +
      summaryLines.join('\n'));
  }
}

function endsWithTld(placement, tld) {
  var domain = placement.replace(/^https?:\/\//, '')
    .replace(/\/.*$/, '').replace(/:\d+$/, '');
  if (domain.length <= tld.length) return false;
  return domain.substring(domain.length - tld.length) === tld;
}

function fmtDate(date) {
  var y = date.getFullYear();
  var m = ('0' + (date.getMonth() + 1)).slice(-2);
  var d = ('0' + date.getDate()).slice(-2);
  return y + '-' + m + '-' + d;
}

function isSupportedCampaignPlacement(campaignType, placement) {
  var lower = String(placement || "").trim().toLowerCase();
  var isAppCategory = lower.indexOf("mobileappcategory::") === 0;
  var isMobileApp =
    lower.indexOf("mobileapp::") === 0 ||
    lower.indexOf("com.") === 0 ||
    lower.indexOf("org.") === 0 ||
    /^[12]-/.test(lower);

  if (!isAppCategory && !isMobileApp) {
    return true;
  }

  return campaignType === "VIDEO";
}

function applyPlacementExclusionSafe(campaign, placement) {
  var lower = String(placement || "").trim().toLowerCase();

  // 1. Mobile app category
  if (lower.indexOf("mobileappcategory::") === 0) {
    var catId = lower.split("::")[1];
    if (!catId) {
      throw new Error("Missing mobile app category ID.");
    }
    if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
      var vt = campaign.videoTargeting();
      if (vt.newMobileAppCategoryBuilder && typeof vt.newMobileAppCategoryBuilder === "function") {
        vt.newMobileAppCategoryBuilder().withMobileAppCategoryId(catId).exclude();
        return;
      }
    }
    throw new Error("Mobile app category exclusions not supported via script for Display/PMax.");
  }

  // 2. Mobile application
  var isApp = (lower.indexOf("mobileapp::") === 0 || lower.indexOf("com.") === 0 || lower.indexOf("org.") === 0 || /^[12]-/.test(lower));
  if (isApp) {
    var appId = normalizeMobileAppId(lower);
    if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
      var vt2 = campaign.videoTargeting();
      if (vt2.newMobileApplicationBuilder && typeof vt2.newMobileApplicationBuilder === "function") {
        vt2.newMobileApplicationBuilder().withAppId(appId).exclude();
        return;
      }
    }
    throw new Error("Mobile app exclusions not supported via script for Display/PMax.");
  }

  // 3. Website placements
  if (campaign.display && typeof campaign.display === "function") {
    var d = campaign.display();
    if (d.newPlacementBuilder && typeof d.newPlacementBuilder === "function") {
      d.newPlacementBuilder().withUrl(placement).exclude();
      return;
    }
  }

  if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
    var vt3 = campaign.videoTargeting();
    if (vt3.newPlacementBuilder && typeof vt3.newPlacementBuilder === "function") {
      vt3.newPlacementBuilder().withUrl(placement).exclude();
      return;
    }
  }

  throw new Error("Campaign type not supported for placement exclusions via script.");
}

function normalizeMobileAppId(appId) {
  if (appId.indexOf("mobileapp::") === 0) {
    appId = appId.replace(/^mobileapp::/, "");
  }

  if (appId.indexOf("com.") === 0 || appId.indexOf("org.") === 0) {
    return "2-" + appId;
  }

  return appId;
}
Notes
This auto-exclusion script was updated to be more conservative when auto-excluding app/category items and to write an audit trail to your configured Google Sheet. Always run in Preview (dry run) first and verify the audit sheet rows before switching to live mode.
Embed (client-side):
<script src="https://gist.github.com/PKBibi/4503042b0ddef2c1bd036a66fab554b1.js"></script>

How to install the scripts

  1. In your MCC, go to Tools > Bulk Actions > Scripts
  2. Click New Script and paste the script code (expand it above or copy from the GitHub Gist)
  3. Update the configuration at the top: set your Google Sheet URL, optional account label filter, and email alert address
  4. Click Authorize and grant permissions
  5. Click Preview to run a test (check the logs)
  6. Set frequency to Daily

Step-by-Step: How to Protect Your Google Ads Budget from GDN Click Fraud

  1. Download the exclusion list. Copy our Google Sheet to your Drive. It contains 228+ placements: domains, app categories, and fraudulent app IDs.
  2. Create exclusion lists in Google Ads. MCC level: Tools > Shared Library > Placement Exclusion Lists. You get 3 lists with 250,000 placements each.
  3. Exclude all mobile apps. Add mobileappcategory::69500 unless app installs are a KPI.
  4. Enable content suitability exclusions. Parked domains, sensitive categories, DL-MA content label.
  5. Deploy the MCC Placement Excluder script. Point it at your Google Sheet. Schedule daily.
  6. Deploy the TLD Auto-Exclusion script. Catches new bad placements automatically. Schedule daily.
  7. Supplement with third-party lists. Add Lunio 100K, Level Agency 55K, and DirectOM 70K lists to your sheet.
  8. Review placement reports monthly. Share bad placements across accounts. One bad placement in one account means it is bad for all accounts.

Third-Party Exclusion Lists to Combine with Ours

SourceSizeFocus
Lunio100,000+65K+ websites, apps, YouTube channels. Built from hundreds of accounts' fraud data.
Level Agency55,000Based on $500K+ combined GDN spend analysis.
DirectOM70,000+Categorised by dating, mobile, gaming, quizzes, sports, high-cost/low-performing.
FraudBlocker450+Focused specifically on MFA/ad-arbitrage domains.
WebMechanix200+ categoriesOrganised by domain theme for selective exclusion.

Google Ads Exclusion Limits

  • Account level: up to 20 exclusion lists, 65,000 placements total
  • MCC level: up to 3 shared exclusion lists, 250,000 placements each
  • Time to take effect: new exclusions typically activate within 12 hours
  • Important caveat: the mobile app category exclusion does not catch every app. Google continuously adds new app IDs, and some in-app ads serve via webviews that appear as browser traffic. Automated scripts provide a second layer of protection.

Content Suitability Settings to Enable

In addition to placement exclusions, enable these content controls in every account:

Content types to exclude

  • Parked domains (auto-excluded since Oct 2024; fully removed from Search Partners as of Feb 2026)
  • Below-the-fold ad slots
  • Embedded YouTube videos (if you want control over video placements)

Sensitive content categories to exclude

  • Tragedy and conflict
  • Sensitive social issues
  • Sexually suggestive content
  • Sensational and shocking
  • Profanity and rough language
  • Juvenile, gross, and bizarre content

Digital content labels

  • Exclude DL-MA (mature audiences) unless your product is relevant
  • Consider excluding DL-T (teen) for conservative brands

GDN Exclusion List FAQ

  • A GDN exclusion list is a collection of websites, mobile apps, and app categories that you block from showing your Google Display Network ads. Exclusion lists prevent your budget from being wasted on low-quality placements, click fraud, Made For Advertising (MFA) sites, and irrelevant inventory. You upload them in Google Ads under Tools > Shared Library > Placement Exclusion Lists.
  • At the individual account level, you can exclude up to 65,000 placements total across up to 20 exclusion lists. At the MCC (manager account) level, you can create up to 3 shared exclusion lists with up to 250,000 placements each. New exclusions typically take effect within 12 hours.
  • Industry research shows approximately 15% of GDN clicks are fraudulent on average (CHEQ). The ANA found that 21% of programmatic impressions land on Made For Advertising (MFA) sites, and MFA sites generate $13 billion globally per year. Seer Interactive found that up to 30% of Search Partner spend was fraudulent traffic.
  • MFA sites are websites built purely to generate advertising revenue through ad arbitrage. They buy cheap traffic via content recommendation widgets like Taboola and Outbrain, then monetise visitors with dense ad placements. MFA sites typically feature low-quality content, multiple pages per article (slideshow format), and 10+ ad units per page. Jounce Media reported a 38% year-over-year rise in active MFA domains.
  • Yes, unless mobile app installs are a specific campaign objective. Mobile apps are the single largest source of accidental clicks and fraudulent traffic on GDN. Children's games, flashlight apps, battery optimisers, and free utility apps generate enormous volumes of invalid clicks. Exclude all apps by adding mobileappcategory::69500 as a placement exclusion.
  • Add mobileappcategory::69500 as a placement exclusion at the account or campaign level. This is the parent category ID that covers all mobile app subcategories across both Google Play and the Apple App Store. You can add it in Google Ads under Tools > Shared Library > Placement Exclusion Lists, or apply it directly to individual campaigns.
  • Review placement reports at least monthly, ideally weekly for accounts with significant Display spend. New fraudulent sites and apps appear constantly. Automated scripts that scan placement reports daily and exclude bad placements by TLD pattern, keyword, or performance threshold provide the best ongoing protection.
  • TLDs with disproportionately high fraud and MFA rates include .xyz, .buzz, .click, .bid, .win, .download, .stream, .gdn, .top, .tk, .ml, .ga, .cf, and .gq. Free TLDs (.tk, .ml, .ga, .cf, .gq) are especially problematic because they attract spam and parked domains. The .gdn TLD is literally named after Google Display Network and is almost exclusively used for ad fraud.