Game Service
Status
Active
Date
2026-04-28
Owners
- Platform Backend
Last Verified Commit
56362a7a
Runtime
API only
Purpose
game_service owns provider callback handling and game-integration-specific logic.
It keeps provider-specific authentication and callback semantics isolated from the
rest of the platform.
Primary Entry Points
/integration/*/HO/*/mg/*/wc/*/bti/*/splus/*/bt1/*/digitain/*
Dependencies
- PostgreSQL
- Redis
wallet_service- provider-specific credentials and callback config
Internal coordination:
- outbound wallet-owner calls use per-caller internal auth
(
X-Caller-Service: gameplusINTERNAL_SERVICE_TOKEN_GAME) - inbound
POST /integration/launchis a trusted backing route forgatewayonly. The gateway forwards the resolvedX-Brand-Id, injects the JWT-verifiedplayer_id, and signs the service-to-service hop withX-Internal-Service-Token/X-Caller-Service: gateway; account-only or direct same-network launch callers are rejected before provider URLs are minted.
Background Work
None documented as a standalone worker today.
Owned Data
- provider callback transaction state
- provider integration metadata and sync behavior local to game integrations
Events
Emits:
- no first-class Redis stream ownership is currently documented
Consumes:
BRAND_CATALOG_CHANGEDRedis pub/sub for brand-code reverse-parse cache invalidationBRAND_PROVIDER_CHANGEDRedis pub/sub for per-brand provider allow-list cache invalidation
Health
- API
/healthchecks DB and Redis
Key Env Vars
DATABASE_URLREDIS_URLSECRET_KEYWALLET_SERVICE_URLMULTI_BRAND_ENFORCEMENT— required; one ofoff/observe/enforce. Production target isenforcepost-Phase-16. Drivesmulti_brand_enforcement_mode{service="game_service"}gauge and the brand-aware vs raw-fallback callback resolution decision (seecallback_brand.resolve_callback_brand).BRAND_SIGNING_KEY— required in production; HMAC-SHA256 secret used to signX-Brand-Signatureon outbound brand-scoped wallet writes from callback handlers (game is one of the 6 wallet write callers).INTERNAL_SERVICE_TOKEN_GATEWAY— required in production on the consumer side; per-caller token accepted from gateway for protected backing routes such as/integration/launchoncePER_CALLER_TOKEN_REQUIRED=on.INTERNAL_SERVICE_TOKEN_GAME— required in production; per-caller token presented to wallet when game callbacks drive settle/credit/rollback.PER_CALLER_TOKEN_REQUIRED—onis the Phase 16 target on the consumer side; game consults it for inbound protected backing routes and downstream services consult it when game is the caller.INTERNAL_SERVICE_TOKEN— legacy single-shared-token; deprecated. Phase 16 release gate requires the bare variant to be absent.VERIFY_CALLBACKS— provider-callback signature verification toggle. MUST betruein production. Default istrue(safe-by-default). The/HO/,/mg/,/wc/paths are listed in gatewayPUBLIC_PATH_PREFIXES; with this set tofalsethe public internet can POST arbitrary settle/credit/rollback payloads to wallet_service. The boot-time guard inapp/core/boot_guards.py::assert_verify_callbacks_safe_for_runtimerefuses to start game_service when the runtime env resolves to a production-grade label (production/prod/staging) andVERIFY_CALLBACKS=false. The runtime bypass branch in each handler also incrementsgame_callback_verification_bypassed_total{provider}so any non-prod accidental enabling stays observable.- provider credentials such as:
HO_*WC_*BTI_*BT1_URLSPLUS_URLDIGITAIN_*MG_*
Multi-Brand Constraints
Per ADR-009:
- game provider credentials and integration secrets stay global; one set per provider serves every brand
- provider availability is brand-scoped through
brand_provider_config. If a brand has no rows, it inherits the globalprovider.is_showcatalog for backward compatibility. Once any policy rows exist,/integration/providers,/integration/games,/integration/top, and/integration/launchonly expose or launch providers enabled for that brand and still globally visible throughprovider.is_show - outbound calls to providers use a brand-namespaced account:
{brand_code}_{account}(or the documented per-provider equivalent for providers that constrain account format) - inbound provider callbacks reverse-parse the namespaced account to recover
brand_idand player account, and route the resulting bet authorization, settlement, or rollback into the correct brand - provider callback transaction state and provider-specific records carry
brand_id - a callback whose namespaced account cannot be reverse-parsed is rejected and logged with provider context but no brand assumption (this rejection is unconditional and not gated by
MULTI_BRAND_ENFORCEMENTbecause there is no safe fallback brand for an unparseable callback); the rejected callback is written togame_callback_dead_letterfor later replay after the brand catalog is fixed - the reverse-parse cache holds
brand_code -> brand_idper process; entries refresh on a 60-second TTL AND are invalidated by theBRAND_CATALOG_CHANGEDRedis pub/sub channel published byadmin_serviceafter every brand create / disable. Eachgame_serviceprocess subscribes on startup; processes that miss a message refresh on TTL expiry within 60 seconds
Outbound account length audit
The platform's brand_code is capped at 16 characters and the player
account at 32 characters, so the worst-case namespaced account
({brand_code}_{account}) is 16 + 1 + 32 = 49 characters. The audit
below documents whether each currently integrated provider accepts a
49-character account; providers that don't get a per-provider fallback.
The provider columns marked unknown are not constrained in the local
schema and the published spec sheets are not version-pinned in this repo;
they are documented under the conservative assumption that 49 characters
fits, which matches the provider integrations actually wired today
(none truncate or reject the legacy account on outbound).
| Provider | Account-field cap | Worst-case 49-char namespaced account fits? | Fallback if not |
|---|---|---|---|
| HO | unknown — provider spec accepts arbitrary username strings; current integration sends account as cw[@uname] (XML attribute) without an enforced cap | yes | (none required) |
| MG | unknown — provider spec accepts opaque playerId strings; integration sends account as playerId (string field) without an enforced cap | yes | (none required) |
| WC | unknown — provider spec accepts member_id opaque strings; integration sends account as member_id and stores it as the wc_account.account PRIMARY KEY (String(255)) | yes | (none required) |
| BTI | unknown — JWT-embedded account claim, no provider-side schema cap; the embedded JWT is opaque to BTI | yes | (none required) |
| BT1 | unknown — same protocol family as BTI | yes | (none required) |
| SPLUS | unknown — same protocol family as BTI | yes | (none required) |
| Digitain | unknown — JWT-embedded account claim, sent in user_id field of launch URL; provider spec does not document a cap | yes | (none required) |
| Integration (launch URL) | n/a — internal launch URL builder | yes | (none required) |
If a future provider is added with a stricter cap, document the
per-provider fallback (suggested: sha256(brand_code + "_" + account)[:N]
hash projection, with the resulting hash recorded alongside the original
account in a side table for reverse lookup) and update the row above
with the resolved implementation: replace the yes / no fits column
with the projection algorithm and link to the side-table migration.
Do NOT leave a literal todo-marker string here — the existence of this
guidance paragraph already serves that role, and a literal marker
would register as outstanding debt in repo-wide scans.
Tests
cd servers_v2/game_service && uv run pytest- key suites:
tests/test_legacy_contracts.py