# Design for availability

PlayerPal is an asynchronous loyalty layer that sits beside your platform, not in front of it. If every PlayerPal service were unavailable, your players could still log in, launch games, place bets, have rounds settled and be paid. This page explains the rule and the outbox pattern that keeps it true in your integration.

## The rule

[Available]

:::info[The rule]
**Login, game launch, wagering, settlement and payouts never call PlayerPal and never wait for it.** Everything PlayerPal needs reaches it later, in batches, from your own outbox.
:::

- **No PlayerPal call before or during game launch.** The operator creates the launch session with the game provider directly.
- **No PlayerPal call per wager or round.** The operator's wallet stays authoritative and the game provider owns the round.
- **The alias travels with the launch.** A game provider receives its PlayerPal player alias (`pp_plr_prv_…`) in the operator's launch metadata. PlayerPal issues it when the player links, so nothing is looked up at launch.
- **Activity is sent later, in batches.** Write it to a local outbox and deliver it asynchronously.
- **The widget is optional and fails open.** If it cannot load, play is unaffected.

## What depends on PlayerPal

| Moment | Calls PlayerPal? | Needed for play? | If PlayerPal is unavailable |
|---|---|---|---|
| Login and game launch | No | No | No impact |
| Each wager, settlement or payout | No | No | No impact; activity stays in your outbox |
| Batch delivery | One call per batch | No | Keep the batch and retry with backoff |
| Player links an account | Yes | No | Show "unavailable"; games stay usable |
| Widget or status display | Optional read | No | Show a neutral or unavailable state |
| Viewing or redeeming Points | Yes | No | Not available until PlayerPal recovers |

## Recommended outbox pattern

[Available]

Record eligible activity in your own database and let a background worker deliver it. The worker is the only part of your system that talks to PlayerPal.

```mermaid
sequenceDiagram
    participant P as Player
    participant G as Your platform
    participant DB as Your database
    participant W as Outbox worker
    participant PP as PlayerPal API

    P->>G: Place bet
    G->>DB: Settle round + write outbox row (one transaction)
    G-->>P: Result and payout
    Note over G,PP: Play never waits for PlayerPal
    loop Every few seconds
        W->>DB: Read queued rows, group into a batch with a stable batch_id
        W->>PP: POST batch (same batch_id on every attempt)
        alt 202 Accepted
            PP-->>W: Receipt
            W->>DB: Mark batch delivered
        else Timeout, network error or 5xx
            W->>W: Back off with jitter, retry the identical batch
        end
    end
```

Guidelines:

1. **Write the outbox row in the same database transaction as settlement.** Then a crash can never settle a round without recording its activity, or the reverse.
2. **Assign the `batch_id` once,** when you form the batch, and store it. Reuse it on every retry so PlayerPal can deduplicate.
3. **Retry until delivered.** Use exponential backoff with jitter and no fixed give-up point. Alert if a batch has been pending for longer than you expect.
4. **Survive restarts.** On startup, resume any batch that was pending or retrying.
5. **Do not let one stuck batch block newer ones.** Keep delivering fresh batches while an older one retries.
6. **Send many events per call.** One batch of hundreds or thousands of events is better than one call per round.
7. **Cache tokens per worker,** not per player, and refresh before expiry.
8. **Check a batch only after an ambiguous response,** using its status endpoint.

### What an outbox record holds

[Available]

Keep each event exactly as you will send it, plus two delivery fields: the `batch_id` it was assigned to (empty until a batch is formed) and its delivery state. The shape below is an example; use whatever storage you already run.

Outbox record (example):

```
{
  "event_id": "round-8812-settled",
  "type": "round_settled",
  "player_id": "pp_plr_prv_XXXX",
  "occurred_at": "2026-10-15T12:03:11Z",
  "game_ref": "your-game-ref",
  "round_ref": "round-8812",
  "currency": "EUR",
  "bet": "2.00",
  "win": "0.00",

  "batch_id": "rounds-2026-10-15-0001",
  "state": "pending"
}
```

| `state` | Meaning |
|---|---|
| `queued` | Written with settlement, not yet in a batch |
| `pending` | Assigned to a batch that is being delivered or retried |
| `accepted` | PlayerPal returned a receipt for its batch |

The event fields match an [activity batch](./sending-activity.mdx). Store the batch itself too, with its `batch_id`, its event IDs and an attempt count, so a restarted worker can resume it unchanged.

## During an outage

[Available]

- **Play continues.** Nothing in login, launch, wagering, settlement or payout calls PlayerPal.
- **Points arrive late, not differently.** Eligibility is based on when the activity happened (`occurred_at`), not when it reached PlayerPal. Activity that happened before a player unlinked still counts when it arrives later.
- **Analytics catch up.** KPIs reflect delayed batches once they are delivered and processed.
- **Your data is safe while it stays in your outbox.** Delivery protection comes from your durable outbox and the reuse of the same `batch_id`.

:::note
PlayerPal does not yet publish availability or freshness commitments. Contact PlayerPal for current targets.
:::

## Ways to deliver data

| Endpoint | Use |
|---|---|
| `POST /v1/activity-batches` | Batches of settled-round activity that feed Points |
| `POST /v1/activity-uploads`, then `.../complete` | Large activity files through a signed upload URL |
| `POST /v1/ingest` | Daily pseudonymous facts that feed KPIs, up to 10,000 records per batch |

All three accept at-least-once delivery. See [Idempotency and errors](./idempotency-and-errors.mdx).

## Availability checklist

1. Login, launch, wagering, settlement and payouts complete with PlayerPal blocked at your network edge.
2. The outbox record is written in the same transaction as settlement.
3. Each batch gets its `batch_id` once, and every retry resends the identical batch.
4. Retries use exponential backoff with jitter, and you alert on batches pending longer than you expect.
5. A restarted worker resumes pending batches, and a stuck batch does not hold back newer ones.
6. Link and widget screens show a neutral or unavailable state instead of an error when PlayerPal does not answer.

Block PlayerPal, play a few rounds and restart your worker. Play is unaffected, the rows stay queued or pending, and once you unblock PlayerPal every batch is accepted exactly once.

## Related

  - [Send activity](./sending-activity.mdx): 
    Batch format and upload flow for activity.
  
  - [Idempotency and errors](./idempotency-and-errors.mdx): 
    What is deduplicated and how to retry.
  
  - [How it works](../product/how-it-works.mdx): 
    Where PlayerPal sits next to your platform.
