Skip to content

RFD: Adding Play Context to Consumer Context Service

RFD: Adding Play Context to Consumer Context Service

Section titled “RFD: Adding Play Context to Consumer Context Service”

Author: Steve Hollinger Date: 2026-04-06 Status: Draft


This document proposes adding a Play domain to the Consumer Context Service (CCS), giving Rewards Assistant and other consumer-facing AI agents access to a user’s Fetch Play activity, progress, and actionable opportunities. Play is Fetch’s rewards-by-gaming feature powered by Adjoe, where users earn points by installing/playing external games and completing milestones, plus a daily word-guessing mini-game.


CCS currently aggregates context from 12+ upstream services across Products, Offers, and Users domains. Play is a major engagement surface with rich per-user data (game installs, milestone progress, streaks, promotions) that is not yet accessible to our AI agents. Adding Play context enables Rewards Assistant to:

  • Inform users about their Play progress and available opportunities
  • Surface high-value actions (“you’re 1 milestone away from completing Coin Master”)
  • Alert users to expiring rewards, streak risks, and active promotions
  • Provide a unified view of the user’s full Fetch experience

Users earn points two ways:

  1. Adjoe Milestones — Install external games, hit sequential/bonus milestones (e.g. “Reach Level 50”), earn points per milestone
  2. Daily Guess Mini-Game — Daily word guessing game with streaks, leaderboards, and point rewards
ServiceOwnerPurposeKey Data
gameplay-serviceRAPPS collectiveCore Play serviceLifetime points, game installs, milestone progress, promotions
mini-game-serviceRAPPS collectiveDaily Guess gameStreak, score, attempts histogram, leaderboard ranking
play-support-serviceRAPPS collectiveSupport toolingUser overview, per-app history, support award tracking

Supporting Services (not directly integrated)

Section titled “Supporting Services (not directly integrated)”
ServicePurpose
rewarded-apps-bffMobile BFF aggregator (proto/Connect RPC — not suitable for CCS)
quest-serviceQuests/challenges (“install 3 games this week”)
app-serviceApp metadata (icons, titles, descriptions)
play-promo-servicePromotion management
adjoe-s2s-serviceAdjoe backend communication (trending shelf)
fetch-play-user-profileIterable profile sync

Base URLs:

EnvironmentURL
Stagehttps://stage-gameplay-service.us-east-1.stage-services.fetchrewards.com
Preprodhttps://preprod-gameplay-service.us-east-1.preprod-services.fetchrewards.com
Prodhttps://prod-gameplay-service.us-east-1.prod-services.fetchrewards.com

Auth: userId header (Fetch user UUID)

User’s lifetime Play summary.

{
"totalPoints": "5000",
"numberOfGamesInstalled": 10,
"lastTransactionDate": "2025-02-15T14:30:00Z",
"lastInstallDate": "2025-02-10T10:00:00Z",
"isNewUser": false
}

Note: totalPoints is a string, not integer.

Per-app milestone progress. This is the richest source of in-game progress data.

{
"milestones": [
{
"appId": "com.moonactive.coinmaster",
"installId": "e958f4d8-7f63-4832-b343-0599bf5ed9c1",
"platform": "android",
"userId": "60a578602b9aa31fe73b7506",
"milestoneId": "91428c9f-a62e-425d-9320-a34d25a82992",
"description": {"en": "Level 1", "es": "Nivel 1"},
"isBonus": false,
"pointAmount": 98,
"status": "COMPLETED",
"installDate": 1736381630,
"pending": false,
"milestoneType": "SEQUENTIAL",
"availableDate": 1736384957,
"completedDate": 1736384957,
"expirationDate": null,
"playDuration": 82,
"index": 1,
"pointAwardedAmount": 98,
"timedCoins": 150,
"timedCoinsExpiration": 1736471357
}
]
}

Key fields:

  • status: COMPLETED, AVAILABLE, UNAVAILABLE, EXPIRED
  • milestoneType: SEQUENTIAL, BONUS, TIME_BOOSTED, IAP_SPEND, IAP_COUNT
  • description: Localized map — the human-readable “what to do” (e.g. “Complete Level 50”)
  • index: Sequential position in the milestone chain
  • Timestamps are Unix seconds (not ISO)

GET /recentActivity?pageSize=N&pageNumber=N

Section titled “GET /recentActivity?pageSize=N&pageNumber=N”

