My general conclusion is: it's stupid that Apple continue to allow this and Trust Us Bro is not a good permission system to allow app developers do shady things, especially when research indicates that they are, unsurprisingly, doing shady things. Apple's static analysis based App Store approval system is also historically swiss-cheesey and I know they could do better. Overall, thematically a black mark on Apple, which has always been surprising for me because they tend to genuinely care about privacy in many other facets.
> 35F9.1
> Declare this reason to access the system boot time in order to measure the amount of time that has elapsed between events that occurred within the app or to perform calculations to enable timers.
> Information accessed for this reason, or any derived information, may not be sent off-device. There is an exception for information about the amount of time that has elapsed between events that occurred within the app, which may be sent off-device.
> 8FFB.1
> Declare this reason to access the system boot time to calculate absolute timestamps for events that occurred within your app, such as events related to the UIKit or AVFAudio frameworks.
> Absolute timestamps for events that occurred within your app may be sent off-device. System boot time accessed for this reason, or any other information derived from system boot time, may not be sent off-device.
15 May 2026
I broke the cipher AppLovin wraps around its ad-mediation traffic and decrypted several thousand real requests captured on my consented mobile-traffic research panel. The conclusion is straightforward: The encrypted bid request carries enough device data to deterministically re-identify the same iPhone across apps from different publishers, even when user denies ATT. That payload reaches AppLovin plus around 12 downstream ad networks on every banner load, every ~30 seconds, for as long as the user is playing. The assumption that ATT is the only way to deterministically identify a user is wrong. Fingerprinting the device works just as well.
Every AppLovin mediation request is HTTPS POST sent to ms4.applovin.com/1.0/mediate. Inside the TLS layer, the payload is wrapped in a second cipher AppLovin built. After base64 decoding, the wire envelope is:
2:8a2387b7dbed018e5e485792eac2b56833ce8a3a:T7NreIR729giTKR-thJPcKeT6JXevACogl57SIFzwKp-1BASwpBT6v:<binary>
Three colon-separated fields then ciphertext:
2)Info.plist on iOS or AndroidManifest.xml on Android.The cipher takes two ingredients: a salt and that SDK key. The salt is a 32-byte constant baked into every AppLovin SDK binary, 21 meaningful bytes followed by 11 zero bytes. The bytes are identical across every IPA and APK I checked (Solitaire Associations Journey, Hypermarket3D, Ludo Star, Yik Yak on iOS; Hypermarket3D on Android). The 40-character protocol-id field on the wire is sha1(salt).hex().

The cipher:
salt = (universal 32-byte constant, baked into the SDK)
sdk_key = (per-publisher 86-char string, baked into the app bundle)
dk = SHA-256(salt || sdk_key[:32]) # 32-byte per-publisher derived key
protocol_id = SHA-1(salt).hex() # constant identifying the version
counter = System.currentTimeMillis() # 8-byte LE — wall clock at encrypt time
masked_ctr = counter ⊕ uint64(dk[0:8]) # what appears on the wire
for i in 0..N-1:
if i % 8 == 0:
x = (counter + i)
x = (x ⊕ (x >>> 33)) * 0xC2B2AE3D27D4EB4F
x = (x ⊕ (x >>> 29)) * 0x85EBCA77C2B2AE63
ks = x ⊕ (x >>> 32)
ciphertext[i] = plaintext[i] ⊕ ((ks >> ((i % 8) * 8)) & 0xFF) ⊕ dk[i % 32]
A few facts about this construction:
System.currentTimeMillis(). Every encrypted envelope on the wire leaks the device's wall-clock time at encryption, to the millisecond, before decryption: recover the masked counter, XOR with uint64(dk[0:8]), get the timestamp.The decrypted plaintext is gzip-compressed JSON with about thirty top-level keys. Two of them carry the privacy weight:
device_info — AppLovin's own copy of the device's fingerprint payload. ~50 fields.signal_data[] — an array of opaque tokens, one per demand-partner ad network installed in the publisher's app.A real device_info from one ATT-denied request:
| Field | Value | What it is |
|---|---|---|
revision |
iPhone14,3 |
Hardware model code (iPhone 13 Pro Max) |
os |
18.6.2 |
OS patch version |
tm |
5918212096 |
Total RAM in bytes (= 5.51 GB) |
ndx × ndy |
1284 × 2778 |
Native pixel screen dimensions |
kb |
en-US,es-ES |
Installed keyboards |
font |
UICTContentSizeCategoryXXXL |
Accessibility text size |
tz_offset |
-4 |
Timezone |
volume |
40 |
System audio volume |
mute_switch |
1 |
Physical mute switch position |
bt_ms_2 |
1770745989000 |
Device boot time (ms epoch) |
dnt / idfa |
true / 00000… |
ATT denied — IDFA zeroed |
idfv |
81E958C3-…-51DE7CE11819 |
Per-app-vendor stable id |
Plus another 35 fields: screen safe-area insets, free memory, carrier code, country code, locale, orientation, status bar height, monotonic clock, battery flags, secure-connection state. Effectively every system property iOS exposes to third-party code.
The user denied ATT. IDFA is zeroed. Everything else flows.
A typical publisher app has ~18 demand-partner SDKs compiled in: Meta, Google, Mintegral, Vungle, ironSource, Unity, InMobi, BidMachine, Fyber, Moloco, TikTok, Pangle, Chartboost, Verve, MobileFuse, Bigo, Yandex, plus AppLovin's own. When a banner needs filling, the AppLovin SDK calls each of those locally, and asks "prepare a bid signal." Each demand SDK independently constructs an opaque token containing whatever device data its publisher backend wants. The AppLovin SDK bundles them all into signal_data[] and ships the whole thing inside its encrypted envelope. AppLovin's server then forwards each token to that bidder's bid server via server-to-server OpenRTB.
The device makes one outgoing network call. The data reaches a dozen separate ad-tech companies.

