Ingestion and availability
PlayerPal is an asynchronous loyalty sidecar. Gameplay must not depend on it.
Long-form: spec §12, §34-§41. Decisions: ADR 0004, 0005, 0006, 0008, 0015.
Failure rule
If every PlayerPal service is unavailable, the casino and provider must still authenticate the player, launch the game, accept bets, settle rounds and pay winnings.
Player opens game → Operator launch → Provider game → Bet and settle
↓ (async, retry)
Provider outbox / batch file
↓
PlayerPal ingestion → loyalty / analytics
No per-player session creation, per-launch resolution, per-round token request or synchronous loyalty calculation.
Call budget (minimum provider integration)
- Receive an already-provisioned
pp_plr_prv_...in the operator's launch metadata. - Write eligible activity to the provider's existing outbox in the same local transaction as settlement where possible.
- Cache a machine token (refresh typically every 5-15 minutes per worker, not per player).
- Send one compressed batch of hundreds or thousands of events.
- Poll batch status only after an ambiguous response.
Control plane vs data plane
Ingestion authenticates, validates a minimal envelope, appends durably, and responds 202. Normalization, campaign evaluation, ledger updates and analytics run asynchronously off that durable log (ADR 0004). Real-time and batch paths share one event model and one reward engine.
202 means durably accepted, not loyalty-complete. Ingest is integration-scoped (x-integration-id). Provider studio reads are a different plane: provider-Party scoped (x-party-id), numbers only (ADR 0026, loyalty.md).
Idempotency
At-least-once delivery, idempotent processing (ADR 0008). Uniqueness (spec §38.1):
batch uniqueness: (integration_id, batch_id)
event uniqueness: (integration_id, event_id)
ledger uniqueness: (campaign_rule_version_id, activity_event_id, ledger_action)
An identical retry returns the original receipt. A reused ID with a different checksum returns 409 idempotency_conflict.
Scope for events comes from the authenticated integration, not from the payload's operator/provider fields (ADR 0014).
Batch delivery
Small/medium batches: POST /v1/activity-batches. Large exports: POST /v1/activity-uploads returns a short-lived signed PUT URL; the partner writes NDJSON, NDJSON.gz, JSON, or CSV to object storage, then POST /v1/activity-uploads/{uploadId}/complete with checksum, schema version, and client batch_id (spec §37.7, ADR 0006). Processing starts only after the stored object matches the checksum. The complete path then uses the same Activity → Points pipeline as inline batches.
Local MinIO is the S3-compatible store (quay.io/minio/minio, pinned by digest in compose). Preconditions: compose minio + minio-init (bucket playerpal-activity), ACTIVITY_S3_* in infra/.env (see .env.example). Host API uses http://localhost:9000; the compose API service uses http://minio:9000 internally and signs PUTs for http://localhost:9000. Verify: .cursor/skills/verify-playerpal/features/activity-uploads.md. Parquet is recognised and rejected (format_unsupported) until a dedicated decoder is added.
Event log phasing
Phase 1: PostgreSQL inbox (or object storage) and workers. Phase 2: Redpanda/Kafka-compatible log when measured throughput, extra consumers, or replay needs justify it (ADR 0005, spec §35.4). Do not guess a date.
Suggested SLOs (not contractual until load-tested)
| Capability | Objective |
|---|---|
| Game launch/wager dependence on PlayerPal | None |
| Link and account APIs | 99.9% monthly |
| Batch acceptance API | 99.95% monthly |
202 latency for a normal batch | p95 under 500 ms after body received |
| Activity-to-ledger freshness | p95 under 5 minutes |
| Duplicate ledger effects | Zero by design |
Interactive/core and ingestion/processing scale independently (spec §41). Token validation uses cached JWKS. Ingestion does not synchronously call IAM, campaign admin or analytics.
Integration profiles (provider batch, operator-submitted activity, object-file, optional overlay): spec §40.