For Prebid.js, AdSense and Google Ad Manager

Stop invalid traffic before the bid, not after the invoice.

RTBProtect detects IVT and SIVT before an ad is requested — bots, headless browsers, datacenter proxies, and spoofed devices are flagged and blocked before a single bid request leaves the page. Run it inside the Prebid auction, or in front of your AdSense or Ad Manager tag so bots never load it.

Prebid.js: a real-time data module, no wrapper changes · AdSense / Ad Manager: one inline script in <head>, ad units untouched

auction wire — live detecting
0 users passed 0 bots rejected
IVT + SIVT
detection coverage
Pre-bid
demand suppression
Your call
fail-open or fail-closed
3 paths
Prebid · AdSense · GAM
How it works

Detect early, settle once, enforce natively.

Detection starts the moment the page starts, the verdict settles exactly once per page, and enforcement happens where the ads are requested: in Prebid.js through its own activity-control system — the same mechanism consent modules use — or, for AdSense and Google Ad Manager, by loading the ad library only after a pass.

STEP 01 — DETECT

Load & analyze

The RTBProtect detector loads first — at setConfig time in Prebid, or from the inline gate at the top of <head> for AdSense or Ad Manager — and starts analysis immediately, racing your page setup rather than waiting for an ad call. No cookies, no persistent identifiers.

STEP 02 — SETTLE

One verdict per page

The first ad call is held while the verdict settles — bounded by your auctionDelay in Prebid, or by the ceiling you pass to detect() for AdSense and Ad Manager. It settles exactly once, so every later auction or ad unit reads it instantly.

STEP 03 — ENFORCE

Deny demand, leave a trail

In Prebid, an invalid verdict makes activity controls deny fetchBids and addBidResponse, and every request carries the verdict in ortb2 and ad server targeting as an audit trail. For AdSense and Ad Manager, the ad library — adsbygoogle.js or gpt.js — is simply never loaded: no ad request, no invalid impression on your account.

Why pre-bid

Built into the ad stack, not bolted onto it.

Pre-bid blocking

Invalid sessions are denied at the activity-control layer — bidders are never called, so bad traffic never inflates bid volume or discrepancy reports.

Signal enrichment

The verdict is written to device.ext.rtbprotect in ortb2 and to an ad server key-value, so demand partners, analytics, and GAM all see the same decision.

Latency-budgeted

Detection races your page setup and the in-auction wait is capped by a hard timeout. With failOpen set, a slow or blocked detector never costs you an auction.

Consent stand-aside

If a consent module denies the detector load, RTBProtect stands aside and lets the auction run. Privacy outcomes are never treated as evidence of invalid traffic.

IVT + SIVT coverage

Declared bots, crawlers, and datacenter traffic — plus sophisticated invalid traffic: headless automation, device emulation, and spoofed user agents.

AdSense & GAM gate

The same pass/fail verdict gates the ad library itself — adsbygoogle.js or gpt.js only loads for visitors that pass — or, through the onVerdict callback in Prebid, the GAM request. A few lines of page code either way.

Documentation

Protect AdSense and Google Ad Manager

Load the ad library — adsbygoogle.js for AdSense, gpt.js for Ad Manager — only for visitors that pass the bot check. Bots never fetch it, never trigger an ad request, and never count as invalid traffic against your account. One inline script in the page head; the ad code itself is not modified — only its loading is deferred, the same shape as a consent-manager gate.

Requirements

before you start
  • Your AdSense publisher ID (ca-pub-…) — the one already in your AdSense head tag.
  • A RTBProtect client ID, issued when your account is provisioned. It goes in the loader's last argument (1 in the example below).
  • Access to the page <head>. The gate is a classic inline script, so any CMS or theme that lets you edit the head will do — no build step, no tag-manager container.
Fail-closed by designOnly an explicit pass loads AdSense. A detection or a missing verdict — timeout, detector blocked — loads nothing, so a session that can't be vouched for never generates an ad request. A fail-open variant is shown under how the gate behaves.

1 · Remove the AdSense head tag

delete, don't keep

Delete the existing AdSense script element from your page — the one whose src is:

the tag to delete
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX" crossorigin="anonymous"></script>

If it stays, the tag loads unconditionally, the gate is bypassed, and AdSense warns about more than one head tag on the page. The gate re-creates this exact tag after a pass.

2 · Paste the gate

<head>, as early as possible

Paste this into <head> as a classic inline script, as early as possible — the verdict is on the critical path for ad render, so the sooner the detector loads, the sooner ads can. The first part is the RTBProtect loader; the second gates AdSense on its verdict.