Paginated list of recently completed milestones. Same milestone schema as above, filtered to status: COMPLETED.

GET /nextUp?pageSize=N&pageNumber=N&platform=X

Section titled “GET /nextUp?pageSize=N&pageNumber=N&platform=X”

Available milestones to complete across all installed games. Same milestone schema, filtered to status: AVAILABLE.

GET /completedApps?pageSize=N&pageNumber=N

Section titled “GET /completedApps?pageSize=N&pageNumber=N”
{
"apps": [
{
"appId": "com.moonactive.coinmaster",
"installId": "a261f1c1-506b-4aa3-ba3e-5cf2169f7973",
"platform": "android",
"userId": "60a578602b9aa31fe73b7506",
"totalPoints": 5924,
"status": "COMPLETED",
"installDate": 1736381630,
"completedDate": 1741637294,
"allMilestonesCompleted": true
}
]
}

App status: COMPLETED, ACTIVE, EXPIRED, UNKNOWN

Same schema as completedApps, with status: EXPIRED.

Returns 200 with promotion or 204 No Content.

{
"startDate": 1740000000,
"endDate": 1742000000,
"multiplier": 2.5,
"promotionType": "FETCH_PLAY_PROMO",
"version": 1
}

Base URLs:

EnvironmentURL
Stagehttps://stage-mini-game-service.us-east-1.stage-services.fetchrewards.com
Prodhttps://prod-mini-game-service.us-east-1.prod-services.fetchrewards.com

Auth: userId in URL path

GET /api/v1/user/{userId}/game-stats?gameId=daily-guess

Section titled “GET /api/v1/user/{userId}/game-stats?gameId=daily-guess”
{
"userId": "67ed4e9b89ab15e9a432dc60",
"gameId": "daily-guess",
"clock": {
"serverNowUtc": "2026-03-03T21:48:20Z",
"gameTimezone": "America/Chicago",
"todayGameDay": "2026-03-03T00:00:00-06:00",
"nextGameDay": "2026-03-04T00:00:00-06:00"
},
"activity": {
"lastPlayedAtUtc": "2026-03-03T13:46:51Z",
"lastWinAtUtc": "2026-03-03T13:46:51Z",
"lastLossAtUtc": "2026-01-25T18:54:27Z"
},
"stats": {
"currentStreakDays": 31,
"maxStreakDays": 113,
"bestScore": 1,
"customStats": {
"attemptsHistogram": {
"0": 5, "1": 2, "2": 12, "3": 57, "4": 48, "5": 43, "6": 21
}
}
},
"ui": {
"suggestedEntryPoint": "CompletedSuccessfully",
"startedToday": true
},
"rewardAmount": 0
}

Key fields:

  • currentStreakDays / maxStreakDays — streak tracking
  • attemptsHistogram — distribution of guesses across all games
  • suggestedEntryPoint: StartStreak, ContinueStreak, CompletedSuccessfully, CompletedUnsuccessfully
  • startedToday — whether they’ve played today
  • All timestamps are RFC3339 UTC
  • Game day boundaries use America/Chicago timezone

GET /api/v1/user/{userId}/game-results?gameId=daily-guess&centralDate=2026-03-03

Section titled “GET /api/v1/user/{userId}/game-results?gameId=daily-guess&centralDate=2026-03-03”

Per-day result card.

{
"outcome": "Win",
"currentStreakDays": 31,
"performanceSummary": {
"args": [
{"type": "Int", "intValue": 5},
{"type": "Int", "intValue": 6},
{"type": "TimeDuration", "intValue": 132}
],
"primaryMetricArgs": [
{"type": "Int", "intValue": 5},
{"type": "Int", "intValue": 6}
],
"secondaryMetricArgs": [
{"type": "TimeDuration", "intValue": 132}
]
},
"ranking": {
"percentileRank": 58.91,
"betterThanUserIds": ["68d457923b09ee447ec5f85a"]
},
"statChips": [
{"key": "games_played", "value": {"type": "Int", "intValue": 188}},
{"key": "longest_streak", "value": {"type": "Int", "intValue": 113}},
{"key": "win_rate", "value": {"type": "Float", "floatValue": 97.34}}
],
"rewardAmount": 0,
"isOptimistic": false
}
[
{
"eventId": "E#daily-guess#2026-03-03#GAME_COMPLETED",
"gameId": "daily-guess",
"receivedAt": "2026-03-03T15:12:10Z",
"rewardAmount": 10
}
]

