Skip to content

ADR-0004 — Anti-cheat layer

ADR-0004 — Anti-cheat layer

Status: draft Date: 2026-05-18 Owner: tech-architect + qa-engineer (anti-cheat metrics) Related canon: D-007 (restrictive anti-cheat, $10k/year ops), D-009 (Firebase App Check)

Status note (2026-05-18): This ADR describes the production target per D-007/D-009. Phase 8b implementation uses the simplified test-phase scope per ADR-0006 — schema validation + impossible-burst-rate guard (>12 steps/sec) + day-in-future + hard-cap-50_000 only. No App Check, no TZ-jump check, no Layer 3 behavioural patterns, no manual review queue, no three-strikes ban. Mock-step apps may pass — accepted risk for the closed self-test. The full three-layer defense in depth described below is deferred to the production-migration phase, currently paused.

Context

D-007 calls for restrictive anti-cheat: mock-step rejection, GPS sanity, TZ-jump rejection, signed device attestation (Play Integrity + Apple DeviceCheck). The system-reviewer flagged that aggressive anti-cheat can also misfire on legitimate walkers (transit users with weak GPS, time-zone-crossing travelers, accessibility cases). This ADR defines the layer with a quarantine band between accept and deny so the system never silently denies a walker their earned steps.

Decision — defense in depth

Three layers, evaluated in order on every /step/ingest call. Each layer can REJECT, QUARANTINE, or PASS.

Layer 1 — platform attestation (binary)

Firebase App Check token attached to every request. Verified server-side via Firebase Admin SDK.

  • iOS: DeviceCheck via App Attest API. Apple-signed assertion that the device is genuine and the app binary is unmodified.
  • Android: Play Integrity API at the MEETS_DEVICE_INTEGRITY level. Google-signed assertion that the device is genuine, Play Store-installed app, no debugger attached.

Verdict:

  • Token invalid / expired → REJECT (HTTP 401). AttestationLog REJECTED.
  • Token shows UNEVALUATED integrity (e.g. dev build, sideloaded) → REJECT in production environment; PASS in staging.
  • Token shows MEETS_DEVICE_INTEGRITY but NOT MEETS_STRONG_INTEGRITY (rooted Android, jailbroken iOS) → QUARANTINE (provisional state held, manual review queue).
  • Token shows full integrity → PASS to Layer 2.

Layer 2 — physiological heuristics (per-request)

Run on the submitted day’s reported count.

CheckThresholdVerdict on violation
count <= MAX_STEPS_PER_DAY50_000REJECT
Burst rate: count / sampleSpan_seconds <= MAX_RATE × 3MAX_RATE = 4 steps/sec → 12 steps/sec ceilingREJECT
Gyro absence flag from devicemobile reports gyroSamplesObserved=true for ingestQUARANTINE if false
Timestamp clustering: over 80% of count falls in under 5% of sampleSpandensity ratioQUARANTINE
TZ jump >12h within 24h since prior submissioncompare to last StepLog.tzREJECT
day > tomorrow or day > 7 days pastday vs server clockREJECT
Cross-source double-count: another source bundle reported the same time window with overlapping countreconciliation stepQUARANTINE the duplicate, accept the first-by-bundleId

Burst-rate ceiling rationale: world-record marathon runner sustains ~3.5 steps/sec for 2 hours; a 3x tolerance (~12 steps/sec) gives generous headroom for sprint intervals and accounts for HealthKit/HC bucketing artifacts that can briefly inflate measured rates.

Layer 3 — behavioural patterns (per-walker, sliding window)

Run as a Cloud Tasks job triggered nightly per walker.

PatternTriggerAction
5+ Layer 2 REJECTS in 24hcounter on StepLogflag walker for manual review
Streak holder with sudden 10× daily increaseT2_30D walker submits day 31 with 10× prior 30-day averageQUARANTINE day, flag for review
Same source bundle, same hour, multiple submissions with non-monotonic countsreconciliation workerQUARANTINE duplicates
App Check MEETS_DEVICE_INTEGRITY ratio drops under 90% over 7 daysper-walker rolling statflag for review

GPS sanity — the privacy tradeoff (deferred to phase 11)

D-007 named GPS sanity. After analyzing the privacy posture (D-009 — no profiling, marketable as “privacy-first walking RPG”), GPS sanity is NOT in the closed-beta build.