html — paste into <head>
<script>
// RTBProtect loader: fetches the detector and exposes rtbprotect.detect(ms)
!function(t, w, d, i, s, g) {
  if (w[t]) return;
  w[t] = ((o) => (o.d = new Promise(r => o.dr = r), o))({
    client_id: i,
    detect: async function(m) {
      return await Promise.race([w[t].d, new Promise((r) => {setTimeout(() => {r({error: 'script_init_timeout'})}, m)})]);
    }
  });
  s = d.createElement("script");
  s.async = !0;
  s.src = "https://api.rtbprotect.com/client";
  g = d.getElementsByTagName('script')[0];
  g.parentNode.insertBefore(s, g)
}("rtbprotect", window, document, 1);

// Gate AdSense on the verdict. Only an explicit pass loads the tag; a detection
// or a missing verdict (timeout, script blocked) loads nothing.
var ADSENSE_CLIENT = "ca-pub-XXXXXXXXXXXXXXXX";

rtbprotect.detect(10000).then(function (res) {
  if (res.detected !== false) return; // bot, or no verdict: no AdSense
  var s = document.createElement("script");
  s.async = true;
  s.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=" + encodeURIComponent(ADSENSE_CLIENT);
  s.crossOrigin = "anonymous";
  document.head.appendChild(s);
});
</script>
What the page seesrtbprotect.detect(ms) resolves with a single { detected: true | false }, or { error: 'script_init_timeout' } if no verdict arrived within ms. No scores, no reasons, no identifiers — nothing on the page can tell why a session was flagged.

3 · Set your publisher ID

ADSENSE_CLIENT

Replace the placeholder with the ca-pub-… ID from the tag you deleted in step 1:

javascript
var ADSENSE_CLIENT = "ca-pub-1234567890123456";

That's the whole integration. Reload the page: real visitors get AdSense exactly as before, and a bot gets a page with no ad code on it.

What stays the same

ad units · auto ads
  • Ad units. Leave every <ins class="adsbygoogle" …> and its (adsbygoogle = window.adsbygoogle || []).push({}) exactly where they are. Until the library loads, adsbygoogle is a plain array and the pushes just queue; the library drains the queue when it arrives after a pass. That is also why Google's own async tag works with pushes that run before the script has loaded.
  • Auto ads. The head tag was the only piece, so nothing else changes.
  • The AdSense code. Not modified — only its loading is deferred.

How the gate behaves

verdict → outcome
VerdictMeaningOutcome
{ detected: false }Real visitoradsbygoogle.js is injected; queued ad units render as usual.
{ detected: true }Bot, headless browser, datacenter or spoofed sessionNothing loads. No ad request, no invalid impression.
{ error: 'script_init_timeout' }No verdict within the ceiling — detector blocked or unreachableNothing loads (fail-closed).

The 10000 ms passed to detect() is a ceiling, not a delay: the promise resolves as soon as the verdict is in, normally well under a second. Fail-closed means no ads while the detection endpoint is unreachable. If you'd rather fail open on res.error, flip that deliberately by changing the first line of the callback:

javascript — fail-open variant
if (res.detected === true) return; // bot: no AdSense. Pass or no verdict: load it

Alternative: preload & pause

pauseAdRequests

If you'd rather keep Google's head tag on the page — so the library is parsed and ready before the verdict — gate only the ad requests, using AdSense's own pause switch instead of injecting the tag. Before the head tag, in an inline script, run:

javascript — before the AdSense head tag
(adsbygoogle = window.adsbygoogle || []).pauseAdRequests = 1;

and replace the gate from step 2 with:

javascript — the gate
rtbprotect.detect(10000).then(function (res) {
  if (res.detected === false) (adsbygoogle = window.adsbygoogle || []).pauseAdRequests = 0;
});
Order matterspauseAdRequests must be set to 1 before any push({}) runs, and it applies to ad units and Auto ads alike. Without the reset to 0 no ads are ever shown — which is the point on a fail.

Test & verify

verification
  1. As a real visitor: open the page with DevTools → Network. api.rtbprotect.com/client loads first, then adsbygoogle.js, then the ad requests. In the console, rtbprotect.detect(1000).then(console.log) prints {detected: false}.
  2. As a bot: load the page under browser automation — Puppeteer, Playwright, Selenium, anything that sets navigator.webdriver. adsbygoogle.js must never be requested.
  3. With the detector blocked: block api.rtbprotect.com in DevTools and confirm ads stay off once the ceiling passes — or load, if you chose the fail-open variant.

Exactly one AdSense head tag should ever be in the DOM after a pass. If AdSense reports duplicate tags, step 1 was skipped.

Google Ad Manager

direct GPT integration

The same gate, for the Google Publisher Tag: gpt.js loads only after a pass, so no slot is ever requested and no invalid impression reaches your Ad Manager reports. Two steps, and nothing to fill in — GPT's URL carries no publisher ID; your network code lives in the ad unit paths, which stay where they are.

