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.
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 Foundationenum 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 GoogleMobileAdsfunc 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.MobileAdsimport com.google.android.gms.ads.RequestConfigurationfun 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.
Layer 2: Block the network in the agent's environment
Self-debug drives the app or site from the outside. So the surest move is to stop ad-serving traffic in the environment where the agent runs. Even if the test-ID swap leaks, no request leaving the box means no production impression.
In the agent sandbox or verification container, make the major ad-serving domains unresolvable.
#!/usr/bin/env bash# block-ad-domains.sh — block ad-serving domains in the agent verify environmentset -euo pipefailHOSTS=/etc/hostsMARKER="# --- agent-verify ad block ---"if grep -q "$MARKER" "$HOSTS"; then echo "Already applied" exit 0ficat <<'BLOCK' | sudo tee -a "$HOSTS" >/dev/null# --- agent-verify ad block ---127.0.0.1 googleads.g.doubleclick.net127.0.0.1 pagead2.googlesyndication.com127.0.0.1 securepubads.g.doubleclick.net127.0.0.1 tpc.googlesyndication.com127.0.0.1 www.googleadservices.com# --- end agent-verify ad block ---BLOCKecho "Ad-serving domains blocked (consent-form fetch uses other domains, so it still works)"
The gotcha here is over-blocking. If you cut off the consent platform (UMP) form fetch or your app's own API, the very flow you wanted to verify stops working. Target only the ad-serving domains and let the rest through. That line is what keeps verification reproducible. If you have ever tripped over consent initialization order, the ATT-before-ad-SDK init-order bug pairs well with this and makes the order-versus-blocking relationship easier to grasp.
Layer 3: A preflight gate that rejects production ad builds
The third layer stops human mistakes with a machine. However well you arrange layers one and two, a single "I forgot to set the test flag today" undoes them. So, right before launching self-debug, inspect whether the build still carries active production ad unit IDs, and refuse to start if it does.
#!/usr/bin/env python3"""preflight_ad_guard.py — refuse to launch self-debug against a production ad build.Exit 1 if any production AdMob unit ID remains in build output or config."""import osimport reimport sysfrom pathlib import Path# Common prefix of Google's public test app/unitTEST_PUB = "ca-app-pub-3940256099942544"# Production unit shape: ca-app-pub-XXXX/YYYY that is not the test prefixUNIT_RE = re.compile(r"ca-app-pub-\d{16}/\d{6,}")def find_prod_units(root: Path) -> list[str]: hits = [] targets = [".plist", ".xml", ".json", ".js", ".ts", ".dart", ".kt", ".swift"] for path in root.rglob("*"): if path.suffix not in targets or not path.is_file(): continue try: text = path.read_text(encoding="utf-8", errors="ignore") except OSError: continue for m in UNIT_RE.findall(text): if not m.startswith(TEST_PUB): hits.append(f"{path}: {m}") return hitsdef main() -> int: if os.environ.get("AGENT_VERIFY") != "1": print("AGENT_VERIFY=1 is not set. Always verify under this flag.") return 1 root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".") hits = find_prod_units(root) if hits: print("Production ad unit IDs detected. Aborting self-debug:") for h in hits[:20]: print(" -", h) return 1 print("No production ad units found. Self-debug allowed.") return 0if __name__ == "__main__": sys.exit(main())
Wire this into the front of any workflow where you hand self-debug to the agent. Whether in an Antigravity task definition or a CI verification job, pinning the first step to "preflight must pass" means one forgotten flag can no longer let production ads run. The idea of aiming verification at a disposable preview is covered in pointing real-browser self-debug at a throwaway preview; think of these three layers as that same principle applied to one concrete side effect, ads.
What actually changed after adding the three layers
I measured two weeks before and two weeks after, on one wallpaper app whose flow wraps an ad. Self-debug run counts were kept roughly equal.
Metric
Before (2 weeks)
After (2 weeks)
Logged impressions attributable to self-debug
~480
0
Estimated stray clicks in the ad frame
3
0
Effect on production eCPM
Noise dropped it ~4% in one week
No measurable difference
Added verification time
—
~2 seconds per run (preflight)
The numbers look modest. But driving self-debug impressions to zero cannot be measured in revenue swings. The real gain is leaving no trace of suspicion on the account. On both the App Store and Google Play, account health is slow to rebuild once you lose it.
One thing surprised me. I had assumed the test-ID swap in layer one would be enough. Yet with a mediated interstitial, one adapter was still calling a production endpoint. Without the network block in layer two I would never have seen it. That experience convinced me the three layers are not redundant; each closes a different hole.
Wrapping up: start with preflight
You do not need all three layers at once. If you add just one today, I would recommend the layer-three preflight gate. Without touching the existing app, dropping one check in front of self-debug stops the worst accident: verifying against a production ad build. Forced test IDs and network blocking can then be stacked on, one at a time.
The more you hand to an agent, the more you owe it to yourself to know what runs on the other side of that handoff. I was lucky to catch the ads under my feet from this one incident. If it helps you pause at the same spot, I am glad. Thank you for reading.
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.