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.

Antigravity311AdMob11self-debug3invalid trafficindie developer11

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 $10 for lifetime access
View Membership →

Related Articles

App Dev2026-06-21
A Few Low-Density Phones Lost Their Bundled Wallpaper — The drawable vs nodpi Boundary in Play's Density Splits
App Bundle density splits will happily split images that should never be split, dropping a static resource on one density bucket only. Here is how I reproduced it with bundletool and fixed it by moving to drawable-nodpi or disabling density split — with the decision criteria.
App Dev2026-05-26
Unifying In-App Review Prompts Across 5 Apps with Antigravity Editor — A Few Days of Notes
Notes from a few days spent unifying SKStoreReviewController trigger conditions across five iOS wallpaper apps I run as an indie developer, using Antigravity Editor's multi-file editing to bring the logic onto one shared coordinator.
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.
📚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
See all →