Base URLs:

EnvironmentURL
Stagehttps://stage-play-support-service.fetchrewards.com
Prodhttps://prod-play-support-service.fetchrewards.com

Auth: None required for GET endpoints.

Note: Uses legacy stage-{name}.fetchrewards.com URL pattern (no region).

GET /play-support-service/overview/{userId}

Section titled “GET /play-support-service/overview/{userId}”
{
"userId": "60a578602b9aa31fe73b7506",
"platform": "ios",
"totalPointsEarned": 5350,
"totalRetentionPointsEarned": 50,
"totalMissingMilestonePointsEarned": 4000,
"missingMilestoneSupportAwardedCount": 2,
"totalMissingGamePointsEarned": 1000,
"missingGameSupportAwardedCount": 1,
"totalManuallyCompletedPoints": 5000,
"manuallyCompletedCount": 3,
"apps": [
{
"appId": "com.moonactive.coinmaster",
"appName": "Monopoly Go",
"platform": "ios",
"userInstalled": true,
"userAppStatus": "In Progress"
}
]
}

App status values: Unconfirmed Install, Installed, In Progress, Complete, Closed - Support Awarded

GET /play-support-service/app-history/{userId}

Section titled “GET /play-support-service/app-history/{userId}”
[
{
"userId": "60a578602b9aa31fe73b7506",
"appId": "com.moonactive.coinmaster",
"appName": "Monopoly Go",
"platform": "ios",
"userAppStatus": "In Progress",
"lastActivityTimeInSeconds": 1705942562,
"lastMilestoneActivityTimeInSeconds": 1705942562,
"totalPointsEarned": 150,
"inAppMilestonesCompletedCount": 2,
"totalMissingMilestonePointsEarned": 0,
"missingMilestoneSupportAwardedCount": 0,
"totalMissingGamePointsEarned": 0,
"missingGameSupportAwardedCount": 0,
"installDateTimeInSeconds": 1705942558
}
]

GET /play-support-service/app-details/{userId}/{appId}

Section titled “GET /play-support-service/app-details/{userId}/{appId}”
{
"userId": "60a578602b9aa31fe73b7506",
"appId": "com.moonactive.coinmaster",
"appName": "Monopoly Go",
"platform": "ios",
"userAppStatus": "In Progress",
"installTimeInSeconds": 0,
"totalPointsAwarded": 100,
"numberMilestonesCompleted": 1,
"activityRecords": [
{
"userId": "60a578602b9aa31fe73b7506",
"appId": "com.moonactive.coinmaster",
"appName": "Monopoly Go",
"platform": "ios",
"numberPointsAwarded": 100,
"pointAwardedReason": "",
"awardType": "in_app",
"eventTimestampInSeconds": 1605942569,
"manuallyCompleted": false,
"numberMilestonesAwarded": 1
}
],
"missingMilestones": null
}

These are what Adjoe sends TO gameplay-service. CCS does not consume these directly, but they’re useful context for understanding data provenance.

All query params:

?user_uuid=534ef82ee4b0644f1c5e3b3c
&sid=bf6e0d3c
&coin_amount=37
&trans_uuid=b6574ccc-7213-4900-8e48-252bc98b6baf
&app_name=Block%20Blast
&app_id=com.block.juggle
&milestone_name=level_4000_score
&milestone_description=Reach%204000%20Score
&event_type=sequential
&reward_type=Playtime
&install_uuid=dad2cf67-c420-487a-80e0-4b87947772c5
&event_boost_factor=1.5
&timed_coins_amount=50
&timed_coins_event_duration=60
&promotion_tag=summer_2024
{
"UserID": "534ef82ee4b0644f1c5e3b3c",
"AppID": "6449094229",
"AppName": "Match Factory!",
"Platform": "ios",
"InstallUUID": "dad2cf67-c420-487a-80e0-4b87947772c5",
"AttributedAt": "2024-11-10T02:27:44.861Z",
"OfferExpiresAt": "2024-12-10T02:27:44.861Z",
"RewardType": "Advance+",
"TotalRewardAmount": 28586,
"RewardMilestones": [
{
"Name": "level_7_completed",
"Description": "Complete Level 7",
"Amount": 131,
"PlayDuration": 5,
"TaskType": "SEQUENTIAL"
},
{
"Name": "purchase",
"Description": "Make Your First Purchase!",
"Amount": 1088,
"TaskType": "BONUS"
}
],
"CashbackConfig": {
"exchangeRate": 0.5,
"maxLimitPerCampaignCoins": 50000,
"maxLimitPerCampaignUSD": 100
},
"EventBoostFactor": 1.5,
"AppCategory": "gaming",
"Placement": "feed_cta"
}