1 · Remove the GPT head tag

delete, don't keep

Delete the GPT script element from your page — the one whose src is:

the tag to delete
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous"></script>

Keep the setup block that follows it — window.googletag = window.googletag || {cmd: []} and your googletag.cmd.push(…) calls — exactly as it is. If the tag stays, the library loads unconditionally and the gate is bypassed.

2 · Paste the gate

<head>, above your googletag setup block

Paste this into <head> as a classic inline script, as early as possible and above your googletag setup block — the verdict is on the critical path for ad render. The first part is the RTBProtect loader; the second gates GPT on its verdict.

html — paste into <head>
<script>
// RTBProtect loader: fetches the detector and exposes rtbprotect.detect(ms)
!function(t, w, d, i, s, g) {
  if (w[t]) return;
  w[t] = ((o) => (o.d = new Promise(r => o.dr = r), o))({
    client_id: i,
    detect: async function(m) {
      return await Promise.race([w[t].d, new Promise((r) => {setTimeout(() => {r({error: 'script_init_timeout'})}, m)})]);
    }
  });
  s = d.createElement("script");
  s.async = !0;
  s.src = "https://api.rtbprotect.com/client";
  g = d.getElementsByTagName('script')[0];
  g.parentNode.insertBefore(s, g)
}("rtbprotect", window, document, 1);

// Gate GPT on the verdict. Only an explicit pass loads the library; a detection
// or a missing verdict (timeout, script blocked) loads nothing.
window.googletag = window.googletag || { cmd: [] }; // so setup and display pushes queue safely

rtbprotect.detect(10000).then(function (res) {
  if (res.detected !== false) return; // bot, or no verdict: no GPT, no ad requests
  var s = document.createElement("script");
  s.async = true;
  s.src = "https://securepubads.g.doubleclick.net/tag/js/gpt.js";
  s.crossOrigin = "anonymous";
  document.head.appendChild(s);
});
</script>

That's the whole integration. The verdict and its outcomes are the ones in how the gate behaves: a pass injects gpt.js and the queued setup and display calls run in order; a bot or a missing verdict loads nothing. The fail-open variant is the same one-line change.

What stays the same

setup block · slots
  • The setup block. window.googletag = window.googletag || {cmd: []} and every googletag.cmd.push(function () { defineSlot(…); enableServices(); }) stay exactly where they are. Until gpt.js loads, googletag.cmd is a plain array and the pushes just queue; the library drains the queue in order when it arrives after a pass — which is also why Google's own async tag works with pushes that run before the script has loaded.
  • Ad slots. Every <div id="div-gpt-ad-…"> and its googletag.cmd.push(function () { googletag.display('div-gpt-ad-…'); }) stay too — they queue the same way. Pushes made after gpt.js has loaded run immediately, as always.
  • The GPT code. Not modified — only its loading is deferred.

Alternative: preload & disableInitialLoad

googletag.pubads().refresh()

If you'd rather keep Google's head tag on the page — so gpt.js is parsed and the slots are defined before the verdict — gate only the ad requests with GPT's own switch: disableInitialLoad() makes googletag.display() register a slot without fetching it, and refresh() fetches every displayed slot at once. In your setup block, before enableServices(), add:

javascript — inside your setup push, before enableServices()
googletag.pubads().disableInitialLoad();

and replace the gate from step 2 with:

javascript — the gate
rtbprotect.detect(10000).then(function (res) {
  if (res.detected !== false) return;
  var go = function () {
    googletag.cmd.push(function () { googletag.pubads().refresh(); });
  };
  // refresh() only fetches slots that have already been display()ed, and the body's
  // display() pushes may still be parsing when an early verdict lands - so wait for them.
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
  else go();
});
Order mattersdisableInitialLoad() must run before enableServices() and applies to every slot on the page. Without the refresh() no ads are ever shown — which is the point on a fail. Slots added after the refresh (lazy or infinite-scroll units) need their own refresh([slot]) call, as they do in any disableInitialLoad setup.

Test & verify

verification
  1. As a real visitor: DevTools → Network shows api.rtbprotect.com/client, then gpt.js, then the ad requests to securepubads.g.doubleclick.net/gampad/ads. Slots render as before.
  2. As a bot: load the page under browser automation — anything that sets navigator.webdriver. gpt.js must never be requested. With the preload alternative, gpt.js loads but no gampad/ads request is ever made.
  3. With the detector blocked: block api.rtbprotect.com in DevTools and confirm no ad requests once the ceiling passes — or that they go out, if you chose the fail-open variant.

Every bot you reject pre-bid is a clawback you never file.

Get a client ID Setup guides