Skip to content

Add a new external game-provider integration

Goal: integrate a licensed external slots / live-casino provider (analogous to PM8, ST8, BGaming, Evogames). Audience: customer engineering team adding a third-party game provider to expand the catalogue. Time: 1–3 weeks. Friction: high — wallet RPC crosses services through Redis pub/sub, which breaks OTel trace propagation. See docs/audits/perf-trace-coverage-audit.md §"transport gap".

What you'll change

Layer Path Action
Provider module apps/api/src/casino/slots/providers/<provider>/ New folder. Copy from pm8/ or bgaming/.
Module wiring apps/api/src/casino/slots/slot-games.module.ts (and parent) Add new module to imports.
Auth subsystem <provider>/system/operator-auth/ HMAC, JWT, or API-key per provider docs.
Signature subsystem <provider>/system/signature/ Webhook signature verification.
Wallet RPC <provider>/wallet/ Cross-service balance / debit / credit / cancel.
Launch service <provider>/<provider>-launch.service.ts Game session creation.
Webhook controller <provider>/<provider>.controller.ts Provider-side callbacks.
Doppler / env .example.env + Doppler Provider URLs, secrets, signature keys.
Compliance flag (see notes) Whitelist provider per jurisdiction.

Canonical examples

The current providers each illustrate a different auth scheme:

  • apps/api/src/casino/slots/providers/pm8/ — operator-signed HMAC, custom signature module.
  • apps/api/src/casino/slots/providers/bgaming/ — different signature pattern.
  • apps/api/src/casino/slots/providers/evogames/ — yet another.
  • apps/api/src/casino/slots/providers/st8/ — has the most complete admin / bonus / hydrate surface.

Inspect 2–3 of these before picking the closest match to your provider's spec.

The PM8 module structure (verified at apps/api/src/casino/slots/providers/pm8/pm8.module.ts:1-15):

pm8/
├── pm8.module.ts
├── pm8.controller.ts
├── pm8-launch.service.ts
├── api/                            # outbound API client
│   └── pm8-api.module.ts
├── system/
│   ├── operator-auth/              # operator → ebit handshake
│   │   └── pm8-auth.module.ts
│   └── signature/                  # webhook signature verify
│       └── pm8-signature.module.ts
├── wallet/                         # debit / credit / balance / cancel
│   └── wallet.module.ts
└── dto/

Steps

1. Provider intake [non-engineering]

Before writing code:

  • [ ] Provider tech contact secured. NDA / integration agreement signed.
  • [ ] Provider sandbox credentials issued (separate from production).
  • [ ] Auth scheme documented: HMAC (which hash, which canonicalisation), JWT (signing key, claim shape), API key (where in headers).
  • [ ] Webhook spec: which events fire (bet, win, refund, freespin), with what payload and what signature.
  • [ ] Wallet RPC spec: what balance / debit / credit / cancel endpoints they call on us.
  • [ ] Currency support matrix per provider (mapping to our CurrencySymbol enum).
  • [ ] Jurisdictional whitelist: which markets is the provider licensed for? Coordinate with docs/security-register.md gating.

2. Add the env keys

Edit .example.env:

YOURPROVIDER_API_URL="https://sandbox.yourprovider.example/v1"
YOURPROVIDER_OPERATOR_ID=""
YOURPROVIDER_API_KEY=""
YOURPROVIDER_SIGNING_SECRET=""
# If JWT-based:
YOURPROVIDER_JWT_PUBLIC_KEY=""

Set values in Doppler for each environment.

3. Scaffold the provider module

cd ebit-api
cp -r apps/api/src/casino/slots/providers/pm8 \
      apps/api/src/casino/slots/providers/yourprovider
# rename: yourprovider.module.ts, yourprovider.controller.ts, yourprovider-launch.service.ts
# rename subfolders: yourprovider-auth.module.ts, yourprovider-signature.module.ts

Inside the new folder, replace every Pm8 with YourProvider. Keep the file structure — sister modules import via the same path conventions.

4. Implement signature verification

Open <provider>/system/signature/<provider>-signature.module.ts. The pattern:

  1. Receive raw request body + signature header.
  2. Re-compute the signature with the shared secret.
  3. Constant-time compare with crypto.timingSafeEqual.
  4. Reject (401) on mismatch — never echo the expected signature in the error.

