◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-07-05Intermediate

Keep the Self-Debugging Agent Away From Live Ads: Three Layers Against AdMob Invalid Traffic

When Antigravity 2.0's real-browser self-debug renders a live AdMob unit, every pass counts as an impression, and Google may read it as invalid traffic. Here is a three-layer setup, with measurements, that keeps the agent from ever touching a production ad.

Antigravity376AdMob12self-debug3invalid trafficindie developer12

✦ Premium Article

One evening I had Antigravity 2.0 fix a monetization flow in one of my wallpaper apps. The instruction was simple: "The review-prompt modal opens twice, please fix it." The agent launched a real Chrome, walked the app preview by clicking through it, and verified its fix by repeating the flow again and again. A genuinely useful thing to watch.

The next morning I opened the AdMob dashboard and paused. Impressions had climbed in an unnatural way overnight. The reason was immediate. The modal I had asked it to fix opened right after an ad. Every time the agent verified the screen, it rendered that screen, and every render logged a real impression against a production ad unit.

For a solo developer, ad revenue funnels into a single account. Accumulated invalid traffic does not just get your earnings clawed back; it can put the whole account at risk. Self-debug is powerful, and for a while I simply did not notice that ads were running underneath it.

Why self-debug is dangerous around ads

When a human verifies by hand, we avoid ads without thinking. There is an unspoken "this is a test, I won't count that ad." An agent has no such reflex. If an ad appears on screen, Google sees one real impression. As the agent hunts for a button to click, a stray tap inside the ad frame is recorded as a real click.

Worse, self-debug runs on a dev machine or a cloud sandbox. A datacenter IP, plus the same screen traversed dozens of times in a short window, is exactly the pattern Google's invalid-traffic detection dislikes most. There is no bad intent, but the footprint is indistinguishable from deliberate inflation.

The key point is that a single countermeasure leaks. Force test ads, and a mediation adapter may still fire a production request through another path. Block the network, and you may still want the consent (UMP) form to load. That is why we stack three layers with different failure modes.

Layer 1: Force test ad units at build time

The first layer swaps the ad unit IDs themselves. Google publishes dedicated test ad unit IDs. They always return test ads and touch neither revenue nor invalid traffic. Add an "agent verify" build mode and route all unit resolution through one function that returns only test IDs in that mode.

On iOS (Swift), keep ad-unit resolution in a single place.

import Foundation
 
enum AdEnvironment {
    // Injected via env var or build setting. Pass AGENT_VERIFY=1 in CI or agent runs.
    static var isAgentVerify: Bool {
        ProcessInfo.processInfo.environment["AGENT_VERIFY"] == "1"
    }
}
 
enum AdUnit {
    // Google's public test ad units (always serve test ads, zero revenue)
    private static let testBanner = "ca-app-pub-3940256099942544/2934735716"
    private static let testInterstitial = "ca-app-pub-3940256099942544/4411468910"
 
    // Production IDs come from Info.plist / env. Never hardcode them here.
    private static var prodBanner: String {
        Bundle.main.object(forInfoDictionaryKey: "AD_BANNER_UNIT") as? String ?? ""
    }
    private static var prodInterstitial: String {
        Bundle.main.object(forInfoDictionaryKey: "AD_INTERSTITIAL_UNIT") as? String ?? ""
    }
 
    static var banner: String {
        AdEnvironment.isAgentVerify ? testBanner : prodBanner
    }
    static var interstitial: String {
        AdEnvironment.isAgentVerify ? testInterstitial : prodInterstitial
    }
}

Registering your verification environment as a test device adds a second safety net: even if the ID swap is missed, requests from that device are treated as test traffic.

import GoogleMobileAds
 
func configureAdsForVerification() {
    let config = MobileAds.shared.requestConfiguration
    if AdEnvironment.isAgentVerify {
        // Register the verification device hash (no effect on real users)
        config.testDeviceIdentifiers = ["SIMULATOR", "AGENT_DEVICE_HASH"]
    }
}

Android (Kotlin) follows the same idea: add one BuildConfig flag and register the test device.

import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.RequestConfiguration
 
fun configureAdsForVerification() {
    if (BuildConfig.AGENT_VERIFY) {
        val config = RequestConfiguration.Builder()
            .setTestDeviceIds(listOf("AGENT_DEVICE_HASH"))
            .build()
        MobileAds.getInstance().requestConfiguration = config
    }
}

This layer alone replaces intended ad displays with test ads. But that assumes the app behaves nicely. The next layer closes the paths that do not.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦How real-browser self-debug quietly logs impressions against your production ad units
✦A three-layer setup: forced test ads, network blocking, and a preflight gate that refuses production builds
✦Before-and-after numbers for logged impressions and eCPM after adding the three layers
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-09-29
Purging Cloudflare Didn't Bring Back the New Wallpapers: the Stale JPEGs Were Held by XPageSpeed on the Origin
An operations record from a wallpaper app: images overwritten at the same URL kept arriving stale even after a Cloudflare purge. Covers the three-way local/origin/edge probe that names the guilty layer, when prefix purge beats per-URL purge, and how to switch off Xserver's XPageSpeed in .htaccess without a 500.
▣ App Dev2026-05-21
Tuning AdMob Placements at Runtime with Firebase Remote Config and an Antigravity Agent
A practical look at how I combine Firebase Remote Config with an Antigravity Agent to nudge AdMob placements while the app is live, drawn from years of running wallpaper apps as an indie developer.
▣ App Dev2026-05-10
Three Decisions I Kept for Myself When Antigravity Maintained My Older Apps
How far could Antigravity maintain my long-running wallpaper and wellness apps? A field log of crash fixes and a staged rollout, with the lines I drew between delegated and retained work, and how I write context notes.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links