Beyond the REST APIs above, Play produces rich event streams relevant for real-time personalization and graph-based intelligence.

The primary event bus for Play, with two event types filtered by SNS subscriptions:

POINTS_AWARDED — emitted when a user completes a game milestone:

FieldTypeNotes
UserIdstringRequired
AppNamestringGame name
AppIdstringAdjoe app identifier
AppCategorystringGame category
PlatformstringiOS / Android
PointsintPoints earned
MilestoneNamestringe.g. “Reach Level 10”
MilestoneNumber*intOrdinal position
RewardLevel*stringReward tier
RewardType*stringReward classification
IsBonusMilestone*boolWhether bonus milestone
IsFirstTransactionboolFirst-ever Play transaction
EventBoostFactor*float32Boost multiplier
Timestamptime.TimeEvent time

APP_INSTALL — emitted when a user installs a game via Adjoe webhook:

FieldTypeNotes
UserIdstring
AppIdstring
AppNamestring
PlatformstringiOS / Android
AppCategorystringGame category
CPIinterface{}Cost per install
Timestamptime.Time

CDC Topic: gameplay-service-gameplay-db-cdc

Section titled “CDC Topic: gameplay-service-gameplay-db-cdc”

DynamoDB change data capture streaming gameplay user state via Debezium. Used by the Sequin ML pipeline.

Mobile Events (WebSocket Service → Snowflake)

Section titled “Mobile Events (WebSocket Service → Snowflake)”

Table: FETCH_SERVICES_PROD.WEBSOCKET_SERVICE.WS_FETCH_PLAY_STAGE

Mini-game lifecycle events:

  • mini_game_section_impression — entry point visible
  • mini_game_section_tapped — entry point card tapped
  • fetch_play_mini_game_started — game starts after webview loads
  • fetch_play_mini_game_completed — game finished
  • fetch_play_mini_game_results_impression — results screen displayed
  • fetch_play_mini_game_results_share_tapped — share button tapped
  • mini_game_share_completed — share action successful

Funnel events:

  • fetch_play_landing_viewed — user navigates to Play tab
  • fetch_play_get_started_tapped — user taps Get Started
  • fetch_play_play_now_tapped — user taps Play Now
  • fetch_play_receipt_cta_viewed — user views receipt detail CTA
  • fetch_play_receipt_cta_tapped — user taps receipt detail CTA
QueueFiltersPurpose
gameplay-push-notification-queuePOINTS_AWARDEDPush notifications for points
battlepass-service-play-events-queuePOINTS_AWARDEDBattle pass progression
recommendations-play-engagementRecs orchestrator input
High-point-amounts queuePOINTS_AWARDEDHigh-value event tracking
Referral completion queuePOINTS_AWARDEDReferral program triggers
ConsumerData IngestedStatus
recommendations-orchestratorPlay engagement via SQSMerged (March 2026)
recommendations-input-listenerPlayEngagementKafkaListener → SNS → FaberLive
Faber (recommendations-proxy-service)Flat feature vectors for personalizationLive
Sequin (3rd-party ML)CDC pipeline via Debezium → IcebergIn progress (RF-1132)
battlepass-servicePOINTS_AWARDED eventsLive
TablePurpose
GAMEPLAY_TRANSACTION_STAGEPoints earned with milestone data
GAMEPLAY_INSTALL_STAGEGame install data from Adjoe
GAMEPLAY_USER_STAGEPlayer lifetime earning points
GAMEPLAY_MILESTONES_STAGEFlattened milestone information
WS_FETCH_PLAY_STAGEMobile engagement events

There are two possible approaches for CCS:

  1. REST polling (simpler) — Call gameplay-service/mini-game-service/play-support-service REST APIs on demand. Simpler to implement, matches current CCS pattern, but data is only as fresh as the request.

  2. Event-driven (richer) — Subscribe to {env}-gameplay-service-events SNS topic and maintain local state. Enables real-time awareness of Play activity (e.g. “you just earned 98 points!”), but adds complexity (SQS consumer, local storage, state management). The consumer-graph-worker already follows this pattern for Neo4j.

