# Economy, Retention & Social Systems Audit

> Generated: 2026-02-07 | Auditor: economy-auditor (static code-path analysis)
> Scope: TypeScript backend (`server/src/`) and iOS client (`ios/lichunWebsocket/`)

---

## Executive Summary

The economy, retention, and social/dating systems have **critical structural defects** that make most monetization flows non-functional and several pathways exploitable. The diamond economy uses two unsynced currency stores, IAP validation is not wired into the active command flow, and the dating/relationship lifecycle is broken at nearly every stage due to payload mismatches between iOS and the TS backend.

**Total issues found: 33** (11 P0, 12 P1, 8 P2, 2 P3)

---

## Table of Contents

1. [Economy System](#1-economy-system)
2. [Retention System](#2-retention-system)
3. [Social/Dating System](#3-socialdating-system)
4. [Diamond Economy Balance Sheet](#4-diamond-economy-balance-sheet)
5. [Fix Priority Recommendations](#5-fix-priority-recommendations)

---

## 1. Economy System

### P0-ECO-1: Client can tamper costs (including negative) in event responses

**Severity: P0 (Exploit)**

Server trusts client-sent `energyCost`/`moneyCost`/`diamondCost` and directly subtracts them. Negative values become currency gains.

- `server/src/handlers/events.ts:164` - energyCost applied from client payload
- `server/src/handlers/events.ts:171` - moneyCost applied from client payload
- `server/src/handlers/events.ts:299` - same for question event responses
- `server/src/handlers/events.ts:306` - same for question event responses

**Fix**: Server must look up event costs from its own event definitions, never trust client-sent cost values.

---

### P0-ECO-2: IAP is effectively unvalidated and replayable

**Severity: P0 (Exploit)**

The active command path uses `purchaseInAppItem` with no receipt validation. iOS sends a direct WebSocket purchase command instead of going through Apple receipt validation.

- `server/src/handlers/purchases.ts:98-99` - handler does not call validation
- `server/src/services/shop/shop_manager.ts:528-533` - grants diamonds without receipt check
- `ios/lichunWebsocket/Features/Store/Views/ProductGridView.swift:110` - iOS sends direct WS command

**Fix**: Wire `handleValidatePurchase` into the purchase flow. Require Apple/Google receipt before granting diamonds.

---

### P0-ECO-3: Receipt-validation path is not wired into command registry

**Severity: P0 (Dead code)**

`handleValidatePurchase` exists in `server/src/monetization/validation.ts:424` but no command in the handler registry maps to it. It is unreachable from normal WebSocket routing.

- `server/src/handlers/index.ts:12` - registry does not include validatePurchase

**Fix**: Register `validatePurchase` command and make iOS/Android use it.

---

### P0-ECO-4: Receipt idempotency is broken in practice

**Severity: P0 (Exploit)**

- `processed_transactions` table is referenced but never created in TS backend init (`server/src/monetization/validation.ts:133,161`)
- On DB error, duplicate check falls back to `false` (accept) and mark-processed errors are swallowed (`validation.ts:139,167`)
- Idempotency key includes `playerId`, so the same Apple transaction can be replayed across different accounts (`validation.ts:118`)

**Fix**: Create `processed_transactions` table in DB init. Key by transaction ID only. Never fall back to accept on error.

---

### P0-ECO-5: Diamond economy is split into two unsynced currencies

**Severity: P0 (Data integrity)**

Retention/validation rewards write to an in-memory `playerDiamonds` map (`server/src/monetization/diamondEconomy.ts:23,68`), not to `player.c.diamonds`. Spend checks for energy/time use the map (`diamondEconomy.ts:158`), but gameplay and client updates use `player.c.diamonds` (`GameEngine.ts:356`, `Person.ts:367`). No sync initialization at session start (`WebSocketServer.ts:135`).

Affected reward paths:
- `server/src/services/retention/achievements.ts:361`
- `server/src/services/retention/dailyRewards.ts:398`
- `server/src/services/retention/dailyQuests.ts:638`

**Fix**: Eliminate the in-memory map. All diamond operations should read/write `player.c.diamonds` directly and persist via the normal save path.

---

### P1-ECO-6: Time skip flow is non-functional with current iOS client

**Severity: P1 (Broken feature)**

- Server requires `playerState` in payload; iOS sends only `skipType` (`timeSkips.ts:306`, `WebSocketService.swift:365`)
- Even when called manually, server returns summary but does not mutate session/player timeline/stats (`timeSkips.ts:200,269`)
- iOS also parses wrong tier key (`duration` vs `durationSeconds`) (`timeSkips.ts:344`, `WebSocketService.swift:868`)

**Fix**: Server should read state from session player, not payload. Apply time progression to the actual game state. Align payload keys.

---

### P1-ECO-7: Energy refill flow does not correctly apply/persist refill

**Severity: P1 (Broken feature)**

- Purchase logic computes from payload `currentEnergy` (default 0) instead of authoritative player state (`energyRefills.ts:181`)
- Handler sends result but does not update `session.player` energy/diamonds (`purchases.ts:124,132`)
- Unlimited energy expiry is in-memory only, not integrated into gameplay checks (`energyRefills.ts:34,137`)
- iOS expects `unlimited_until` but server sends `unlimitedUntil` (`energyRefills.ts:129`, `WebSocketService.swift:852`)

**Fix**: Read energy from session player. Apply refill result to player state. Persist unlimited-energy expiry. Align key names.

---

### P1-ECO-8: Shop purchase flow is broken

**Severity: P1 (Broken feature)**

- iOS sends item ID as plain string; server expects object with `itemId` (`StoreItemCard.swift:186`, `purchases.ts:46`)
- Store item IDs are random UUIDs regenerated each `getStoreItems()` call; purchase lookup regenerates new IDs so lookup always fails (`shop_manager.ts:56,410`, `Player.ts:29`)
- Currency deduction code itself is correct once an item resolves (`shop_manager.ts:420,428`)
- No negative-price catalog items found

**Fix**: Use stable item IDs (e.g., slug-based). Accept both string and object payload format.

---

### P2-ECO-9: iOS message handling mismatches hide economy failures

**Severity: P2 (Silent failures)**

- iOS expects `error_code`, server sends `errorCode` (`WebSocketService.swift:1122`, `energyRefills.ts:186`)
- iOS expects nested `result` for daily reward/quest claim, server sends fields at root level (`WebSocketService.swift:937,986`, `retention.ts:190,272`)
- No handling for `inAppPurchaseComplete`, `purchaseValidated`, `diamondUpdate` message types (`WebSocketService.swift:831`)
- Store UI marks purchase success optimistically before server confirmation (`StoreItemCard.swift:188`)

**Fix**: Standardize error/success response shapes. Add handlers for all purchase-related message types.

---

## 2. Retention System

### P0-RET-1: Daily rewards can award wrong day (Day 7 on first claim)

**Severity: P0 (Logic bug)**

`dailyRewards.ts:245` initializes `next_reward_day = 1`, and the claim computes `rewardDay = 7` when `next_reward_day === 1` due to modular arithmetic (`dailyRewards.ts:383`). First-time claimers get the Day 7 (50 diamond) reward.

**Fix**: Initialize `next_reward_day = 0` or fix the reward day calculation to use `next_reward_day` directly.

---

### P0-RET-2: Daily reward claim is not transaction-safe (race exploit)

**Severity: P0 (Exploit)**

`claimDailyReward` does read-check-award-update without locking or transactions (`dailyRewards.ts:344,398,413`). Concurrent WebSocket claims can double-award.

**Fix**: Wrap the claim in a DB transaction with row-level locking, or use an atomic UPDATE with WHERE conditions.

---

### P0-RET-3: Achievement duplicate-reward exploit

**Severity: P0 (Exploit)**

Achievement unlock checks use an in-memory cache that is only loaded when `getAchievements` is called (`achievements.ts:314,391`, `retention.ts:56`). A returning player who hasn't called `getAchievements` can re-trigger already-unlocked achievements and re-earn diamonds.

**Fix**: Load player's unlocked achievements at session start. Or check the DB on every unlock attempt.

---

### P0-RET-4: claimEvent has no idempotency/validation in active handler

**Severity: P0 (Exploit)**

The active `claimEvent` handler in `retention.ts:296` just returns success without verifying event existence, claimed state, or reward logic. The alternate handler in `events.ts:238` that checks a Set is not the mapped command handler (`index.ts:52`).

**Fix**: Route `claimEvent` to a handler that validates the event exists, is unclaimed, and applies rewards atomically.

---

### P1-RET-5: Daily quest DB path is broken for claiming

**Severity: P1 (Broken feature)**

- Quest completion immediately sets `completed_at` (`dailyQuests.ts:414`) and awards diamonds (`dailyQuests.ts:638`)
- Claim path then treats `completed_at` as "already claimed" (`dailyQuests.ts:870`), blocking the explicit claim
- DB path also parses `playerId` as integer (`dailyQuests.ts:272`), incompatible with iOS UUID player IDs

**Fix**: Separate `completed_at` from `claimed_at`. Handle UUID player IDs.

---

### P1-RET-6: Most daily quest templates are not completable

**Severity: P1 (Feature gap)**

Only 3 of 11 quest types have active progress tracking wired:
- `talk_to_characters` (via `conversations.ts:106`)
- `buy_item` (via `purchases.ts:79`)
- `go_on_date` (via `romance.ts:67`)

The other 8 templates (`attend_class`, `socialize`, `work_hours`, `complete_activities`, `study`, `spend_energy`, `earn_money`, `increase_affinity`) have no active `updateQuestProgress` calls.

**Fix**: Add `updateQuestProgress` calls in the relevant handlers/game loop paths for all quest types.

---

### P1-RET-7: Diamond rewards from retention are not reliably credited

**Severity: P1 (Data integrity)**

`awardDiamonds` writes to an in-memory map (`diamondEconomy.ts:23,57`), not `player.c.diamonds`. Retention claim handlers do not update the player diamonds object (`retention.ts:186`). Rewards can be non-persistent and out of sync with what the client displays.

(Same root cause as P0-ECO-5, listed here for retention context.)

---

### P1-RET-8: Many achievement conditions have no active trigger callsites

**Severity: P1 (Feature gap)**

Active integration paths only call a subset of achievement check types:
- `start_school`, `get_job`, `promotion`, `fired`, `dating`, `marriage`, `affinity_milestone`, `purchase_item`, `birthday`, `death`

Many achievements (e.g., `health_maintained`, `dropout`, `retire`, `work_hours`, `annual_earnings`, `first_kiss`) have defined conditions but no code path that calls `checkAchievementsAsync` with those event types.

**Fix**: Wire achievement checks into the relevant game loop/handler paths for all defined achievement types.

---

### P2-RET-9: Daily reward Day 5 "item" is UI-only

**Severity: P2 (Cosmetic/misleading)**

Day 5 reward shows "bonus item" in UI labels but the backend does not implement any item grant (`dailyRewards.ts:498,407`).

**Fix**: Either implement the item grant or change the UI label to match what is actually awarded.

---

### P2-RET-10: iOS retention response parsing mismatches

**Severity: P2 (Silent failures)**

- iOS expects `dailyRewardClaimed.result` but server sends root-level fields (`WebSocketService.swift:937`, `retention.ts:190`)
- iOS expects `questRewardClaimed.result` but server sends root-level fields (`WebSocketService.swift:986`, `retention.ts:273`)
- iOS decodes `questProgress` as full `DailyQuestsState`; server sends delta object (`WebSocketService.swift:969`, `integration.ts:68`)
- iOS decodes full `Achievement` for `achievementUnlocked`; server payload omits required fields (`Achievement.swift:15`, `integration.ts:37`)

**Fix**: Standardize response shapes. Either wrap in `result` or have iOS parse root-level fields.

---

### P2-RET-11: Timezone inconsistency in daily systems

**Severity: P2 (Edge case)**

- Daily rewards use a mix of UTC strings and local midnight calculations (`dailyRewards.ts:103,110,130`)
- Daily quests reset by UTC day via `toISOString().split('T')[0]` (`dailyQuests.ts:257`)
- Players in late timezones may see inconsistent reset times between systems

**Fix**: Standardize on UTC for all daily reset calculations and document the reset time for players.

---

### P3-RET-12: Streak logic is correct but day-1 edge case exists

**Severity: P3 (Minor)**

Streak resets to 1 when a player misses more than 1 day (`dailyRewards.ts:291`). The core logic is correct, but the first-claim Day 7 bug (P0-RET-1) means the streak system is unreliable until that is fixed.

---

### P2-RET-13: iOS daily reward claim-day selection is off-by-one

**Severity: P2 (UI bug)**

UI only enables claim on `reward.id == ((currentStreak % 7) + 1)` (`DailyRewardsView.swift:94`), which produces incorrect day mapping.

**Fix**: Align with server's reward day calculation.

---

## 3. Social/Dating System

### P0-SOC-1: iOS romance payloads do not match server handler expectations

**Severity: P0 (Broken feature)**

Server handlers destructure `{partnerId}` / `{ideaName}` from payload objects, but WebSocket dispatch passes iOS message strings directly, resulting in `undefined` IDs and failing operations.

- `server/src/server/WebSocketServer.ts:101` - dispatch passes raw message
- `server/src/handlers/romance.ts:31,53` - expects object destructuring
- `ios/lichunWebsocket/Features/Dating/Views/DatingView.swift:58,110` - sends string payload
- `ios/lichunWebsocket/Features/Character/Views/NPCProfileView.swift:233` - sends string payload

**Fix**: Normalize romance handler to accept both string and object payloads, or update iOS to send objects.

---

### P0-SOC-2: relData schema is incompatible between TS backend and iOS model

**Severity: P0 (Broken feature)**

iOS expects `relationshipNotes`/`commonInterests`/`challenges`/`futurePlans` in Relationship model, but TS `createRelationship` emits `description` and omits those fields. iOS decoding fails.

- `ios/lichunWebsocket/Core/Models/Player.swift:44` - iOS Relationship model
- `server/src/services/relationships/relationship_manager.ts:63` - TS relationship creation
- `ios/lichunWebsocket/WebSocketService.swift:662` - iOS relData parsing

**Fix**: Align the Relationship schema between server and iOS. Add missing fields or make them optional.

---

### P0-SOC-3: relData is overloaded with two incompatible object types

**Severity: P0 (Data corruption)**

Conversation messaging stores `{characterId, messaging_modifiers}` in `relData`, while romance code pushes relationship objects there. iOS decodes `[Relationship]` which breaks on mixed entries.

- `server/src/models/Player.ts:90` - relData array
- `server/src/events/conversations/messagingStyle.ts:91` - messaging modifier pushed to relData
- `server/src/services/relationships/relationship_manager.ts:353` - relationship pushed to relData

**Fix**: Separate messaging modifiers into their own field (e.g., `messagingModifiers`). Reserve `relData` for relationships only.

---

### P0-SOC-4: Relationship progression is blocked (Prospect dead-end)

**Severity: P0 (Broken feature)**

`romance()` creates a `Prospect` relationship. But `dateNight` and active relationship logic only recognize `Dating` | `Married` status. No wired path calls `propose()` or `marry()` to advance the state.

- `server/src/services/relationships/relationship_manager.ts:349` - creates Prospect
- `server/src/services/relationships/relationship_manager.ts:206` - requires Dating|Married
- `server/src/services/relationships/relationship_manager.ts:485` - propose exists but unused

**Fix**: Wire the state machine: `Prospect -> Dating -> Engaged -> Married`. Add handlers or automatic transitions.

---

### P0-SOC-5: Break-up/divorce leaves stale relationship state

**Severity: P0 (State leak)**

Only `player.c.relationship` is cleared on breakup/divorce. `player.c.partner`, partner relationship tags (`dating_match`, `partner`), and partner-side relationship links are not cleaned. Events elsewhere gate on `player.character.partner`.

- `server/src/services/relationships/relationship_manager.ts:383` - breakup logic
- `server/src/services/relationships/relationship_manager.ts:531` - divorce logic
- `server/src/services/character/character_manager.ts:344` - partner reference

**Fix**: Implement complete cleanup: clear `c.partner`, remove relationship tags from both sides, remove relData entry, clear partner's back-reference.

---

### P1-SOC-6: Swipe matching bypasses server matching logic

**Severity: P1 (Design issue)**

`services/dating/matching.ts` is effectively unused. iOS does random match locally via `Bool.random()` and mutates local state before server sync.

- `server/src/services/dating/matching.ts:60` - unused matching service
- `ios/lichunWebsocket/Features/Dating/Views/SwipeDatingView.swift:266` - client-side random
- `ios/lichunWebsocket/Features/Dating/Views/SwipeDatingView.swift:288` - local state mutation

**Fix**: Move match decision to server. Use the compatibility scoring system that already exists.

---

### P1-SOC-7: Date activity flow is broken end-to-end

**Severity: P1 (Broken feature)**

- `DateActivitySelectionView` sends unsupported `startDate` command (`DateActivitySelectionView.swift:466`)
- `DatingView` sends `dateNight` with local names that don't match backend date idea names (`DatingView.swift:110`)
- Server expects object payload for `dateNight` (`romance.ts:54`)

**Fix**: Align date activity names and payload format between iOS and server. Register `startDate` if needed.

---

### P1-SOC-8: Relationship-event and date-minigame commands are unhandled

**Severity: P1 (Dead feature)**

iOS sends `relationshipEventResponse` and `dateMiniGameResponse` but the handler registry has no handlers for these. Fallback returns generic error. iOS only surfaces errors with `error_code` key (which server doesn't send).

- `ios/lichunWebsocket/Features/Dating/Components/RelationshipEventModal.swift:436`
- `ios/lichunWebsocket/Features/Dating/Views/DateMiniGameView.swift:565`
- `server/src/handlers/index.ts:12` - not registered
- `ios/lichunWebsocket/WebSocketService.swift:1122` - expects `error_code`

**Fix**: Register handlers for these commands. Align error key naming.

---

### P1-SOC-9: Relationship event protocol mismatch

**Severity: P1 (Incompatible)**

Server event builder emits `answers` + snake_case types (e.g., `breakup_threat`), while iOS expects `choices` + camelCase enum (`breakupThreat`).

- `server/src/services/dating/relationshipEvents.ts:96,303` - server format
- `ios/lichunWebsocket/WebSocketService.swift:1165` - iOS parsing
- `ios/lichunWebsocket/Features/Dating/Components/RelationshipEventModal.swift:26` - iOS enum

**Fix**: Standardize on one naming convention. Update either server or iOS to match.

---

### P2-SOC-10: iOS relationship display is inconsistent across features

**Severity: P2 (UI inconsistency)**

Dating view is driven by `person.relationship` + `relData`; Character components use `relationships[]` tags or raw `person.relationship` text (which can be a UUID). This can show contradictory states.

- `ios/lichunWebsocket/Features/Dating/Views/DatingView.swift:27`
- `ios/lichunWebsocket/Features/Character/Components/ProfileHeader.swift:109`
- `ios/lichunWebsocket/Features/Character/Components/CharacterCard.swift:63`

**Fix**: Unify relationship display logic into a shared helper.

---

### P2-SOC-11: Affinity persistence is correct but semantics are inconsistent

**Severity: P2 (Design inconsistency)**

Affinity mutations on `player.r` persist via `toJSON` + periodic/disconnect save. However, `dateNight` changes `score`/`prestige` only (not affinity), making date outcomes feel disconnected from relationship strength.

- `server/src/services/relationships/relationship_manager.ts:413` - dateNight modifies score
- `server/src/services/relationships/relationship_manager.ts:451` - no affinity change

**Fix**: Have date activities also modify affinity to provide meaningful relationship progression.

---

## 4. Diamond Economy Balance Sheet

### Sources (Diamond Income)

| Source | Amount | Notes |
|--------|--------|-------|
| **Achievements (44 total)** | | |
| first_steps | 25 | Tutorial completion |
| first_day_school | 10 | |
| graduate_hs | 25 | |
| graduate_college | 50 | |
| graduate_masters | 75 | |
| graduate_phd | 100 | |
| get_married | 30 | |
| have_child | 40 | |
| live_to_50 | 20 | |
| live_to_100 | 200 | |
| first_job | 10 | |
| promotion | 20 | |
| become_manager | 30 | |
| become_ceo | 75 | |
| earn_100k | 50 | |
| earn_1m_lifetime | 100 | |
| earn_10m_lifetime | 250 | |
| perfect_performance | 25 | |
| make_first_friend | 10 | |
| best_friend | 30 | |
| date_10_people | 25 | |
| first_kiss | 15 | |
| golden_anniversary | 100 | |
| five_children | 50 | |
| popular | 35 | |
| first_purchase | 5 | |
| own_25_items | 20 | |
| own_50_items | 40 | |
| own_100_items | 80 | |
| prestige_100 | 25 | |
| prestige_500 | 75 | |
| max_prestige | 150 | |
| die_at_69 | 50 | |
| fired_3_times | 25 | |
| never_marry | 30 | |
| marry_3_times | 40 | |
| die_poor | 20 | |
| die_rich | 100 | |
| perfect_health | 50 | |
| workaholic | 30 | |
| early_retirement | 75 | |
| straight_a | 40 | |
| dropout | 15 | |
| no_friends | 25 | |
| **Achievement total (one lifetime)** | **~2,260** | Not all achievable in one life |
| | | |
| **Daily Rewards (7-day cycle)** | | |
| Day 1 | 5 | |
| Day 2 | 10 | |
| Day 3 | 0 | Energy only (50 energy) |
| Day 4 | 15 | |
| Day 5 | 20 | UI shows "item" but only diamonds |
| Day 6 | 25 | |
| Day 7 | 50 | |
| **Weekly total** | **125** | |
| | | |
| **Daily Quests (per day, 3 assigned)** | | |
| talk_to_characters (3) | 10 | Wired |
| buy_item (1) | 5 | Wired |
| attend_class (2) | 8 | NOT wired |
| socialize (2) | 10 | NOT wired |
| work_hours (6) | 15 | NOT wired |
| go_on_date (1) | 20 | Wired |
| complete_activities (5) | 18 | NOT wired |
| study (4) | 15 | NOT wired |
| spend_energy (50) | 25 | NOT wired |
| earn_money (500) | 30 | NOT wired |
| increase_affinity (20) | 35 | NOT wired |
| **Daily quest average (3 random)** | **~30-50** | Only if wired quests are assigned |
| | | |
| **Tutorial/Onboarding** | | |
| Tutorial completion | 50 | `tutorial.ts:138` |
| Onboarding completion | 25 | `tutorial.ts:139` |
| Tutorial event rewards | 3, 5, 10, 25 | Various onboarding events |
| | | |
| **IAP Diamond Packs** | | |
| diamond1 ($0.99) | 100 | |
| diamond2 ($1.99) | 210 | |
| diamond3 ($2.99) | 330 | |
| diamond4 ($3.99) | 460 | |
| diamond5 ($4.99) | 600 | |
| Validation-path packs | 100-2000 | Dead code, not routed |

### Sinks (Diamond Spend)

| Sink | Cost | Notes |
|------|------|-------|
| **Energy Refills** | | |
| Small refill | 10 | |
| Medium refill | 20 | |
| Full refill | 35 | |
| Unlimited 24h | 50 | |
| | | |
| **Time Skips** | | |
| 1 hour | 5 | |
| 1 day | 25 | |
| 1 week | 100 | |
| Next event | 50 | |
| | | |
| **Date Activities** | 30, 50, 100 | `dateActivities.ts:110` |
| **Relationship Events** | 20-50 | Various options |
| **Event Choices** | 5-10 | School, career, family events |

### Balance Assessment

- **First-week earning potential**: ~125 (daily rewards) + ~75 (tutorial) + ~100-200 (quests, if working) = ~300-400 diamonds
- **First-week spend pressure**: 1 energy refill (10-50) + possible time skip (5-100) = moderate
- **Long-term**: Achievement diamonds (~2,260 total across a full lifetime) provide a reasonable long-term drip, but many achievement triggers are not wired
- **Critical issue**: Due to the dual-currency bug (P0-ECO-5), earned diamonds may not actually be spendable, and due to broken purchase flows (P1-ECO-6,7,8), spending is largely non-functional

---

## 5. Fix Priority Recommendations

### Immediate (P0 - Exploitable or data-corrupting)

1. **Fix client-controlled costs exploit** (P0-ECO-1) - Server-side cost lookup
2. **Wire IAP receipt validation** (P0-ECO-2, P0-ECO-3) - Route `validatePurchase` command
3. **Fix processed_transactions table** (P0-ECO-4) - Create table, fix key, remove fallback
4. **Unify diamond currency** (P0-ECO-5) - Eliminate in-memory map, use `player.c.diamonds`
5. **Fix daily reward Day 7 on first claim** (P0-RET-1) - Fix reward day calculation
6. **Add transaction safety to daily reward claims** (P0-RET-2) - DB transaction + locking
7. **Fix achievement re-trigger exploit** (P0-RET-3) - Load achievements at session start
8. **Fix claimEvent handler** (P0-RET-4) - Add validation and idempotency
9. **Fix romance payload mismatches** (P0-SOC-1) - Normalize handler input
10. **Separate relData types** (P0-SOC-2, P0-SOC-3) - Messaging modifiers vs relationships
11. **Fix relationship state machine** (P0-SOC-4) - Wire Prospect -> Dating -> Married transitions
12. **Fix break-up/divorce cleanup** (P0-SOC-5) - Complete state cleanup on both sides

### High (P1 - Broken features)

13. **Fix time skip flow** (P1-ECO-6) - Read from session, apply to game state
14. **Fix energy refill flow** (P1-ECO-7) - Use authoritative state, persist expiry
15. **Fix shop purchase flow** (P1-ECO-8) - Stable item IDs, accept both payload formats
16. **Fix daily quest claiming** (P1-RET-5) - Separate completed_at from claimed_at
17. **Wire remaining quest progress** (P1-RET-6) - Add updateQuestProgress calls
18. **Wire remaining achievement triggers** (P1-RET-8) - Add checkAchievements calls
19. **Move match decision to server** (P1-SOC-6) - Use compatibility scoring
20. **Fix date activity flow** (P1-SOC-7) - Align names and payload format
21. **Register relationship event handlers** (P1-SOC-8) - Wire commands
22. **Fix relationship event protocol** (P1-SOC-9) - Standardize naming

### Medium (P2 - UI/UX issues)

23. **Standardize error/success response shapes** (P2-ECO-9)
24. **Fix Day 5 item label** (P2-RET-9)
25. **Fix iOS retention response parsing** (P2-RET-10)
26. **Standardize daily reset timezone** (P2-RET-11)
27. **Fix iOS daily reward day selection** (P2-RET-13)
28. **Unify relationship display logic** (P2-SOC-10)
29. **Add affinity changes to dates** (P2-SOC-11)

### Low (P3 - Minor)

30. **Streak edge case** (P3-RET-12) - Will be fixed by P0-RET-1

---

*This is a static code-path audit. No runtime tests were executed.*
