KPIs
This is the reference for partner and staff KPIs: who sees what, how each KPI is defined, and how comparisons stay anonymous. The decision record is ADR 0033. Definitions are implemented as Cube measures in services/cube/model/ and must match this page.
Status key: ā
data exists Ā· š” needs the domain event log Ā· š needs the activity market Ā· ā³ future (no source table yet).
Visibility tiersā
| Tier | Audience | Population | Protection |
|---|---|---|---|
| Own | The party itself | Players linked to the party. Operator: active identity_links with audience_party_id = the party. Provider: Accounts reached through its integrations (same scope as studio analytics). Affiliate: attributed players (ā³). | Account floor ā„ 50 per cell; drill-down only by the party's own aliases |
| Network comparison | The party | Y = your players; R = all active PlayerPal Accounts minus Y; N = all active Accounts | Rest-of-network floor, dominance rule, per-player figures only |
| Benchmark | The party | Peers of the same party type | Peer floor, dominance rule, statistics only |
| Network | PlayerPal staff | Everyone | Staff permission reports.aggregate.read; every access is audited |
Scope always comes from the authenticated principal. A party_id in the query string or an x-party-id header is ignored on partner routes.
Anonymity rulesā
They apply to the Network comparison and Benchmark tiers. They are enforced when segments are written and checked again when they are read. Thresholds are configuration; production must not go below these values.
| Rule | Value | Config |
|---|---|---|
| Account floor | ā„ 50 distinct Accounts | BENCHMARK_MIN_ACCOUNTS |
| Peer floor | ā„ 5 other parties of the requester's type | BENCHMARK_MIN_PEERS |
| Dominance | suppress if one other party > 50 % of the measure | BENCHMARK_DOMINANCE_P |
| Dominance, top two | suppress if the top two together > 75 % | BENCHMARK_DOMINANCE_P2 |
- Published: p25, p50, p75, mean and the requester's percentile band (for example
p50āp75). - Never published:
- min, max or an exact rank
- per-peer values, names or IDs
- absolute network totals (network size is shown only as a band, for example "100k+ players")
- Fixed segments only: closed ISO weeks or calendar months, and markets from the catalogue. Arbitrary date ranges are not allowed, so two segments can't be differenced to isolate a peer.
- Fallback: a market segment that fails a rule falls back to global. If global also fails, the response is
{ "suppressed": true, "reason": "<rule>" }. - Cross-party KPIs (
cmp.share_of_wallet,cmp.overlap_rate,cmp.multi_homing,cmp.market_mix): your players' activity with other parties must come from at least 5 parties and pass the dominance rule.
Marketā
market is the ISO 3166-1 alpha-2 licensed jurisdiction (uppercase, e.g. MT) that the partner reports on each activity event, in the JSON body or as a market column in uploads.
- A malformed value rejects the whole batch with
market_invalid(400). - If the integration declares
marketsand the event's market isn't one of them, the event is rejected withmarket_not_licensed. - If the event has no market and the integration declares exactly one, that market is used. Otherwise the market stays empty, and the event only counts in global figures.
- The batch idempotency checksum covers
marketonly when it is sent, so replays of batches sent before markets existed still match.
Nothing is stored on the player. A player's market for a day is derived from that day's activity, and a player active in two markets counts in both.
Own-tier KPIsā
| ID | KPI | Definition | Who | Source | Status |
|---|---|---|---|---|---|
own.linked_players | Linked players | Accounts with an active link at the end of the period | op | identity_links | ā |
own.new_links | New links | Links completed in the period | op | identity_links.linked_at | ā |
own.unlink_rate | Unlink rate | Links unlinked or revoked in the period Ć· active links at the start of the period | op | identity_links | ā |
own.link_funnel | Link funnel | link_intents created ā completed; conversion % | op | link_intents | ā |
own.active_accounts | Active accounts | Distinct Accounts with ā„ 1 round (DAU / WAU / MAU) | op, prv | analytics_account_*_day | ā |
own.stickiness | Stickiness | Mean DAU Ć· MAU | op, prv | as above | ā |
own.rounds | Rounds | Ī£ rounds | op, prv | as above | ā |
own.bet_eur / own.win_eur | Turnover / wins | Ī£ bet_eur, Ī£ wins (EUR) | op, prv | as above | ā (š split) |
own.ggr | GGR proxy | Ī£ (bet ā win) | op, prv | as above | ā |
own.share_of_turnover | Share of linked players' turnover | Provider studio "share of slot turnover" (ADR 0026) | prv | analytics_studio_account_day | ā |
own.retention_d7 / _d30 | Retention | Of Accounts linked in the cohort week, the share active again on day 7 / 30 | op, prv | player_active_day | š” |
own.churn_rate | Monthly churn | Active in Pā1 and not in P Ć· active in Pā1 | op, prv | player_active_day | š” |
own.points_issued | Points issued / funded | Ī£ ledger credits by campaign | op | points_ledger | ā |
own.points_per_player | Points per active account | points issued Ć· active accounts | op | points_ledger | ā |
own.qualification_rate | Rewarded-round qualification | Rounds credited Ć· rounds | prv | points_ledger, activity | ā |
own.events_accepted | Ingest acceptance | Accepted Ć· received events, by reject_reason | op, prv (own integrations) | activity_events | ā |
own.batch_latency | Batch latency | p50 / p95 of completed_at ā received_at | op, prv | activity_batches | ā |
own.attributed_players | Attributed / activated players | Players attributed to the affiliate; activation % | aff | attribution model | ā³ |
own.rewards_claimed | Rewards claimed | partner | rewards model | ā³ |
Network comparison KPIs (cmp.*)ā
Live in v1 (GET /v1/kpis/compare, see api/kpis.md):
active_days,rounds_per_player,bet_per_player,ggr_per_player,points_per_playeravg_stake,payout_ratio,churn_rate,link_conversionmulti_homing,overlap_rate,share_of_wallet
The other rows below are planned.
How "the network" is measured:
- Per-player KPIs compare the median of your players with the distribution over the players of other parties of your type, one unit per (player, party) relationship.
- Ratio KPIs compare your ratio with the distribution of other parties' ratios.
Populations for a period P (closed week or month) and an optional market M:
- Y: Accounts in the party's reach on at least one day in P.
- R: all active PlayerPal Accounts (
players.status = 'active') that were active in P, minus Y. R is the default baseline. - N: all active Accounts, including Y. Shown as a secondary figure.
Response fields:
you: the value over Ynetwork: the value over R, plusnetwork_allover Nindex:you Ć· network Ć 100band: where your players' median sits in the network's per-player distributionnetwork_dist:{p25, p50, p75}suppressed, plusreasonwhen it is
Per-player measures are computed per Account first and then aggregated, so the figures report medians as well as means.
| ID | KPI | Definition (for Y and for R) | Who | Source | Status |
|---|---|---|---|---|---|
cmp.active_rate | Active rate | Active Accounts Ć· Accounts in the population at the start of P | op, prv, aff | player_active_day, party_reach_day | š” |
cmp.active_days | Active days per active player | Median / mean of distinct active days per Account | op, prv | player_active_day | ā |
cmp.stickiness | Stickiness | Mean DAU Ć· MAU | op, prv | player_active_day | ā |
cmp.rounds_per_player | Rounds per active player | Median / mean of rounds per Account | op, prv | analytics_account_day | ā |
cmp.bet_per_player | Turnover per active player | Median / mean of bet_eur per Account | op, prv | analytics_account_day | ā |
cmp.ggr_per_player | GGR proxy per active player | Median / mean of (bet ā win) per Account | op, prv | analytics_account_day | ā |
cmp.avg_stake | Average stake | Ī£ bet_eur Ć· Ī£ rounds | op, prv | analytics_account_day | ā |
cmp.payout_ratio | Observed payout ratio | Ī£ win Ć· Ī£ bet | op, prv | analytics_account_day | ā |
cmp.retention_d7 / _d30 | New-player retention | Of Accounts that joined in the cohort week (Y: linked to you; R: activated on PlayerPal), the share active again 7 / 30 days later | op, prv | player_active_day, identity_links, players.activated_at | š” |
cmp.churn_rate | Monthly churn | Active in Pā1 and not in P Ć· active in Pā1 | op, prv | player_active_day | š” |
cmp.reactivation_rate | Reactivation | Active in P after ā„ 30 inactive days Ć· Accounts that were inactive | op, prv | player_active_day | š” |
cmp.tenure | Player tenure | Median days from players.activated_at to the end of P, over active Accounts | op, prv, aff | players | ā |
cmp.points_per_player | Points earned per active player | Median / mean of ledger credits per Account | op | points_ledger | ā |
cmp.redemption_rate | Redemption rate | Points redeemed Ć· points earned | op | rewards model | ā³ |
cmp.multi_homing | Operators per player | Median / mean of active operator links per Account | op, prv | identity_links | ā |
cmp.overlap_rate | Overlap | Share of Y also active with ā„ 1 other party of your type (count only, never which) | op, prv | party_reach_day, activity | ā |
cmp.share_of_wallet | Share of your players' turnover | Y's bet_eur with you Ć· Y's total bet_eur across PlayerPal. The network figure is the median share across parties of your type. | op, prv | analytics_account_*_day | ā (op š) |
cmp.market_mix | Market mix | Share of bet_eur per market, Y vs R | op, prv | activity market | š |
cmp.link_conversion | Link conversion | Completed Ć· created link_intents in P | op | link_intents | ā |
cmp.time_to_first_play | Time to first play | Median hours from link completed to the first round with that party | op, prv | identity_links, activity | ā |
cmp.game_mix | Game-category mix | Share of bet_eur by game category, Y vs R | op, prv | game category metadata | ā³ |
Benchmark KPIsā
Any KPI marked benchmarkable in the catalogue: the per-party value of an Own-tier or cmp.* KPI, compared across peers of the same type. Response: peer {p25, p50, p75, mean}, the requester's band, peers shown as a band (e.g. "5ā10"), or suppressed.
Staff-only KPIs (Network tier)ā
| KPI | Source | Status |
|---|---|---|
| Signups and signup funnel by method | player_registrations, players | ā |
| Network DAU / WAU / MAU, stickiness | player_active_day | š” |
| Network turnover, GGR, points liability | activity rollups, points_ledger | ā |
| Login success / failure by method | domain_events | š” |
| Recovery-code use, account merges, deletions | recovery_attempt_log, player_account_merges, domain_events | ā / š” |
| Connected parties by type, integrations by status | parties, integrations | ā |
| Ingest health across all integrations | activity_events, activity_batches | ā |
| KPI API usage per party and client | domain_events (kpi.api.accessed) | š” |
Domain eventsā
Append-only domain_events, written in the same transaction as the change. No PII. Delete Account nulls player_id and keeps the event.
| Group | Event types |
|---|---|
| Player | player.registered, player.activated, player.deleted |
| Auth | auth.login.succeeded (a session was issued; scope), auth.login.failed (method, reason), auth.session.revoked (actor player or staff) |
| Linking | link.intent_created (led), link.completed (proof_type), link.unlinked (cascaded), alias.issued / alias.revoked (alias_type); party_id is the audience |
| Planned | account.merged, auth.method.added / auth.method.revoked, auth.recovery.used |
| Ingest | batch.received (events), batch.completed (state, events, rejected, latency_ms); party_id is the provider |
| Data access | kpi.api.accessed |
| Partner credentials | partner.credential.created, partner.credential.rotated, partner.credential.revoked |