# Loyalty

The private player owns loyalty and analytical facts. External aliases are access handles, not separate wallets.

Long-form: spec §18, §33, §38. Decisions: ADR 0007, 0008, 0009, [0026](decisions/0026-analytics-subject-and-provider-cohort.md).

## Authority

PlayerPal calculates points. Providers report factual activity; they do not calculate the PlayerPal reward. The append-only points ledger is authoritative. Cached balances and projections are derived views.

The live ledger may reference the internal player UUID (`players.id`) in memory. Partner-facing analytics do not: they are Audience-scoped numbers, never UUID, `pp_plr_...`, another Audience’s alias, or the analytics subject.

## Phase 1 demo earn path

- Partners (or `POST /demo/activity-batches` when `DEMO_PARTNER_HELPER=1`) submit Activity facts; Nest awards Points.
- Fixed campaign version `pp_cmp_BET_EUR_V1`: **1 Point per €1 bet**, EUR only, `round_settled` events for linked `pp_plr_prv_…` aliases.
- Machine ingest requires `x-api-key` and `x-integration-id` (no default audience). ACK is `202`.
- StarBucks is owned by the NetEnt provider iframe (`:5175`), which receives
  only the NetEnt×Bucks (`pp_int_NETENT_BUCKS`) alias and batches settled rounds
  through a retrying demo outbox. Big Bass → Pragmatic×Bucks remains distinct.

## Eligibility

- Earning starts only after explicit registration/activation. No pre-registration shadow profiles. No historical points by default (spec §33). Spec §18's "pre-enrolment accrual" option is not current product policy.
- An activity event is eligible only when `occurred_at >= connection.effective_from` and before Unlink revocation (`unlinked_at`; spec §36.6 `effective_at`).
- Unlink stops qualifying later Activity for that Audience; Points already on the ledger stay. Delete Account erases the identity graph and does not confiscate retained gameplay facts (no identity keys).
- A cookie expiring does not stop asynchronous earning once the server-side link exists.

## Campaigns

Campaign configuration is versioned and snapshotted (ADR 0007). `reward_event` references `campaign_version_id` (or rule version), never a mutable campaign row. Past decisions stay explainable after edits.

The domain model is generic (campaign → eligibility → trigger → reward → limits → schedule → audience), not casino-only.

## Ledger

Raw activity is immutable. Corrections are adjustments, reversals or compensating events, not mutations. Merge of two player accounts does not rewrite history; it applies programme-specific reconciliation and compensating entries (ADR 0019).

Idempotency on the reward side: `UNIQUE (campaign_rule_version_id, activity_event_id, ledger_action)` (spec §38.1).

## Analytics

Processing Activity upserts daily serving rows. Partner studio HTTP (`GET /v1/analytics/studio`, `GET /v1/analytics/studio/aliases/:alias`) is provider-Party scoped. The provider comes from the partner principal: an OAuth client or a PP-HMAC-SHA256 key with scope `studio.read` ([api/hmac-signing.md](api/hmac-signing.md)). The legacy shared machine key with `x-party-id` (`pp_prv_...`) is still accepted while `STUDIO_LEGACY_PARTY_HEADER` is not `false`, and must be turned off in production. Ingest stays integration-scoped (`x-integration-id`). Scope never comes from a payload `operator_id`.

Per-alias lookup uses only a `pp_plr_prv_...` that provider was issued. Two aliases for the same Account at that provider return the same Account-level numbers. A foreign alias is the same unknown as a missing alias. After Delete Account, live per-alias GET is unknown.

“On you” is all of that provider’s integrations for the Account that period, not one casino. Response fields (numbers only): `from`, `to`, `bets`, `wins`, `bet_eur`, `share_of_their_slot_turnover`, `rounds_on_you`, `rounds_elsewhere`. Studio rollup adds `accounts` (distinct Accounts, not a sum of per-alias rows) and suppresses windows below a configurable Account floor (production default 50).

A provider-global **cohort** is allowed; a provider-global **alias** is not (ADR 0014, ADR 0026).

Each Activity event may carry a licensed `market` (ISO 3166-1 alpha-2). It is checked against the integration's declared `markets`, and events outside them are rejected with `market_not_licensed`. Market feeds partner KPIs and anonymous comparisons; see [kpis.md](kpis.md) and ADR 0033.
