Skip to main content
InternalMarkdown

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​

TierAudiencePopulationProtection
OwnThe party itselfPlayers 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 comparisonThe partyY = your players; R = all active PlayerPal Accounts minus Y; N = all active AccountsRest-of-network floor, dominance rule, per-player figures only
BenchmarkThe partyPeers of the same party typePeer floor, dominance rule, statistics only
NetworkPlayerPal staffEveryoneStaff 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.

RuleValueConfig
Account floor≄ 50 distinct AccountsBENCHMARK_MIN_ACCOUNTS
Peer floor≄ 5 other parties of the requester's typeBENCHMARK_MIN_PEERS
Dominancesuppress if one other party > 50 % of the measureBENCHMARK_DOMINANCE_P
Dominance, top twosuppress 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 markets and the event's market isn't one of them, the event is rejected with market_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 market only 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​

IDKPIDefinitionWhoSourceStatus
own.linked_playersLinked playersAccounts with an active link at the end of the periodopidentity_linksāœ…
own.new_linksNew linksLinks completed in the periodopidentity_links.linked_atāœ…
own.unlink_rateUnlink rateLinks unlinked or revoked in the period Ć· active links at the start of the periodopidentity_linksāœ…
own.link_funnelLink funnellink_intents created → completed; conversion %oplink_intentsāœ…
own.active_accountsActive accountsDistinct Accounts with ≄ 1 round (DAU / WAU / MAU)op, prvanalytics_account_*_dayāœ…
own.stickinessStickinessMean DAU Ć· MAUop, prvas aboveāœ…
own.roundsRoundsĪ£ roundsop, prvas aboveāœ…
own.bet_eur / own.win_eurTurnover / winsĪ£ bet_eur, Ī£ wins (EUR)op, prvas aboveāœ… (šŸŒ split)
own.ggrGGR proxyĪ£ (bet āˆ’ win)op, prvas aboveāœ…
own.share_of_turnoverShare of linked players' turnoverProvider studio "share of slot turnover" (ADR 0026)prvanalytics_studio_account_dayāœ…
own.retention_d7 / _d30RetentionOf Accounts linked in the cohort week, the share active again on day 7 / 30op, prvplayer_active_day🟔
own.churn_rateMonthly churnActive in Pāˆ’1 and not in P Ć· active in Pāˆ’1op, prvplayer_active_day🟔
own.points_issuedPoints issued / fundedĪ£ ledger credits by campaignoppoints_ledgerāœ…
own.points_per_playerPoints per active accountpoints issued Ć· active accountsoppoints_ledgerāœ…
own.qualification_rateRewarded-round qualificationRounds credited Ć· roundsprvpoints_ledger, activityāœ…
own.events_acceptedIngest acceptanceAccepted Ć· received events, by reject_reasonop, prv (own integrations)activity_eventsāœ…
own.batch_latencyBatch latencyp50 / p95 of completed_at āˆ’ received_atop, prvactivity_batchesāœ…
own.attributed_playersAttributed / activated playersPlayers attributed to the affiliate; activation %affattribution modelā³
own.rewards_claimedRewards claimedpartnerrewards 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_player
  • avg_stake, payout_ratio, churn_rate, link_conversion
  • multi_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 Y
  • network: the value over R, plus network_all over N
  • index: you Ć· network Ɨ 100
  • band: where your players' median sits in the network's per-player distribution
  • network_dist: {p25, p50, p75}
  • suppressed, plus reason when it is

Per-player measures are computed per Account first and then aggregated, so the figures report medians as well as means.

IDKPIDefinition (for Y and for R)WhoSourceStatus
cmp.active_rateActive rateActive Accounts ÷ Accounts in the population at the start of Pop, prv, affplayer_active_day, party_reach_day🟔
cmp.active_daysActive days per active playerMedian / mean of distinct active days per Accountop, prvplayer_active_dayāœ…
cmp.stickinessStickinessMean DAU Ć· MAUop, prvplayer_active_dayāœ…
cmp.rounds_per_playerRounds per active playerMedian / mean of rounds per Accountop, prvanalytics_account_dayāœ…
cmp.bet_per_playerTurnover per active playerMedian / mean of bet_eur per Accountop, prvanalytics_account_dayāœ…
cmp.ggr_per_playerGGR proxy per active playerMedian / mean of (bet āˆ’ win) per Accountop, prvanalytics_account_dayāœ…
cmp.avg_stakeAverage stakeĪ£ bet_eur Ć· Ī£ roundsop, prvanalytics_account_dayāœ…
cmp.payout_ratioObserved payout ratioĪ£ win Ć· Ī£ betop, prvanalytics_account_dayāœ…
cmp.retention_d7 / _d30New-player retentionOf Accounts that joined in the cohort week (Y: linked to you; R: activated on PlayerPal), the share active again 7 / 30 days laterop, prvplayer_active_day, identity_links, players.activated_at🟔
cmp.churn_rateMonthly churnActive in Pāˆ’1 and not in P Ć· active in Pāˆ’1op, prvplayer_active_day🟔
cmp.reactivation_rateReactivationActive in P after ≄ 30 inactive days Ć· Accounts that were inactiveop, prvplayer_active_day🟔
cmp.tenurePlayer tenureMedian days from players.activated_at to the end of P, over active Accountsop, prv, affplayersāœ…
cmp.points_per_playerPoints earned per active playerMedian / mean of ledger credits per Accountoppoints_ledgerāœ…
cmp.redemption_rateRedemption ratePoints redeemed Ć· points earnedoprewards modelā³
cmp.multi_homingOperators per playerMedian / mean of active operator links per Accountop, prvidentity_linksāœ…
cmp.overlap_rateOverlapShare of Y also active with ≄ 1 other party of your type (count only, never which)op, prvparty_reach_day, activityāœ…
cmp.share_of_walletShare of your players' turnoverY'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, prvanalytics_account_*_dayāœ… (op šŸŒ)
cmp.market_mixMarket mixShare of bet_eur per market, Y vs Rop, prvactivity marketšŸŒ
cmp.link_conversionLink conversionCompleted Ć· created link_intents in Poplink_intentsāœ…
cmp.time_to_first_playTime to first playMedian hours from link completed to the first round with that partyop, prvidentity_links, activityāœ…
cmp.game_mixGame-category mixShare of bet_eur by game category, Y vs Rop, prvgame 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)​

KPISourceStatus
Signups and signup funnel by methodplayer_registrations, playersāœ…
Network DAU / WAU / MAU, stickinessplayer_active_day🟔
Network turnover, GGR, points liabilityactivity rollups, points_ledgerāœ…
Login success / failure by methoddomain_events🟔
Recovery-code use, account merges, deletionsrecovery_attempt_log, player_account_merges, domain_eventsāœ… / 🟔
Connected parties by type, integrations by statusparties, integrationsāœ…
Ingest health across all integrationsactivity_events, activity_batchesāœ…
KPI API usage per party and clientdomain_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.

GroupEvent types
Playerplayer.registered, player.activated, player.deleted
Authauth.login.succeeded (a session was issued; scope), auth.login.failed (method, reason), auth.session.revoked (actor player or staff)
Linkinglink.intent_created (led), link.completed (proof_type), link.unlinked (cascaded), alias.issued / alias.revoked (alias_type); party_id is the audience
Plannedaccount.merged, auth.method.added / auth.method.revoked, auth.recovery.used
Ingestbatch.received (events), batch.completed (state, events, rejected, latency_ms); party_id is the provider
Data accesskpi.api.accessed
Partner credentialspartner.credential.created, partner.credential.rotated, partner.credential.revoked