Reverse-Engineering Dossier · Dating & Ad-Tech

The Hidden Passengers

Inside Grindr's Android client — thirteen advertising companies, a 129-field telemetry pipeline, and the behavioural score it prints on your profile.

109 MBAPK analysed
37Permissions requested
13Ad & tracking SDKs
129Telemetry fields
59,859Files decompiled

Grindr is the world's largest dating app for gay, bi, trans and queer people, and it asks for 37 permissions to do it. This report documents a full static reverse-engineering of the Android client, build 26.22.0, and two findings follow. The first is that Grindr is not one application — it ships at least thirteen separate advertising and tracking companies, one of which runs a continuous telemetry pipeline that samples 129 distinct fields about the device, from battery level and screen brightness to USB connection state and raw accelerometer readings. The second is that Grindr scores its users: it computes response rates, initiation rates and habitual chat patterns, then labels other people's profiles with predictions such as "Reply likely" and sells the fuller picture behind a subscription.

IThe chain of custody

Every claim below is traceable to a specific artefact and a reproducible step. The client was acquired from APKMirror as a signed, unmodified distribution, fingerprinted before extraction, and decompiled through a deliberately segmented pipeline — a full-APK run exceeded the memory available on the analysis host, so the ten DEX files were batched instead.

Artefact  : grindr-26.22.0.apkm
Size      : 108,988,089 bytes
SHA-256   : 223123d0f6d5eabbd37776d095b32ab03bd5782cb8c2bc4db4f13f51dd7e6e7b
            (matches APKMirror's published hash for this release)

Extracted : bundle/base.apk  ·  109,282,803 bytes
SHA-256   : 007bb88b59e5844f9adeec8fb801c664e5bd558b7917f41576e7afdaf8bcbd60
Cert SHA-256 : 6e21a5f966a7252628cd8f5982d93086a0fcf76c09edb9d7731964bb1c842897
Package   : com.grindrapp.android
Version   : 26.22.0 (versionCode 192710) · arm64-v8a · min API 29

Pipeline  : jadx 1.5.6 on Temurin JDK 21.0.12.1
            10 DEX files, 69,162,012 bytes of bytecode
            batched 3 ways + 1 resource-only pass
            output 59,859 .java files, 0 empty
            decompilation errors: 960 classes (2.5%)
Fig. 1 — Provenance. The published APKMirror hash was verified against the downloaded file before extraction, so the analysed artefact is provably the distributed one. The three-way DEX split is a memory constraint of the analysis host, not a property of the sample.

IIThirteen passengers

Counting third-party code inside a binary is a blunt instrument, but file counts say something a package name alone does: how much of the application you are actually running belongs to somebody else. Grindr's own first-party code accounts for 2,372 files. Google's does not merely match it — it exceeds it.

CompanyFilesRole
Google3,602Ads, Firebase analytics, Maps, Play
Mbridge1,813Ad network
InMobi1,674Ad network
Fyber1,563Ad exchange (Inneractive)
Unity / ironSource940 + 305Ads — owner of the telemetry pipeline in §III
AppLovin749Ad network
Vungle610Video advertising
PubMatic341Ad exchange
AppsFlyer318Install attribution
Braze292Marketing email and push
Facebook276Login and advertising
Explorestack251OpenRTB / AdCOM bidding protocol
Digital Turbine2Ignite app-install delivery
Fig. 2 — Advertising and tracking SDKs by decompiled file count. Grindr's own code (com.grindrapp) totals 2,372 files. Alongside these sit the IAB standards body (813 files), plus smaller modules for fraud (Sift), consent (Ketch), biometric face verification (FaceTec), content moderation (SafetyKit) and anti-tamper (PairIP).

The endpoint sweep corroborates the inventory rather than relying on it: googleads.g.doubleclick.net, pagead2.googlesyndication.com, ads.pubmatic.com, telemetry.sdk.inmobi.com, cdn.pubnative.net, cdn2.inner-active.mobi, cdn.doubleverify.com, ms.applovin.com, gateway.unityads.unity3d.com, app-measurement.com, www.braze.com, dev.facetec.com. These are hard-coded in the decompiled sources, not inferred from behaviour.

IIIThe telemetry pipeline

The most substantial third-party payload belongs to Unity. Buried in the sources is a package named gatewayprotocol — 305 generated schema files across two versioned namespaces, p025v1 and p036v1. It is not Grindr's own format. Two hundred and three Unity files feed it, and it posts to a single hard-coded address.

Endpoint constants · UnityAdsConstants.java, ServiceProvider.java
public static final String GATEWAY_URL = "https://gateway.unityads.unity3d.com/v1";
public static final String GATEWAY_HOST = "gateway.unityads.unity3d.com";

// and the HTTP client primes HTTP/3 for it:
builderCreateCronetEngineBuilder.addQuicHint("gateway.unityads.unity3d.com", 443, 443);

Two message types carry the device state. StaticDeviceInfo holds 64 fields that describe what the phone is; DynamicDeviceInfo holds 65 fields that describe what the phone is doing. Together, 129.

StaticDeviceInfo — 64 fieldsDynamicDeviceInfo — 65 fields
build fingerprint, board, bootloader, brand, device, display, hardware, host, id, product

cpu model and count · gpu model · webgl renderer and vendor

keyboard languages · sim operator · phone type

screen width, height, scale, density

rooted · simulator · adb-enabled app · app installer · signing-certificate hash

os version, api level, bundle id, version code

total disk, total RAM, JVM max memory, system boot time
battery level, battery status, charging type

volume, max volume, ringer mode · screen brightness

language, locale list, timezone and offset, orientation, UI theme

network type, operator, metered, RTT, downlink, effective type, interfaces

adb enabled · USB connected · SD card present · stay-awake-while-charging

animator scale · display inversion · grayscale · font scale · daltonizer

free RAM and disk, JVM free/total, uptime and elapsed realtime

accelerometer samples: x, y, z, timestamp
Fig. 3 — The 129 fields, split by message type. Bolded entries are the ones that matter: several are emulator and developer-mode detection signals, several are classic device-fingerprinting inputs, and the accelerometer block is raw sensor data.
StaticDeviceInfo DynamicDeviceInfo Pii (gated) Universal Request gateway.unityads .unity3d.com/v1 sessionId · token · make/model HTTPS · HTTP/3 on 443 one of 17 payload types
Fig. 4 — The pipeline, reconstructed from the data-source and repository classes. The envelope carries session identity and device make/model on every packet; the payload selects one of seventeen event types, beginning with the initialisation request.

The accelerometer is not merely declared. An AccelerometerRepository registers a real sensor listener, buffers readings against a configurable samplingIntervalMs and maxSamples, and drains them into the outgoing packet:

AndroidDynamicDeviceInfoDataSource.java · line 362
public DynamicDeviceInfoOuterClass.DynamicDeviceInfo fetch(boolean drainAccelerometerSamples) {
    ...
    if (drainAccelerometerSamples) {
        dsl_create.addAllAccelerometerSamples(
            dsl_create.getAccelerometerSamples(),
            this.accelerometerRepository.drainSamples());
    }

IVThe consent gate

In fairness to the implementation, there is a real gate, and it defaults to closed. Identifiers are attached one at a time, each behind its own flag, and the flags start out false.

AndroidPrivacyDeviceInfoDataSource.java · line 54
public PiiOuterClass.Pii fetch(AllowedPiiOuterClass.AllowedPii allowed) {
    allowed.getClass();
    initializeAdvertisingIds();
    ...
    if (allowed.getIdfa())     { dsl.setAdvertisingId(...);
                                 dsl.setOpenAdvertisingTrackingId(...); }
    if (allowed.getFid())      { dsl.setFid(...); }
    if (allowed.getAppsetId()) { dsl.setAppsetId(...); }
    return dsl._build();
}

// AndroidDeviceInfoRepository.java · line 56 — the default
this.allowedPii = AllowedPii.getDefaultInstance();   // every boolean = false
What is right here. On a fresh install the permitted-identifier set is empty, so no advertising ID, installation ID or app-set ID leaves the device until something turns it on. The gate is per-field, not a single blanket switch. This is a defensible design and the report would be dishonest not to say so.
What complicates it. The allowed set is not decided solely on the device. The gateway response writes it back: allowedPii = mutableData.getAllowedPii() in AndroidHandleGatewayUniversalResponse. Separately, setAllowedPii is exposed as a JavaScript-callable function to the advertising WebView, meaning code running inside an advertisement can alter which identifiers the app considers permissible.

That second point is the sharpest thing in this report, and it is the one that most needs stating precisely: the function's existence is proven by its decompiled definition, but whether Grindr's specific integration exposes it to untrusted ad creative was not established here. It should be confirmed with a live capture before it is asserted.

VWhat your profile is scored on

The second finding lives in a different corner of the codebase. The app contains an insights package with 29 files, and it explains itself in a dialog the user can open:

Profile Insights looks at a person's activity and behavior on the app to give you a clearer picture of what they're into. We then compare your profile details with their behavior to show how well you match up.

Read carefully, that is a description of the matching model: your profile is compared against their behaviour. The application then displays the result as a set of named scores and labels.

LabelSource stringMeaning
Response rate%1$d%% response rateHow often they answer
Initiation rate%1$d%% initiation rateHow often they start the chat
Reply likelyedge_insights_badge_reply_likelyPredicted to answer you
Responsiveedge_insights_badge_responsiveAnswers quickly
HabitOften chats with %1$sWho they habitually talk to
PredictionMost likely to interact with %1$sPredicted preference
IntentLooking for chats / dates / hookupsStated goal
Timinglast chatted %1$s · Recent activityRecency
Fig. 5 — The scorecard. Every row is a literal string resource in res/values/strings.xml, of which there are 66 edge_insights_* entries. Behaviour is additionally bucketed by time of day: Morning, Afternoon, Evening, Night — an enum in the compiled source.

The data model confirms this is computed and shipped, not derived locally. ProfileInsightsInference carries an insightKey, an inferenceType, a title, a value, an optional chart and an isRemovable flag. ProfileInsightsBehavior carries title, value, type, insightKey and timeOfDay.

Locked behind the subscription. The strings edge_insights_locked_age, edge_insights_locked_tribe, edge_insights_locked_position, edge_insights_locked_initiation_rate and edge_insights_locked_response_rate are all rendered with edge_insights_locked_state_description = "Locked" until the user buys Edge. Age, tribal label and sexual position are therefore presented as premium insight attributes about other people.
The opt-out is real. The app ships Insights Off, Insights not available for this profile for targets who have opted out, and a dedicated GaiFeaturesOptOutConfirmationDialogFragment with a "Keep insights unpersonalized" action. The pressure is soft rather than absent: To unlock the full value of Edge, turn on Personalize gAI™ Features.

VIThe feed is not just people

The browse grid is served as a single ordered list, and the response model says plainly that the server controls the order.

CascadeResponse — field names recovered from toString()
CascadeResponse(
    items,               // the mixed stream you are scrolling
    nextPage,            // cursor pagination
    shuffled,            // the server tells the client it reordered the list
    hiddenProfiles,      // locally hidden accounts
    hiddenProfilesInfo
)

No ordering logic exists in the client. Every compareTo call in Grindr's own code is an Android lifecycle comparison, not a profile sort. Ranking happens server-side and is not recoverable from the binary. What is recoverable is what the list is made of — and it is not homogeneous:

Feed item typeWhat it is
Profile / PartialProfileAn actual person
SponsoredProfileSomeone paid to appear here
SmartBoostProfileA boosted account
ForYou · ForYouNow · RightNowRecommendation modules
EdgeRecommendsSubscription-tier suggestions
Ad · MpuCardRewardedAd · AdsFreeSessionCardRewardedAdAdvertising units
FavoritesXtraUpsell · FavoritesHeaderUpgrade prompts
BrazeEventProfileA marketing card driven by the Braze CRM
HiddenProfileAccounts you have hidden
Fig. 6 — The CascadeItem sealed hierarchy, from cascade/data/model/. Advertising and upsell units are first-class members of the same list as people, which is why the grid cannot be understood as a pure directory of users.

VIIThe other passengers

Thirteen ad companies and a telemetry pipeline are the headline, but they are not the whole cargo. Seven further artefacts sit in this binary that a dating app has no obvious reason to carry, each of which changed how the analysis read. They are listed together because their common feature is that none of them is visible in the permission prompt.

A second SDK is larger than the ad stack's

Twenty-two native libraries ship in the arm64 split. Three of them belong to Agora, and the largest — libagora-rtc-sdk.so at 13,489,584 bytes — is bigger than every advertising native library in the package combined. Agora is a real-time voice and video engine, and its 347 decompiled Java files are wired to Grindr's own video-call screen: VideoCallActivity imports io.agora.rtc2.video.VideoCanvas directly.

lib/arm64-v8a/libagora-rtc-sdk.so            13,489,584   ← Grindr video calls
lib/arm64-v8a/libagora_ffmpeg.so                 804,784
lib/arm64-v8a/libagora-soundtouch.so              67,344
lib/arm64-v8a/libvideo_dec.so                   1,312,816   ← Unity ad decode
lib/arm64-v8a/libapplovin-native-crash-reporter.so 843,152
lib/arm64-v8a/libpairipcore.so                   608,896   ← anti-repackaging
lib/arm64-v8a/libunitycoherencenative.so         514,424   ← Unity game engine
22 files total, arm64-v8a only
Fig. 7 — Native libraries by size. The real-time communication engine outweighs the advertising ones. This is the code behind RECORD_AUDIO and CAMERA, and it is a product feature rather than a tracker — which is worth saying plainly, because the rest of this section is not.

The ad WebView can make its own network requests

§IV established that code running inside an advertisement can read and set which identifiers are permitted. The exposed-function table that carries that hook carries considerably more:

ExposedFunctionLocation.java · bridge functions available to the ad WebView
SET_ALLOWED_PII   = "…AdViewer.setPrivacyAllowedPii";
SET_PRIVACY       = "…AdViewer.setPrivacyPayload";
SET_PRIVACY_FSM   = "…AdViewer.setPrivacyFsm";      // privacy state machine
REQUEST_GET       = "…Request.get";                 // arbitrary HTTP GET
REQUEST_POST      = "…Request.post";                // arbitrary HTTP POST
REQUEST_HEAD      = "…Request.head";
OPEN_URL          = "…AdViewer.openUrl";
CAPTURE_SCREENSHOT= "…AdViewer.captureScreenshot";
DOWNLOAD          = "…AdViewer.download";
GET_BLACK_PIXEL_PERCENTAGE = "…";                    // viewability probe

Two things follow. First, the privacy apparatus in §IV is not a one-way gate but a mutable state machine — SET_PRIVACY_FSM and SEND_PRIVACY_UPDATE_REQUEST are both callable from creative, which is what PrivacyFsm and OnPrivacyFsmChangeEvent in the ad-player package implement. Second, and more consequentially, an advertisement can issue GET and POST requests through the host application's own networking stack. The reachability caveat from §IV applies here too: the bridge is defined, and whether any given integration exposes it to untrusted creative was not established without a live capture.

The gateway can redirect where traffic goes

Unity's initialisation response carries a RequestUrlOverride message — a list of request types paired with replacement URLs. The client stores it on the session repository and consults it before every call:

AndroidRequestUrlFactory.java · lines 22–33
InitializationResponseOuterClass.RequestType t =
    getRequestUrlOverrideType(operationType);
if (t != null) {
    Iterator it = sessionRepository.getRequestUrlOverrides().iterator();
    do { } while (((RequestUrlOverride) next).getRequestType() != t);
    String url = requestUrlOverride != null ? requestUrlOverride.getUrl() : null;
}

The endpoint is nominally hard-coded (§III), but the server can replace it at runtime without the app updating. That is a normal operational feature for an ad SDK. It also means the hard-coded constant is not the final word on where the telemetry goes — an observation that belongs in any honest reading of §III.

The auction runs with your device and user context attached

BidMachine's request token payload is compiled into the binary as a protobuf descriptor, and its field list is readable directly from CacheProto. It binds the auction to a context object:

io/bidmachine/protobuf/CacheProto.java · RequestTokenPayload
device_data   → bidmachine.protobuf.sdk.context.Device
user_data     → bidmachine.protobuf.sdk.context.User
app_data      → bidmachine.protobuf.sdk.context.App
regs_data     → bidmachine.protobuf.sdk.context.Regs   // consent strings
session_data  → session_id, sessionduration
hb_placement  → headerbidding.HeaderBiddingPlacement
custom_data   → free-form key/value map
session_placement_data → impdepth, lastbundle, lastadomain,
                         clickrate, lastclick, completionrate

regs_data is the TCF consent string — so the regulatory signal travels with the bid. lastadomain and clickrate mean the exchange learns which advertisers you have previously seen and clicked, per session. The header-bidding side is equally explicit: HeaderBiddingPlacement and HeaderBiddingAd are compiled in as their own messages, with token counters exposed as bridge functions (HEADER_BIDDING_TOKEN_INCREMENT_WINS, …_INCREMENT_STARTS), so the client tracks its own auction win rate.

The click, the install and the receipt are all captured

Three attribution artefacts round out the picture:

The app guards itself against being modified

PairIP anti-repackaging ships as 17 Java files plus libpairipcore.so (608,896 bytes), alongside a devicesigning package. Separately, the manifest declares its own permission:

AndroidManifest.xml · lines 8–10
<permission android:name="com.grindrapp.android"
           android:protectionLevel="signature"/>
<uses-permission android:name="com.grindrapp.android"/>

A signature-level permission can only be held by an application signed with the same certificate as its definer — Grindr's own (cert SHA-256, §I). This is a legitimate inter-process-integration pattern and grants the app nothing from third parties; it is documented here because it is the one entry in the 37-permission count that is neither an Android platform permission nor obviously risky, and an earlier pass over the manifest misread it as an error.

What is defensible in this section. Agora is a real product feature, not surveillance. InstallReferrer is a platform API. The signature permission is correct usage. Listing them as artefacts is not the same as calling them abuses — and a report that blurred that line would be worth less than one that does not.

VIIIThe regulatory record

Static analysis establishes what an application contains. It does not establish what a company did with it. For that this report cross-checks against the public enforcement record, and the correspondence is close enough to be worth stating explicitly.

In December 2021 the Norwegian Data Protection Authority fined Grindr €6.5 million for disclosing user data to third parties for behavioural advertising without a legal basis — the largest fine the regulator had issued at the time. The regulator's own list of the data involved was GPS location, IP address, advertising ID, age, gender, and the fact that the user was on Grindr. Compare that with the schema in §III: advertising ID is a gated Pii field, location is requested through ACCESS_FINE_LOCATION, and IP, age and presence-on-platform are carried by the surrounding telemetry and session envelope.

What this section does not claim. The 2021 decision concerned the consent mechanism as it existed then, and the regulator explicitly noted it had not assessed the current mechanism. Nothing here should be read as evidence that build 26.22.0 is non-compliant. The finding is structural — the categories of data identified by the regulator are the same categories the current schema is built to carry.

What this actually means

For the reader who came here wondering what a dating app does with their phone: it is not one app, it is a host environment for thirteen advertising companies, and the most sophisticated of them maintains a 129-field model of your device that refreshes continuously and includes your motion sensor. Your personal identifiers are gated and gated off by default, which is more than several of its peers manage — but the gate is not entirely yours to control.

Separately, Grindr computes behavioural scores about its users, labels them with predictions, and shows those labels to other people. Some of that is opt-out. The fuller version, including age, tribe and position, is for sale.

And the thing that started most people's suspicion — why one profile appears above another — cannot be answered from the binary at all. The order arrives pre-computed and frequently shuffled. Anyone claiming otherwise is guessing.

What you can do

References

  1. BBC News — Grindr fined £5.5m for sharing data for ads, 15 December 2021. bbc.com
  2. The Guardian — Grindr fined £8.6m in Norway over sharing personal information, 26 January 2021. theguardian.com
  3. APKMirror — Grindr LLC, Grindr – Gay Dating & Chat release listing (source of the analysed artefact). apkmirror.com
  4. Unity Technologies — Unity Ads documentation (owner of the gatewayprotocol schema and the gateway.unityads.unity3d.com endpoint observed in the decompiled client). docs.unity3d.com

Primary technical evidence is from the analysed artefact grindr-26.22.0.apkm (SHA-256 above), its extracted base.apk, and the 59,859 Java files produced from it. Code blocks and field lists are transcribed from that decompilation. The regulatory figures were verified against the linked BBC report in October 2026. Several claims that circulate alongside this material — including a later appeal outcome and a separate fine against an advertising partner — were omitted because they could not be confirmed against a primary source in the time available.