In the request I'll quote throughout, twelve of the eighteen adapters returned bid signals, ranging from 29 bytes (Verve, probably just a fetch token) to 14.4 KB (Unity Ads). Of those twelve, four are themselves readable inside the AppLovin envelope; eight are encrypted to the destination bidder, opaque to AppLovin, only decodable on the recipient's bid server.
The four readable ones are the ones worth dwelling on. The InMobi bid signal — 36 URL-encoded key=value pairs, decoded in full:
d-devicemachinehw = iPhone17,5
os-v = 26.2.1
h-user-agent = Mozilla/5.0 (iPhone; CPU iPhone OS 18_7…)
d-language = en-US
d-localization = en_US
d-key-lang = ["en-US"]
d-device-screen-density = 3
d-device-screen-margins = {"right":0,"left":0,"bottom":34,"top":47}
d-media-volume = 15
d-drk-m = 1
d-bat-lev = 25
d-bat-sav = 0
d-bat-chrg = 0
d-av-disk = 6275 MB
d-tot-disk = 116837 MB
u-app-orientations = 1
u-appbid = com.hitappsgames.wordsolitaire
u-appdnm = Solitaire Associations: Journey
u-appver = 1.9.0
u-tracking-status = 3
u-age-restricted = 0
s-skan = -1
InMobi's token contains signals AppLovin's own device_info doesn't: available disk space in megabytes (6,275 — varies hour to hour, very high entropy), total disk space, battery level, charging state, dark-mode preference, the model-specific safe-area inset dimensions. The downstream bidder collects more device data than the mediator forwarding it.
BidMachine's signal contains, additionally: the IANA timezone string (America/New_York, more specific than the numeric offset), carrier code, the SKAdNetwork acceptance list, and a separate 36-character UUID that's BidMachine's own per-user identifier. They've built their own persistent cross-app key, stored in their SDK's UserDefaults, shipped alongside Apple's IDFV on every request.
Fyber's signal is the most conservative: User-Agent, bundle, model, OS, IDFA, IDFV, locale, plus a stack of internal A/B test flag names.
Across the four readable mini-envelopes, the device fingerprint reaches four ad-tech companies in four different schemas. The other eight ship comparable payloads to eight more companies, encrypted in ways we can't inspect from outside. The fingerprint fans out on every banner load.
api_did sentinelInside app_info there's an 18-character hex field called api_did. AppLovin's server assigns it on first SDK init — the SDK POSTs the full device_info + app_info to applovin.com/2.0/device once on fresh install, the server returns a device_id, the SDK caches it and echoes it as api_did on every subsequent request. Server-issued, persistent, designed to be cross-app.
Across six distinct physical iPhones I observed with ATT denied — different hardware revisions, different IDFVs, different apps from different publishers — 100% of envelopes had api_did starting with the same eight hex characters: 10badd1d. Read those bytes as ASCII: 0xBADD1D = BADDID = "Bad Device ID." It's a sentinel — AppLovin's server returns the same prefix for every ATT-denied caller, with a random trailing salt per app. For ATT-granted users I observed three different prefixes (1023c..., 10621..., 10ebf...) — one per IDFA, confirming that when ATT is granted, api_did is a deterministic transformation of the IDFA. When ATT is denied, the prefix carries no device-identifying information.
AppLovin's own server-issued cross-app identifier respects ATT cleanly. Credit where due. This is real and worth saying out loud.
api_did excludedapi_did is one field. IDFA is one more. The encrypted envelope contains ~48 additional device_info fields, plus 12 mini-envelopes each carrying its own copy of the fingerprint. ATT zeroes one identifier. It does not touch:
I built a SHA-256 fingerprint from nine of those fields — revision + os + tm + ndx + ndy + kb + font + locale + tz_offset — over the ten distinct physical iPhones in my decrypted corpus. Result: ten distinct fingerprints for ten distinct devices. 100% uniqueness in a panel that included multiple iPhones of the same hardware model.

For one ATT-denied user whose device appeared in three different apps from three different publishers, the fingerprint hash was identical across all three apps: 321d60c4d72ddf2a. Different bundle ids. Different IDFVs. Different SDK versions.
That is the privacy argument, made by direct construction: looking only at the device-info payload that flows in every encrypted mediation request, with IDFA zeroed and api_did excluded from consideration, the data is sufficient to deterministically re-identify the same physical iPhone across apps from different publishers, with ATT denied throughout. That payload reaches AppLovin and ~12 demand partners simultaneously, on every banner refresh, every ~30 seconds.
ATT is a control over the Apple-issued cross-app identifier (IDFA). It is honored on the wire, IDFA is genuinely zeroed when the user denies. AppLovin's own server-issued identifier (api_did) is honored too, the BADDID sentinel returns the same value to every denied user. Both of those controls operate at the identifier layer.
The protocol AppLovin and the twelve downstream ad networks chose to send each other doesn't operate at the identifier layer. It operates at the device-fingerprint layer, which iOS does not gate, which Apple has no mechanical control over, and which ATT does not touch. Every encrypted mediation request carries 50 device fields to AppLovin, then 18 different bid signals to 18 different ad networks, then by server-to-server fan-out to those bidders' own downstream DSPs. Each of those parties has its own device fingerprint, its own identifier, its own ability to relink the same physical iPhone across publishers and sessions.