Direction TBD — depends on whether Rewards Assistant needs real-time Play awareness or if request-time freshness is sufficient.


Architecture Decision: Direct Service Calls (Not BFF)

Section titled “Architecture Decision: Direct Service Calls (Not BFF)”

The rewarded-apps-bff aggregates Play data for mobile clients using proto/Connect RPC with complex dependency graphs. For CCS, we should call downstream services directly because:

  1. BFF is designed for mobile UI rendering, not AI context
  2. BFF adds unnecessary aggregation overhead and an extra network hop
  3. Direct calls match our existing pattern (all other CCS clients are direct)
  4. We need raw data to compose into agent-friendly context, not pre-rendered UI sections

Following the existing pattern (products/, offers/, users/):

internal/
play/
types.go # PlayContext, GameProgress, MiniGameStats, PlayOpportunity
service.go # GetPlayContext(userId) orchestrating parallel calls
clients/
gameplay.go # gameplay-service client
minigame.go # mini-game-service client
playsupport.go # play-support-service client
type PlayContext struct {
User PlayUser // Lifetime summary (from gameplay-service)
Overview *PlayOverview // Aggregated overview (from play-support-service)
AppHistory []AppHistory // Per-app history with status (from play-support-service)
Games []GameProgress // Per-game milestone progress (from gameplay-service)
MiniGame *MiniGameStats // Daily Guess stats (nil if never played)
Promotion *ActivePromotion // Current promo (nil if none)
Opportunities []PlayOpportunity // Ranked actionable opportunities
}
ClientServiceEndpoints UsedValue
GameplayClientgameplay-serviceGET /user, GET /nextUp, GET /completedApps, GET /expiredApps, GET /milestones/{appId}, GET /currentPromotionReal-time milestone progress, available actions, promotions
MiniGameClientmini-game-serviceGET /api/v1/user/{userId}/game-stats, GET /api/v1/user/{userId}/completed-gamesStreak tracking, daily guess stats
PlaySupportClientplay-support-serviceGET /overview/{userId}, GET /app-history/{userId}, GET /app-details/{userId}/{appId}Pre-aggregated summaries, app names, support history

play-support-service is an event-sourced materialized view of gameplay-service data. It consumes POINTS_AWARDED and APP_INSTALL events from gameplay-service via SNS/SQS and stores them in DynamoDB (partitioned by userId, sorted by event date). All reads query DynamoDB and aggregate in-memory.

While it overlaps with gameplay-service, it provides three things gameplay-service does not:

  1. Pre-aggregated per-app summariesGET /app-history/{userId} returns total points, milestone counts, install dates, and activity timestamps per app in a single call. Without this, we’d need to paginate through gameplay-service milestones and aggregate ourselves.

  2. App names — gameplay-service returns only appId (e.g. com.moonactive.coinmaster). play-support-service stores and returns appName (e.g. “Coin Master”) because it captures the name from gameplay events. This avoids needing a separate app-service integration.

  3. Support award history — Tracks manual point awards with zendesk ticket IDs, award reasons (“OTE Missing Milestone”, “OTE Missing Game”), and who awarded them. Useful for Rewards Assistant to understand if a user has had support interactions around Play.

Data ingestion:
gameplay-service → SNS (gameplay-service-events)
→ SQS (play-support-in-app-event-queue)
→ 4 SQS worker goroutines → facade.ProcessEvent() → DynamoDB
DynamoDB schema:
Table: {env}-play-support-events
PK: userId
SK: {eventDateUnixSeconds}|{transactionId} (newest first)
Each record: appId, appName, platform, pointsAwarded, awardType (in-app|support),
milestoneName, milestoneDescription, supportAwardedReason,
zendeskTicketId, manuallyCompleted, rewardType, eventBoostFactor
Read path:
All GET endpoints → db.GetAll(userId) → in-memory aggregation → response

play-support-service derives app status from event history:

Unconfirmed Install → Installed (install event received)
→ In Progress (in-app points earned)
→ Closed - Support Awarded (manually completed)
→ Complete (all milestones done)

Complete and Closed - Support Awarded are final states.

