◉ANTIGRAVITY LABJP
Articles/Integrations
⬡ Integrations/2026-05-23Advanced

Antigravity × UMP/ATT Consent Rate Optimization Agent — A Weekly Autonomous Loop That Lifts AdMob Revenue Region by Region

ATT and Google UMP consent rates are hidden levers that move AdMob eCPM by 1.3–2.0x. This is a working memo on letting Antigravity sub-agents run weekly experiments on regional consent UX across six apps, and how that lifted ARPDAU.

antigravity461admob17umpattconsentmonetization31agents144

✦ Premium Article

Over the last couple of months I went back through the consent UX of all six apps I currently run, both the iOS ATT (App Tracking Transparency) flow and the Google UMP (User Messaging Platform) flow. The reason was almost embarrassingly simple: when I lined up my AdMob monthly report by country, a ten-point swing in consent rate was moving eCPM by 1.3x to 2.0x. The lever was right there in front of me.

I started building apps independently in 2014, my titles have crossed 50 million downloads cumulatively, and AdMob still pays the bills. When I first touched the internet in 1997 at sixteen, a stranger on a forum overseas read my code and shipped me back a fix — that small moment of "we can build across borders" still quietly shapes how I think about consent UX today. A consent prompt is not just a legal checkbox. It is the front door of a relationship with a user in a specific region, and if the door is rude, everything downstream — retention, ARPDAU, reviews — drags.

This article is a working memo on how I let Antigravity sub-agents run a weekly observe → hypothesize → implement → verify loop on consent UX, where I draw the line between autonomous and human-in-the-loop, and which mistakes I personally walked into. It is not a guide to integrating the SDKs or to writing the legal copy. It assumes you already have UMP and ATT in place and want to keep improving them without doing it ad hoc.

Why consent rate deserves to be a top-level AdMob KPI

When a user denies ATT, no IDFA is shared and AdMob serves NPA (non-personalized ads). When a UMP user in the EEA refuses, same outcome. NPA eCPM lands somewhere around 0.5x to 0.7x of personalized eCPM based on my own apps and the public benchmarks I've seen. Lifting consent from 60% to 80% in a given country roughly translates to a 10–15% lift in that country's ad revenue. In the country that improved the most for me, the monthly AdMob revenue came out about 12% higher.

The number itself is technically easy to measure. ATT exposes ATTrackingManager.trackingAuthorizationStatus and UMP exposes consentStatus on ConsentInformation; you just log the final state. The hard part is what to compare against and whether you keep comparing. I have twice run a one-off improvement, walked away pleased, and come back six months later to find the rate had slipped eight points because of an OS release or a region rollout. That is what pushed me to treat consent rate as a seasonally variable KPI and to put Antigravity in charge of watching it every week.

Step 1: event schema and BigQuery export

The first job is shaping events at a granularity that a sub-agent can actually use. If you skimp here, no amount of clever prompting downstream will save you. My apps emit the events below through Firebase Analytics and push them to BigQuery.

// Android (Kotlin) — log the settled UMP state
private fun logUmpStatus(status: Int, geo: String) {
    val statusLabel = when (status) {
        ConsentInformation.ConsentStatus.OBTAINED     -> "obtained"
        ConsentInformation.ConsentStatus.REQUIRED     -> "required"
        ConsentInformation.ConsentStatus.NOT_REQUIRED -> "not_required"
        ConsentInformation.ConsentStatus.UNKNOWN      -> "unknown"
        else -> "other"
    }
    firebaseAnalytics.logEvent("ump_status_settled") {
        param("status", statusLabel)
        param("geo", geo)                  // ISO-3166 alpha-2
        param("os_major", Build.VERSION.SDK_INT.toString())
        param("app_version", BuildConfig.VERSION_NAME)
        param("prompt_variant", currentVariantId)  // from Remote Config
    }
}
// iOS (Swift) — log the settled ATT state
import AppTrackingTransparency
import FirebaseAnalytics
 
func logAttStatus(geo: String) {
    let status = ATTrackingManager.trackingAuthorizationStatus
    let label: String
    switch status {
    case .authorized:    label = "authorized"
    case .denied:        label = "denied"
    case .restricted:    label = "restricted"
    case .notDetermined: label = "not_determined"
    @unknown default:    label = "other"
    }
    Analytics.logEvent("att_status_settled", parameters: [
        "status": label,
        "geo": geo,
        "os_major": String(ProcessInfo.processInfo.operatingSystemVersion.majorVersion),
        "app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "",
        "prompt_variant": currentVariantId
    ])
}

I personally prefer suffixing the event with _settled. A prompt_shown style event tends to drift away from the final outcome, and downstream aggregation gets messy. The geo field uses the region the SDK estimates, not Locale.current. More on why later — I once miscategorized travelers using device locale and it hurt.

On the BigQuery side I build a single normalized view. Sub-agents should never touch raw events; that way the event schema can change underneath without breaking the agent layer.

-- BigQuery: weekly consent rate aggregated by region × OS × variant
CREATE OR REPLACE VIEW analytics.consent_rate_weekly AS
SELECT
  DATE_TRUNC(DATE(event_timestamp), WEEK(MONDAY)) AS week_start,
  app_id,
  platform,                                    -- ios / android
  geo,
  os_major,
  prompt_variant,
  COUNTIF(event_name IN ('att_status_settled','ump_status_settled')) AS settled,
  COUNTIF(event_name = 'att_status_settled' AND status = 'authorized') AS att_yes,
  COUNTIF(event_name = 'att_status_settled' AND status = 'denied')     AS att_no,
  COUNTIF(event_name = 'ump_status_settled' AND status = 'obtained')   AS ump_yes,
  COUNTIF(event_name = 'ump_status_settled' AND status IN ('required','unknown')) AS ump_no
FROM analytics.events
WHERE _PARTITIONDATE >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
GROUP BY 1,2,3,4,5,6;
✦

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
✦Event schema for normalizing UMP/ATT consent state across region × OS into BigQuery, in a shape Antigravity sub-agents can actually reason over
✦How to split a weekly experiment loop into four sub-agents — observer, hypothesis-writer, variant-implementer, result-judge — with a single human-approval gate
✦Real production pitfalls: trying to re-prompt ATT, initializing AdMob before UMP, mis-detecting EU users by Locale, and missing the consent-withdrawal entry point in the EEA
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-03-27
Keeping the AdMob Waterfall While Adding Bidding: A Decision Record from Four Live Apps
How I added bidding on top of an existing AdMob waterfall across four live iOS apps: the ATT and init ordering, ad-gate design, and the metrics to watch.
⬡ Integrations2026-10-07
The Image Model Shuts Down on October 29 — Find Every Call in Your Agent Setup and Cut Over with a Way Back
The Gemini API deprecations table lists a shutdown date of October 29 for the image model gemini-3.1-flash-image. Here is how I locate every call scattered across configs and code, match them to a ledger, and fix a cutover date and a fallback in advance — with scripts that run.
⬡ Integrations2026-09-05
Pointing GOOGLE_GEMINI_BASE_URL at Your Own Gateway Does Not Change Who Holds the Key
I pointed the Antigravity CLI at a gateway of my own and recorded exactly which credentials travelled with it. Authorization is dropped across a cross-origin redirect, but the API key header survives every hop. Here is the measurement, and how I moved the key off my laptop.
📚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