Play Context Domain for Consumer Context Service
Play Context Domain for Consumer Context Service
Section titled “Play Context Domain for Consumer Context Service”1. Problem Statement
Section titled “1. Problem Statement”Rewards Assistant and other consumer-facing AI agents have no visibility into a user’s Fetch Play activity. Play is a major engagement surface where users earn points by installing/playing external games (Adjoe milestones) and a daily word-guessing mini-game (Daily Guess). Users cannot currently ask Rewards Assistant about their Play progress, available opportunities, streak status, or active promotions.
Without Play context, Rewards Assistant provides an incomplete picture of the user’s Fetch experience. Users miss actionable opportunities — expiring rewards, streak risks, available milestones worth hundreds of points, and active 2.5x promotions — because the AI agents simply don’t know about them.
CCS already aggregates 12+ upstream services across Products, Offers, and Users domains. Play is the most significant missing domain.
2. Background & Context
Section titled “2. Background & Context”2.1 Current State
Section titled “2.1 Current State”CCS has three established domain patterns:
- Products (
internal/products/) — enriches product data from FPS, FIDORA, Button, Retailer services - Offers (
internal/offers/) — offer lookup from Offer Guardian, Neli, LIDAR, Offer Search - Users (
internal/users/) — purchase history and repurchase candidates from Neo4j, Purchase History Service
Each domain follows the same pattern: types in types.go, service orchestration in service.go, client adapters in internal/clients/, thin REST handlers in internal/api/handler.go, and thin MCP tools in internal/mcp/tools.go.
Play data exists across three RAPPS-collective services but has never been integrated into CCS.
2.2 Key Decisions Already Made
Section titled “2.2 Key Decisions Already Made”-
Direct service calls, not BFF — The
rewarded-apps-bffaggregates Play data for mobile clients using proto/Connect RPC. CCS will call downstream services directly because the BFF adds unnecessary overhead and we need raw data for AI context, not pre-rendered UI sections. See RFD. -
play-support-service in scope — Despite data overlap with gameplay-service, play-support-service provides pre-aggregated app summaries, app names (not available from gameplay-service), and support award history. See RFD section “play-support-service: Why It’s In Scope”.
-
REST polling for initial implementation — Event-driven integration (SNS/Kafka) is deferred. REST polling matches the current CCS pattern and is sufficient for request-time freshness. Event-driven approach may be explored in a future spec.
2.3 Glossary
Section titled “2.3 Glossary”| Term | Definition |
|---|---|
| Adjoe | Third-party gaming rewards platform powering Fetch Play milestone games |
| Milestone | A progress marker in an external game (e.g. “Reach Level 50”) that awards points when completed |
| Daily Guess | Fetch’s daily word-guessing mini-game with streak tracking and leaderboards |
| Timed Coins | Bonus coins awarded for milestone completion that expire after a set duration |
| Sequential Milestone | Milestones that unlock in order (index 1, 2, 3…) |
| Bonus Milestone | One-time milestones (e.g. “Make Your First Purchase!”) |
| Opportunity | A derived, actionable item surfaced to the user (e.g. “your streak is at risk”) |
| RAPPS | Rewarded Apps — the Fetch collective that owns Play services |
3. Requirements
Section titled “3. Requirements”3.1 Functional Requirements
Section titled “3.1 Functional Requirements”- The system MUST retrieve a user’s Play lifetime summary (total points, games installed, last transaction date, last install date, new user flag) from gameplay-service.
- The system MUST retrieve per-game milestone progress including status (COMPLETED/AVAILABLE/UNAVAILABLE/EXPIRED), point amounts, descriptions, expiration dates, and milestone types.
- The system MUST retrieve available milestones across all installed games via the nextUp endpoint.
- The system MUST retrieve completed and expired app lists with per-app point totals.
- The system MUST retrieve the current active promotion (multiplier, date range) or indicate no promotion is active.
- The system MUST retrieve Daily Guess mini-game stats including current streak, max streak, best score, attempts histogram, and whether the user has played today.
- The system MUST retrieve recently completed mini-games with reward amounts.
- The system MUST retrieve the user’s Play overview from play-support-service including total points earned, app list with names and status, and support award counts.
- The system MUST retrieve per-app history from play-support-service including app names, total points, milestone counts, install dates, and activity timestamps.
- The system MUST derive and rank actionable opportunities from the raw Play data (see Section 4.4).
- The system SHOULD expose Play context via both REST API and MCP tool.
- The system MUST degrade gracefully when any upstream service is unavailable — return partial context with nil fields rather than failing the entire request.
- The system MUST call upstream services in parallel where possible to minimize latency.
- The system MAY cache promotion data with a longer TTL than user-specific data.
3.2 Non-Functional Requirements
Section titled “3.2 Non-Functional Requirements”- Play context retrieval MUST complete in < 2000ms at p95 (aggregating 3 upstream services).
- Individual upstream client calls MUST timeout at 1500ms.
- The system MUST handle users with 0 Play activity (new/non-Play users) without errors.
- Cached promotion data SHOULD have a TTL of 15 minutes.
- Cached user/milestone data SHOULD have a TTL of 5 minutes.
- Mini-game stats SHOULD NOT be cached (streak accuracy matters for opportunity detection).
- The system MUST NOT increase CCS memory usage by more than 50MB under steady-state load.
3.3 Acceptance Criteria
Section titled “3.3 Acceptance Criteria”- AC-1: Given a user with Play activity, when
GetPlayContext(userId)is called, then the response includes lifetime points, at least one game with milestone data, and mini-game stats. - AC-2: Given a user with no Play activity, when
GetPlayContext(userId)is called, then the response returns a valid PlayContext with zero points, empty games list, nil MiniGame, and no opportunities. - AC-3: Given a user with an active 31-day Daily Guess streak who has not played today, when opportunities are derived, then a “Streak at Risk” opportunity is included with the current streak count.
- AC-4: Given a user with available milestones worth 240 points and an active 2.5x promotion, when opportunities are derived, then an “Available Milestone” opportunity is included with effective value of 600 points.
- AC-5: Given a user with timed coins expiring within 24 hours, when opportunities are derived, then an “Expiring Timed Coins” opportunity is included.
- AC-6: Given a user with a game where 90%+ milestones are completed, when opportunities are derived, then an “Almost-Complete Game” opportunity is included.
- AC-7: Given gameplay-service is unavailable but mini-game-service and play-support-service respond, when
GetPlayContext(userId)is called, then the response includes MiniGame and Overview data with nil User and empty Games list. - AC-8: Given a REST request to
GET /play/{userId}, then the response is valid JSON matching the PlayContext schema. - AC-9: Given an MCP tool call for
get_play_contextwith a userId, then the response contains the same data as the REST endpoint. - AC-10: Given a user with games that have expiration dates approaching within 7 days and unclaimed milestones, when opportunities are derived, then an “Expiring Game Rewards” opportunity is included with the total unclaimed points.
- AC-11: Given a user with milestones containing
cashbackConfig, when opportunities are derived, then a “Cashback Offer” opportunity is included with the exchange rate and max limit. - AC-12: Given a user with
repetitiveRemainingCount > 0on a milestone, when opportunities are derived, then a “Repeatable Milestone” opportunity is included with the remaining count.
4. Solution Design
Section titled “4. Solution Design”4.1 Architecture
Section titled “4.1 Architecture” ┌─────────────────────┐ │ Rewards Assistant / AI Agent │ └──────────┬──────────┘ │ ┌───────────────┼───────────────┐ │ │ │ REST :8080 MCP :8081 (future gRPC) │ │ └───────┬───────┘ │ ┌────────▼────────┐ │ play.Service │ │ GetPlayContext() │ └────────┬────────┘ │ parallel calls ┌─────────────┼─────────────┐ │ │ │ ┌─────────▼───┐ ┌─────▼─────┐ ┌────▼──────────┐ │ Gameplay │ │ MiniGame │ │ PlaySupport │ │ Client │ │ Client │ │ Client │ │ │ │ │ │ │ │ GET /user │ │ GET /stats│ │ GET /overview │ │ GET /nextUp │ │ GET /games│ │ GET /history │ │ GET /apps │ │ │ │ │ │ GET /promo │ │ │ │ │ └─────────────┘ └───────────┘ └───────────────┘4.2 Data Model
Section titled “4.2 Data Model”type PlayContext struct { User *PlayUser `json:"user,omitempty"` Overview *PlayOverview `json:"overview,omitempty"` AppHistory []AppHistory `json:"appHistory,omitempty"` Games []GameProgress `json:"games,omitempty"` MiniGame *MiniGameStats `json:"miniGame,omitempty"` Promotion *ActivePromotion `json:"promotion,omitempty"` Opportunities []PlayOpportunity `json:"opportunities,omitempty"`}
type PlayUser struct { TotalPoints int `json:"totalPoints"` NumberOfGamesInstalled int `json:"numberOfGamesInstalled"` LastTransactionDate *time.Time `json:"lastTransactionDate,omitempty"` LastInstallDate *time.Time `json:"lastInstallDate,omitempty"` IsNewUser bool `json:"isNewUser"`}
type PlayOverview struct { TotalPointsEarned int `json:"totalPointsEarned"` Platform string `json:"platform,omitempty"` TotalMissingMilestonePointsEarned int `json:"totalMissingMilestonePointsEarned"` MissingMilestoneSupportAwardedCount int `json:"missingMilestoneSupportAwardedCount"` TotalManuallyCompletedPoints int `json:"totalManuallyCompletedPoints"` ManuallyCompletedCount int `json:"manuallyCompletedCount"` Apps []PlayApp `json:"apps,omitempty"`}
type PlayApp struct { AppID string `json:"appId"` AppName string `json:"appName"` Platform string `json:"platform"` UserInstalled bool `json:"userInstalled"` UserAppStatus string `json:"userAppStatus"`}
type AppHistory struct { AppID string `json:"appId"` AppName string `json:"appName"` Platform string `json:"platform"` UserAppStatus string `json:"userAppStatus"` TotalPointsEarned int `json:"totalPointsEarned"` InAppMilestonesCompletedCount int `json:"inAppMilestonesCompletedCount"` LastActivityTime int64 `json:"lastActivityTimeInSeconds"` InstallDate *int64 `json:"installDateTimeInSeconds,omitempty"`}
type GameProgress struct { AppID string `json:"appId"` AppName string `json:"appName,omitempty"` Platform string `json:"platform"` Status string `json:"status"` TotalPoints int `json:"totalPoints"` InstallDate int64 `json:"installDate"` CompletedDate *int64 `json:"completedDate,omitempty"` ExpirationDate *int64 `json:"expirationDate,omitempty"` AllMilestonesCompleted bool `json:"allMilestonesCompleted"` Milestones []Milestone `json:"milestones,omitempty"`}
type Milestone struct { MilestoneID string `json:"milestoneId"` Description map[string]string `json:"description"` PointAmount int `json:"pointAmount"` Status string `json:"status"` MilestoneType string `json:"milestoneType"` Index int `json:"index"` CompletedDate *int64 `json:"completedDate,omitempty"` ExpirationDate *int64 `json:"expirationDate,omitempty"` TimedCoins int `json:"timedCoins,omitempty"` TimedCoinsExp *int64 `json:"timedCoinsExpiration,omitempty"` CashbackConfig *CashbackConfig `json:"cashbackConfig,omitempty"` RepetitiveRemaining *int `json:"repetitiveRemainingCount,omitempty"`}
type CashbackConfig struct { ExchangeRate float64 `json:"exchangeRate"` MaxLimitPerCampaignUSD float64 `json:"maxLimitPerCampaignUSD"`}
type MiniGameStats struct { CurrentStreakDays int `json:"currentStreakDays"` MaxStreakDays int `json:"maxStreakDays"` BestScore int `json:"bestScore,omitempty"` AttemptsHistogram map[string]int `json:"attemptsHistogram,omitempty"` LastPlayedAt *time.Time `json:"lastPlayedAt,omitempty"` StartedToday bool `json:"startedToday"` SuggestedEntry string `json:"suggestedEntryPoint"` CompletedGames []CompletedGame `json:"completedGames,omitempty"`}
type CompletedGame struct { GameID string `json:"gameId"` ReceivedAt time.Time `json:"receivedAt"` RewardAmount int `json:"rewardAmount"`}
type ActivePromotion struct { StartDate int64 `json:"startDate"` EndDate int64 `json:"endDate"` Multiplier float64 `json:"multiplier"` PromotionType string `json:"promotionType"`}
type PlayOpportunity struct { Type string `json:"type"` Description string `json:"description"` PointsValue int `json:"pointsValue"` EffectiveValue int `json:"effectiveValue,omitempty"` Urgency string `json:"urgency,omitempty"` AppID string `json:"appId,omitempty"` AppName string `json:"appName,omitempty"` ExpiresAt *int64 `json:"expiresAt,omitempty"` Score float64 `json:"score"`}4.3 API Contracts
Section titled “4.3 API Contracts”REST: GET /play/{userId}
Section titled “REST: GET /play/{userId}”Response: 200 OK with PlayContext JSON body (schema above).
Error codes:
400— missing or invalid userId500— all upstream services failed
Returns 200 with partial data if some upstreams fail (nil fields for failed services).
MCP: get_play_context
Section titled “MCP: get_play_context”Input:
{ "userId": "60a578602b9aa31fe73b7506"}Output: Same PlayContext schema as REST.
4.4 Key Implementation Details
Section titled “4.4 Key Implementation Details”Opportunity Detection Algorithm
Section titled “Opportunity Detection Algorithm”Opportunities are derived after all upstream data is collected. Each opportunity type has a detection function and a scoring function:
| Type | Detection | Score Formula |
|---|---|---|
| Available Milestone | status == AVAILABLE | pointAmount * multiplier |
| Expiring Timed Coins | timedCoins > 0 && timedCoinsExpiration < now + 24h | timedCoins * (1 / hoursRemaining) |
| Active Promotion | multiplier > 1 | 1000 * multiplier (high base — affects all activity) |
| Almost-Complete Game | completedMilestones / totalMilestones > 0.8 | remainingPoints * (completionRatio ^ 2) |
| Expiring Game | expirationDate < now + 7d && hasUnclaimedMilestones | unclaimedPoints * (1 / daysRemaining) |
| Streak at Risk | !startedToday && currentStreakDays > 0 | currentStreakDays * 100 |
| Daily Guess Available | suggestedEntryPoint in [StartStreak, ContinueStreak] | 500 (fixed — daily engagement) |
| Cashback Offer | cashbackConfig != nil | maxLimitPerCampaignUSD * 100 |
| Repeatable Milestone | repetitiveRemainingCount > 0 | pointAmount * remainingCount |
Opportunities are sorted by score descending. Top 10 returned by default.
App Name Resolution
Section titled “App Name Resolution”gameplay-service returns appId only. play-support-service /app-history returns appName. The service MUST cross-reference app names from play-support-service data when building GameProgress entries. If play-support-service is unavailable, appName is omitted.
Parallel Call Strategy
Section titled “Parallel Call Strategy”GetPlayContext(userId): parallel: 1. gameplay-service GET /user 2. gameplay-service GET /nextUp?pageSize=50&pageNumber=0 3. gameplay-service GET /completedApps?pageSize=50&pageNumber=0 4. gameplay-service GET /expiredApps?pageSize=50&pageNumber=0 5. gameplay-service GET /currentPromotion 6. mini-game-service GET /game-stats?gameId=daily-guess 7. mini-game-service GET /completed-games 8. play-support-service GET /overview/{userId} 9. play-support-service GET /app-history/{userId} then: - Cross-reference app names from play-support-service into gameplay data - Derive opportunities from combined data - Sort opportunities by score - Return PlayContextTimestamp Normalization
Section titled “Timestamp Normalization”All timestamps in the PlayContext response use the format from the upstream source to avoid lossy conversions:
- gameplay-service milestone timestamps: Unix seconds (int64)
- gameplay-service
/userdates: ISO 8601 strings → parsed totime.Time - mini-game-service timestamps: RFC3339 strings → parsed to
time.Time - play-support-service timestamps: Unix seconds (int64)
gameplay-service totalPoints String Parsing
Section titled “gameplay-service totalPoints String Parsing”gameplay-service returns totalPoints as a string (e.g. "5000"). The client adapter MUST parse this to int using strconv.Atoi and handle parse errors gracefully (default to 0).
5. Dependencies
Section titled “5. Dependencies”5.1 Spec Dependencies
Section titled “5.1 Spec Dependencies”| Spec ID | What We Need From It | Why |
|---|---|---|
| (none) | First spec in the system | — |
5.2 External Dependencies
Section titled “5.2 External Dependencies”| Dependency | Owner | Status | Blocker? |
|---|---|---|---|
| gameplay-service REST API | RAPPS collective | Live (stage + prod) | No |
| mini-game-service REST API | RAPPS collective | Live (stage + prod) | No |
| play-support-service REST API | RAPPS collective | Live (stage + prod) | No |
| CCS staging deployment | Pilot team | Live | No |
| Network access from CCS ECS to Play services | Platform/SRE | Needs verification | Possible |
6. Risks & Open Questions
Section titled “6. Risks & Open Questions”| # | Risk / Question | Impact | Mitigation / Answer |
|---|---|---|---|
| 1 | Network connectivity — CCS may not have network access to Play service endpoints in stage/prod | Blocks all Play context | Verify VPC/security group access before implementation begins |
| 2 | play-support-service eventual consistency — data lags gameplay-service by SNS/SQS propagation | Stale app names or summaries | Acceptable for AI context; document in response metadata |
| 3 | Pagination limits — power users with 10+ games may exceed pageSize=50 | Missing games in context | Start with pageSize=50; monitor and increase if needed |
| 4 | Platform parameter for nextUp — we may not know user’s platform | Missing platform-specific milestones | Use play-support-service /overview platform field; fall back to omitting platform param |
| 5 | quest-service integration deferred | Incomplete Play picture for users with active quests | Acceptable for v1; add in future spec if needed |
| 6 | Opportunity ranking weights are heuristic | Suboptimal ordering | Start with proposed weights; tune based on Rewards Assistant feedback |
| 7 | Event-driven integration deferred | No real-time Play awareness | REST polling is sufficient for v1; revisit based on Rewards Assistant needs |
7. Testing Strategy
Section titled “7. Testing Strategy”-
Unit tests: Test each client adapter with
httptestmock servers returning realistic payloads from the RFD. Test opportunity detection logic for each of the 9 opportunity types. Test timestamp parsing and totalPoints string conversion. Test nil-safe degradation (each combination of failed upstream). -
Integration tests: Call staging endpoints with known test users to verify response shapes match expected schemas. Verify cross-referencing of app names between gameplay-service and play-support-service data.
-
Contract tests: Validate client adapter response parsing against actual upstream OpenAPI specs (
gameplay-service/api/openapi.yaml). Pin response schemas in test fixtures. -
Manual validation: Call
GET /play/{userId}on staging with known Play users. Verify opportunity detection produces sensible results. Test with non-Play user to confirm graceful empty response. Test MCP tool via Claude Desktop.
8. Rollout & Observability
Section titled “8. Rollout & Observability”8.1 Rollout Plan
Section titled “8.1 Rollout Plan”- Implement client adapters and Play domain service on feature branch
- Deploy to staging and verify with known Play test users
- Validate with Rewards Assistant team that context is useful and opportunity ranking is sensible
- Deploy to production behind feature flag if needed (or direct deploy given read-only nature)
8.2 Metrics & Alerts
Section titled “8.2 Metrics & Alerts”- Latency: p50/p95/p99 for
GetPlayContextand each upstream client call - Error rates: Per-upstream-service failure rate (5xx, timeout, connection refused)
- Degradation: Count of partial responses (at least one upstream failed but context returned)
- Cache hit rates: Promotion cache hits vs misses
- Opportunity counts: Average number of opportunities per user (track to ensure detection is working)
8.3 Rollback Plan
Section titled “8.3 Rollback Plan”Play context is additive — it adds a new domain without modifying existing Products/Offers/Users domains. Rollback is simply removing the /play/{userId} route and MCP tool registration. No data migrations to reverse.
9. Appendix
Section titled “9. Appendix”Upstream Service Base URLs
Section titled “Upstream Service Base URLs”| Service | Stage | Prod |
|---|---|---|
| gameplay-service | https://stage-gameplay-service.us-east-1.stage-services.fetchrewards.com | https://prod-gameplay-service.us-east-1.prod-services.fetchrewards.com |
| mini-game-service | https://stage-mini-game-service.us-east-1.stage-services.fetchrewards.com | https://prod-mini-game-service.us-east-1.prod-services.fetchrewards.com |
| play-support-service | https://stage-play-support-service.fetchrewards.com | https://prod-play-support-service.fetchrewards.com |
Research Documents
Section titled “Research Documents”- RFD: Adding Play Context to CCS — Full API contracts, payloads, architecture analysis
- Play Ecosystem Research — Service inventory, data models, integration notes
- Confluence: Play Context Research Findings — Team-facing summary with Kafka/SNS event surface
Event Streams (Future Reference)
Section titled “Event Streams (Future Reference)”SNS topic {env}-gameplay-service-events produces POINTS_AWARDED and APP_INSTALL events. Current consumers include recommendations-orchestrator, Faber, battlepass-service, and Sequin. CDC topic gameplay-service-gameplay-db-cdc streams DynamoDB state via Debezium. These are documented in the Confluence page and RFD for future event-driven integration consideration.
RAPPS Team Contacts
Section titled “RAPPS Team Contacts”- Slack:
#collective-rapps-dev,#fetch-play - Alarms:
#rewarded-apps-alarms