Test both positive (valid signature accepted) and negative (mutation of body rejected) cases.

5. Implement wallet RPC [high friction]

The wallet handlers are the most-called endpoints in any external-provider integration — the provider hits them on every spin. They live under <provider>/wallet/.

Required handlers (names vary per provider):

  • balance — return the user's balance for a given currency. Read-only.
  • debit / buyin — deduct the bet amount, write a Bet row.
  • credit / payout — credit a win, update the bet's settlement.
  • cancel — refund a previously debited bet (timeout, error). Must be idempotent.

Pattern verified in ST8 (the most mature integration) at apps/api/src/casino/slots/providers/st8/:

  • Uses @bebkovan/server-core's WaitMutex to serialise per-user-per-game wallet ops.
  • Uses Prisma upsert with a unique transaction id to enforce idempotency.
  • Handles "cancel of an unknown transaction" by inserting a marker row (St8CanceledUnknownSlotTransaction) so a late-arriving original debit can be no-op'd.

These idempotency patterns are non-negotiable — providers retry on timeout, and a non-idempotent debit will double-charge users.

6. Wire the module

Add YourProviderModule to apps/api/src/casino/slots/slot-games.module.ts imports (or to the parent slots module that aggregates providers).

7. Tracing gap workaround [high friction]

The wallet RPC layer routes through ExternalControllerClient over Redis pub/sub. OTel trace context does not propagate — see project_otel_microservice_transport_gap.md and docs/engineering/observability-runbook.md §5.

Mitigations during integration:

  1. Log the provider's transaction id in every wallet handler so you can correlate by id, not by trace_id.
  2. Search Loki by user_id, not by trace_id, when chasing a wallet bug:
    {service_name="ebit-api"} |= "user_id=<id>"
    
  3. Manual span wrapping the wallet handler entry point will at least capture the local processing time:
    const tracer = trace.getTracer('slots.yourprovider.wallet');
    return tracer.startActiveSpan('yourprovider.debit', async (span) => {  });
    

Plan for this when estimating debugging time.

8. Frontend launch flow

The frontend doesn't usually need provider-specific code — the launch URL is a redirect / iframe that the backend builds. Verify the <provider>-launch.service.ts returns a launch URL that the FE can iframe.

9. Compliance / jurisdiction whitelist [non-engineering]

Coordinate with the security / compliance team:

  • [ ] Provider is licensed for each jurisdiction the customer operates in.
  • [ ] Add the provider to the jurisdiction-allowlist (location: {{TBD: confirm with security review — likely a config in libs/_prisma/src/seed/ or a runtime feature flag}}).
  • [ ] Update docs/security-register.md if the integration introduces new data-sharing flows (bet history outbound, KYC outbound, etc.).

10. Tests

npm test -- apps/api/src/casino/slots/providers/yourprovider/

Required specs: 1. Signature verify (positive + negative). 2. Wallet idempotency: replay the same debit twice, expect a single bet row. 3. Cancel-of-unknown-transaction handling. 4. Currency mapping (provider currency code → our CurrencySymbol).

11. Sandbox integration

Use the provider's sandbox to drive a real round end-to-end:

  1. Configure the provider's sandbox with the local ebit-api URL via ngrok / cloudflared.
  2. Trigger a launch from the FE (or directly via <provider>-launch.service).
  3. Watch the wallet handlers receive debitcredit (or cancel).
  4. Confirm DB rows + balance update.
  5. Repeat with retries (network drops) to exercise idempotency.

12. Update the API surface SOT

./docs/api/sync-postman.sh

Append docs/api/changelog.md entry: new endpoints under "Casino API" / "Webhooks" tag.

Verification

  1. Sandbox round trip: Provider → wallet → DB → balance update completes.
  2. Idempotency: Replay the same debit request 3 times, expect 1 Bet row.
  3. Cancel: Provider's cancel arrives, the previously debited amount is restored.
  4. Loki: search by provider_transaction_id returns the full handler chain even though the trace is fragmented.
  5. Postman: new endpoints visible under the regenerated collection.