# Brief: consume Golden Whale Foundation events Your task: build a service in [YOUR STACK] that ingests Golden Whale Foundation events into [YOUR SYSTEM]. Golden Whale emits one event per player, carrying their latest segmentations (value, churn risk, session churn risk, incentive usage, incentive guidance, reactivation potential) and ranked recommendations (incentives, game categories, games). Events reach you over one or both channels: - Kafka: topic [YOUR TOPIC], one JSON event per message - CSV: [YOUR CSV LOCATION], one flattened event per row Brokers, credentials and file locations come from your Golden Whale onboarding. Keep them in configuration, never in code. Source of truth: https://developer.goldenwhale.foundation. Read these first: - Full reference, one Markdown file: https://developer.goldenwhale.foundation/llms-full.txt - JSON Schema 3.0.0 (draft-07): https://developer.goldenwhale.foundation/schemas/foundation-event-3.0.0.schema.json - Example event, JSON: https://developer.goldenwhale.foundation/schemas/foundation-event.example.json - The same example event, CSV: https://developer.goldenwhale.foundation/foundation-event.example.csv - Migration notes per version: https://developer.goldenwhale.foundation/whats-new/ ## The event - `player` is the only required root field; `player.id`, `player.cohort` and `player.group` are required inside it. Every other block is optional and is **absent** when not sent. - Store `player.id` as a **string**, even when it looks numeric. It can be a GUID or a number; a numeric parse loses leading zeros. - An event carries only the blocks produced for it. **Merge per block, never replace the player record**: an event holding only `sessionChurnRisk` must leave the stored `value`, `churnRisk` and the rest untouched. - `trigger` says what produced the event: `scheduled` (batch run) or `session` (real-time). All predictions in one event share it. ### Segmentations `value`, `churnRisk`, `sessionChurnRisk`, `incentiveUsage`, `incentiveGuidance` and `reactivationPotential` all share one shape: `current` (required) and `previous` (optional), each with `score`, `sequence`, `segment` and `date`. - **Order and compare by `sequence`, never by parsing the `segment` label.** The label's number prefix does not always equal `sequence`: incentive usage labels start at `0.`, its `sequence` at 1. - Movement is `current.sequence` against `previous.sequence`. `previous` may be absent (no earlier prediction) or the same segment as `current`. - Per block, **keep the one with the newer `current.date`**. Events can be redelivered; an older block must never overwrite a newer one, and the same event processed twice must change nothing. - `score` scales differ per block: churn risk, session churn risk and reactivation potential are 0–1; value is an unbounded lifetime-value figure; incentive usage and guidance scores are not probabilities. Do not normalise or compare scores across blocks. - `incentiveGuidance` is a recommended posture (increase, continue, limit, avoid), not a risk score. - `date` is ISO-8601 with an offset (`2023-11-20T13:45:00+02:00`). Parse it timezone-aware and store UTC. - Segment meanings on /reference/segments/ are a draft pending product review. Build logic on segment values and `sequence`, not on that prose. ### Recommendations `incentiveRecommendations`, `favoriteGameCategories` and `recommendedGames` hold at most 3 items each, ranked by `rank` (1-based). - Treat each array as one unit: when an event carries it, replace the stored list. Do not merge items across events. - `recommendationId` is unique per emitted incentive recommendation. **Before acting on one (granting a bonus, sending an offer), check you have not already acted on that `recommendationId`.** A redelivered event must not grant twice. ### Target and Control `player.group` is `Target` or `Control`. Control is the holdout: store its events, but **never apply model-driven treatment (incentives, targeted offers) to Control players**, or the model's impact can no longer be measured. ### Forward compatibility - The schema does not forbid extra fields, and new versions add them. **Ignore fields you do not recognise**; a strict decoder that rejects unknown keys will break on a Golden Whale release. - Enum sets have grown before (churn risk gained `4. Dormant`). Store an unrecognised `segment` or `trigger` value as-is and log it; do not drop the event. - Major versions rename fields: 3.0.0 renamed `customerLTV` to `value` and removed `player.siteId`. Pin the schema version you build against and read the migration notes before upgrading. - `player.metadata` is free-form, customer-specific key/value data (e.g. `siteId`, `brand`, `region`) and is not validated by the schema. Keep it as JSON; do not assume any key exists. ## Kafka - Message value: UTF-8 JSON, one event per message. - Header `trigger`: `scheduled` or `session`, the same value as the root `trigger` field. Use it to route or filter without parsing the payload; fall back to the payload when the header is missing. - Do not rely on the message key. Identify the player by `player.id`. - Commit offsets only after the event is durably stored. With the merge rules above, redelivery is harmless. - An event that fails JSON parsing or schema validation goes to a dead-letter store with its raw bytes and the error. Never let one bad message block the partition. ## CSV - The first row is the header. **Match columns by name, never by position.** Your delivery may carry a subset of the columns; a missing column means the field was not sent. - Column names are the nested JSON paths in snake_case, joined with `_` (`value_current_segment`). Arrays widen into 1-based indexed columns, up to three (`recommended_games_1_game_id` … `recommended_games_3_game_id`). - An empty cell means absent. A block whose cells are all empty was not sent: do not overwrite stored data with nulls. - Numbers may be written as floats (`4.0`, `2000.0`, a rank of `1.0`). Parse numerically, then cast where an integer is expected. Keep `player_id` a string. - `player_metadata` is one cell holding JSON text; parse it as JSON. - Quoting is standard CSV (quotes doubled inside quoted cells). Use a real CSV parser, not a string split. - Loading the same file twice must change nothing. ## Suggested steps 1. Fetch the schema and the JSON example; validate the example with a draft-07 validator. Do not continue until it passes. 2. Parse the CSV example and check every value matches the JSON example. It is the same event. 3. Design storage: players keyed by `player.id`, one record per segmentation block holding `current` and `previous`, recommendation arrays stored as units, and the raw event kept for audit. 4. Build the Kafka consumer and/or the CSV loader on the same merge code. 5. Test at least: the same event twice; an event carrying only `sessionChurnRisk` (other blocks untouched); an older block arriving after a newer one; an unknown root field; an unknown `segment` value; a `Control` player; a CSV with missing columns and float-written integers; and a redelivered incentive recommendation (acted on once).