CCS Use Caseplay-support-service EndpointWhy Not gameplay-service
”What games has this user played?”GET /app-history/{userId}Returns app names + aggregated stats in one call
”User overview for Play”GET /overview/{userId}Pre-computed totals across all apps
”What happened with this specific game?”GET /app-details/{userId}/{appId}Full activity log with milestone descriptions
”Has this user had support issues?”GET /overview/{userId}Support award counts, manual completion tracking
  • No auth on GETs — all read endpoints are unauthenticated
  • Legacy URL patternstage-play-support-service.fetchrewards.com (no region subdomain)
  • Timestamps are Unix secondslastActivityTimeInSeconds, installDateTimeInSeconds, eventTimestampInSeconds
  • Platform derived from appId — contains ”.” = Android, all numeric = iOS, else unknown
  • Event-sourced — data is eventually consistent with gameplay-service (SNS/SQS lag)

The richest value-add for Rewards Assistant is deriving actionable opportunities from raw Play data. Each opportunity has a type, description, point value, and urgency signal.

TypeSource DataSignalExample
Available MilestoneGET /nextUpstatus: AVAILABLE, pointAmount”Complete Level 50 in Match Factory for 240 points”
Expiring Timed Coinsmilestones with timedCoins > 0timedCoinsExpiration approaching”150 bonus coins on Coin Master expire in 6 hours”
Active PromotionGET /currentPromotionmultiplier > 1”2.5x points boost running through Jan 15”
Almost-Complete GamecompletedApps + milestonesFew milestones remaining vs total”5,200 of 5,924 points earned in Coin Master — 2 milestones left”
Expiring GameexpiredAppsexpirationDate approaching”Match Factory rewards expire in 4 days — 1,500 points unclaimed”
Streak at Riskgame-statsstartedToday: false && currentStreakDays > 0”31-day Daily Guess streak — don’t forget to play today”
Daily Guess Availablegame-statssuggestedEntryPoint: StartStreak/ContinueStreak”Today’s Daily Guess is ready”
Cashback Offermilestones with cashbackConfigmilestoneType: IAP_SPEND/IAP_COUNT”Make a purchase in Match Factory for up to $10 back in points”
Repeatable MilestonemilestonesrepetitiveRemainingCount > 0”Earn points 3 more times from in-app purchases”

Opportunities can be scored using:

SignalSourceWeight Rationale
pointAmount × multipliermilestone + promotionRaw reward value
expirationDate proximitymilestones/appsUrgency — value lost if not acted on
completedMilestones / totalMilestonesper-app dataCompletion proximity — high sunk cost
playDurationmilestonesInvestment already made
currentStreakDaysgame-statsStreak loss risk — high emotional value
timedCoinsExpiration proximitymilestonesTime-sensitive bonus

AspectDetail
Timestamp inconsistencygameplay-service uses Unix seconds for milestones but ISO strings for /user. mini-game-service uses RFC3339 everywhere. play-support-service uses Unix seconds.
totalPoints is a stringgameplay-service /user returns totalPoints as string, not int. Must parse.
Game day timezonemini-game-service uses America/Chicago for day boundaries, streaks, and event IDs.
204 for no promotionGET /currentPromotion returns 204 No Content (not an empty object) when no promo is active.
PaginationrecentActivity, nextUp, completedApps, expiredApps are all paginated (0-indexed pageNumber).
No auth on play-supportGET endpoints on play-support-service require no authentication.
URL patterns differgameplay-service and mini-game-service use stage-{name}.us-east-1.stage-services.fetchrewards.com. play-support-service uses legacy stage-{name}.fetchrewards.com.

  1. quest-service integration — Should we include quest/challenge progress? This is a separate system (Bitbucket repo, different team) that tracks Fetch-defined challenges like “install 3 games this week”. Adds complexity but rounds out the Play picture.

  2. App metadata enrichment — gameplay-service returns appId but not app names or icons. Should we integrate app-service for display metadata, or is the appId + milestone descriptions sufficient for agent context?

  3. Caching strategy — User Play data changes with game activity. What TTL is appropriate? Suggestion: 5 minutes for user/milestone data, 15 minutes for promotion, no cache for mini-game stats (streak accuracy matters).

  4. Platform parameterGET /nextUp accepts a platform param (ios/android). Do we know the user’s platform from other CCS context, or should we fetch both?

  5. Pagination limits — How many games/milestones should we fetch? Power users may have 10+ games. Suggestion: cap at pageSize=50 for nextUp/recentActivity, fetch all completedApps/expiredApps.