play-integrity-service
FRAUD PREVENTION NestJS 10 Cloud Run Stateless HMAC

Gate fraudulent installs
with Play Integrity verdicts

NestJS backend service that verifies Google Play Integrity tokens before mobile attribution SDKs fire install events. Closes the gap between attribution-reported installs and real Play Store installs.

01

Problem

Attribution platform
High
reported installs
Play Store actuals
Lower
verified installs
Gap
fraud
SDK spoof · emulator · root

Mobile attribution SDKs often over-count installs relative to Play Store actuals — especially on incentive-heavy ad networks. The gap signals fraud: SDK spoofing, emulators, rooted devices, click injection. Each fraudulent install inflates marketing-spend attribution and wastes budget.

02

Goal & Non-goals

GOAL

Gate Adjust SDK install events with Google Play Integrity verdicts, reducing fraud volume and aligning Adjust install count closer to Play Store reality.

Target: gap ≤ 15% after 30 days
NON-GOALS
  • Catch click injection (bots on real devices) — Play Integrity has limited visibility
  • Replace Adjust Fraud Prevention Suite — this layers on top
  • Multi-app (phase 1 is LBP only; architecture is generic)
03

Architecture

Two-step flow: client requests a server-signed HMAC nonce, requests Google for an integrity token bound to that nonce, then sends both back. Backend verifies HMAC server-side first (cheap) before calling Google.

sequenceDiagram participant Game as Unity Game
(Android) participant BE as play-integrity-
service (NestJS) participant Google as Google Play
Integrity API Game->>BE: ① GET /api/nonce
(X-API-Key) BE-->>Game: {nonce, expiresAt} Game->>Game: ② IntegrityManager.
requestIntegrityToken(nonce) Game->>Google: ③ requestIntegrityToken Google-->>Game: integrityToken Game->>BE: ④ POST /api/verify
(token, nonce) BE->>Google: ⑤ decodeIntegrityToken Google-->>BE: {appIntegrity,
deviceIntegrity,
accountDetails} BE->>BE: ⑥ apply policy
(verdict=pass/fail) BE-->>Game: {verdict, reason,
decisionId} Game->>Game: ⑦ if verdict=pass:
init Adjust SDK
else: skip
Base path
/api/*
All business + probe routes prefixed with /api. Only /docs (Swagger UI) stays at root.
Endpoints
  • GET /api/nonce — issue HMAC nonce
  • POST /api/verify — verdict
  • GET /api/healthz — liveness
  • GET /api/readyz — readiness
  • GET /docs — Swagger UI
04

Authentication & Client Headers

REQUIRED X-API-Key header

Static secret in X-API-Key header. SHA-256 hashed both sides → crypto.timingSafeEqual (constant-time, resists timing attacks).

OPTIONAL · PASSIVE Client tracking headers Logged, never gate logic
Header Example Notes
X-Bundle-Idcom.iecgames.lbpShould match server PLAY_INTEGRITY_PACKAGE_NAME
X-Platformandroid / iosPhase 1: Android only
X-Version-Name1.4.2Semantic app version
X-Version-Code142Android versionCode / iOS build number
X-Os-VersionAndroid 14Free-form
X-Device-ModelPixel 8 ProBuild.MODEL on Android
X-Device-Localevi-VNBCP-47 locale tag
Cloud Logging query
jsonPayload.client.x-platform="android" AND jsonPayload.client.x-version-name="1.4.2"
05

Tech Stack

Component Choice Why
FrameworkNestJS 10 + TypeScriptTeam expertise, DI, Swagger auto-gen
DeploymentCloud Run (us-central1)SEA user base, ~150ms RTT savings vs us-central1
AuthWorkload Identity + ADCNo secret rotation, no JSON key leak risk
CacheStateless HMAC (no Redis)YAGNI — saves $80/mo + 1 IO hop
Loggingpino + Cloud LoggingJSON structured logs, native GCP integration
Static landing@nestjs/serve-staticInternal docs at / (env toggle)
ADR-006
Why no Redis?

Nonce re-use within 5-min TTL window is acceptable for fraud-gating — attacker still needs a valid one-shot integrity token from Google. Saves Memorystore + VPC connector overhead.

06

Success Metrics

Metric Baseline Target (30d)
Adjust vs Play Console gap~70%≤ 15%
/verify P99 latency< 800ms
Legit pass rate≥ 95%
Fail-open events (BE down)< 0.5%
MTG cost-per-installinflatedreflects fraud removal