Reasoning:

  • GPS would let us correlate claimed step count with actual displacement, which detects “phone strapped to dog/pendulum” mock setups.
  • But GPS is continuous location data, which is exactly what the privacy-first marketing promises NOT to collect.
  • The platform attestation (Layer 1) already kills mock-step apps. The remaining mock vector is mechanical (pendulum, dog collar), which is real but low-volume — qa-engineer estimates it would account for under 0.5% of cheat attempts based on PoE-community surveys.

Phase-11 revisit: if telemetry shows mechanical mock-step abuse >2% of attestation passes, mechanics-designer + tech-architect + game-director re-open the question. Possible mitigation: opt-in GPS verification for tournament/leaderboard contexts only, with explicit per-event consent. Never default-on.

Bands — accept / quarantine / deny

Critical UX principle: never silently deny earned steps. Three bands:

BandServer stateWalker UX
PASSStepLog ACCEPTED, Energy credited, streak advancedNormal play
QUARANTINEStepLog QUARANTINED, provisional Energy granted (badge shown), allocation slots held but flagged “pending validation”Walker sees “we’re validating your last walk — this may take up to 24h” toast. Allocations made under quarantine survive if review accepts; revert + refund + apology push if review rejects.
REJECTStepLog REJECTED, no Energy granted, AttestationLog reason loggedWalker sees explicit error message naming the cause: “We could not verify your device attestation. Please update WalkRPG or check your device security settings.” NEVER “you cheated”.

Quarantine is the safety net for false positives on transit users, jailbroken hobbyists who aren’t actually cheating, and timezone-crossing travelers.

Manual review queue

Quarantined walkers and pattern-flagged walkers land in a Cloud Tasks-backed review queue. Phase 11 builds the admin UI; closed-beta-1 uses a Cloud SQL view + manual SQL triage.

Review verdicts:

  • CLEAR → flip StepLog to ACCEPTED, run reconciliation, restore Energy and streak.
  • WARN → keep StepLog QUARANTINED, walker stays in heightened-scrutiny pool for 30 days.
  • STRIKE → flip StepLog to REJECTED, refund allocations, walker stays in scrutiny pool for 90 days.
  • BAN → terminal action. Walker account disabled, GDPR delete pipeline can still be invoked.

Three STRIKEs within 180 days → automatic BAN.

Operational cost (per D-007 $10k/year band)

ItemAnnual cost band
Play Integrity API quota (Band C — 500M req/yr)~$3_500
Apple DeviceCheck (free tier covers our volume)$0
Cloud Logging retention (forensic trail, 90 days)~$1_200
Cloud Armor managed rules (anti-DDoS + bot)~$1_500
Cloud Tasks (review queue)~$200
Manual review human-hours (~0.5 FTE-equivalent contractor, phase 11)~$3_500
Anti-cheat ops total~$9_900

Aligns with D-007 forecast. Band B (1k DAU) is roughly 1/10 of this; Band A (100 DAU) ~$200/year for the technology line and review is in-CEO.

Cloud Logging integration

Every layer writes a structured log entry to a dedicated anti-cheat log sink:

{
"walkerId": "<uuid>",
"day": "2026-05-18",
"layer": 1 | 2 | 3,
"verdict": "PASS" | "QUARANTINE" | "REJECT",
"reasons": ["<reason-code>", ...],
"attestationTokenId": "<short-hash>",
"ipCountry": "<iso2>",
"sourceBundleId": "...",
"ts": "..."
}

Logs are retained 90 days (forensic trail) then auto-deleted (GDPR posture). Logs DO NOT contain step counts or location — only the verdict + reason codes.

Consequences

  • The system is restrictive but not punitive. Quarantine gives a 24h safety window for false positives.
  • GPS sanity is explicitly deferred to preserve the privacy-first marketing line. Phase 11 revisit triggered by metrics.
  • Manual review queue requires a 0.5 FTE-equivalent at Band C — closed beta uses CEO-as-reviewer, scale up at launch.
  • Three-strikes ban is enforceable; appeal flow is a phase-11 UX item.
  • The anti-cheat layer is separable from gameplay: turning it off (e.g. for a hackathon mode) is a config flag flip, not a schema change.