Control Plane
The control plane is Surface 1: Platform → Service over HTTP. These are the endpoints the game service hosts and the platform calls into.
| Endpoint | Auth |
|---|---|
PUT /internal/engines/desired-state | x-internal-control-secret |
GET /internal/engines | none (diagnostic) |
GET /internal/engines/:engineId/status | x-internal-control-secret |
GET /health | none |
POST /internal/engines/:engineId/pause · /resume | x-internal-control-secret |
PUT /internal/engines/desired-state
Section titled “PUT /internal/engines/desired-state”The most important endpoint. The platform states which engines should be running. It is idempotent: every call describes the full desired set, and the service reconciles its actual state toward it.
Headers:
Content-Type: application/jsonx-internal-control-secret: <secret>Request body:
{ "protocolVersion": "1.1", "serviceUid": "wp9dvdgp4wfqmxq4", "engines": [ { "engineId": "wp9dvdgp4wfqmxq4:p:42", "engineType": "wp9dvdgp4wfqmxq4", "runtimeProfileId": 42, "canonicalGameExternalId": 9000, "serviceUid": "wp9dvdgp4wfqmxq4", "memberGames": [ { "gameExternalId": 9000, "name": "Hi-Lo Classic", "skinKey": "default", "suspended": false }, { "gameExternalId": 9001, "name": "Hi-Lo Express", "skinKey": "neon", "suspended": true } ] } ]}| Field | Description |
|---|---|
protocolVersion | CGSP version ("1.1"). An unknown major version must be rejected (see versioning). |
serviceUid (top-level) | The target service’s service_uid. Stored from the first valid push (self-discovery); a later mismatch is drift — reject and warn. |
engines[] | The full set of engines that should be running. |
engines[].engineId | {serviceUid}:{bucketKey} (bucketKey = p:{runtimeProfileId} or g:{gameExternalId}). |
engines[].engineType | The owning service’s service_uid; must match the top-level serviceUid. |
engines[].runtimeProfileId | Used to fetch config via GET /internal/runtime-profiles/:id. |
engines[].canonicalGameExternalId | The smallest gameId in the bucket; used on ledger calls. |
engines[].serviceUid | Same as the top-level value (repeated per entry); prepended to bet UIDs. |
engines[].memberGames[] | The games under this engine. |
memberGames[].gameExternalId | The game’s external ID. |
memberGames[].name | Display name (for logs). |
memberGames[].skinKey | Skin identifier. |
memberGames[].suspended | When true, the game is suspended; show players “temporarily unavailable”. |
Reconcile logic (the service implements an idempotent reconcile):
1. For each engine in the payload: - not present locally → create + start it. - present locally → update its state (memberGames may change).2. For each local engine: - not in the payload → stop it, drop in-memory state.3. memberGames[].suspended === true: - broadcast to players (the engine keeps running; only betting closes).Response (200):
{ "ok": true, "applied": { "created": 1, "updated": 0, "removed": 0 } }Errors:
401 Unauthorized— wrong or missingx-internal-control-secret.400 Bad Request— malformed payload; reject the whole payload, do not apply partially. The platform re-sends on the next heartbeat.5xx— a service-side error; log and return 5xx. The platform retries after 60 seconds.
Heartbeat: the platform calls this endpoint for two reasons — edge-triggered (an admin added/removed a profile, suspended a game, …) and level-triggered (every 60 seconds, re-sending the last valid state). The same payload arrives repeatedly, so the reconcile must be idempotent — otherwise engines are torn down and recreated every 60 seconds.
GET /internal/engines
Section titled “GET /internal/engines”A diagnostic endpoint listing the engines currently held in memory. Used by the admin and for troubleshooting. No auth (assumed to be on an internal network).
Response (200):
{ "protocolVersion": "1.1", "engines": [ { "engineId": "wp9dvdgp4wfqmxq4:p:42", "engineType": "wp9dvdgp4wfqmxq4", "runtimeProfileId": 42, "memberGameCount": 2, "status": "running" } ]}Status values: running, starting, paused, pausing, stopping,
error (pausing added in v1.1).
GET /internal/engines/:engineId/status
Section titled “GET /internal/engines/:engineId/status”Detail for a single engine. :engineId is URL-encoded (for example
wp9dvdgp4wfqmxq4%3Ap%3A42, because : is reserved).
Headers: x-internal-control-secret: <secret>
Response (200):
{ "engineId": "wp9dvdgp4wfqmxq4:p:42", "status": "running", "currentRoundId": 1234, "memberGameCount": 2, "activeBetCount": 5, "connectedClientCount": 12, "uptimeSeconds": 3600, "lastReconcileAt": "2026-05-18T10:00:00Z"}Only engineId and status are required; the rest are optional (they make the
admin dashboard richer). A 404 makes the platform treat the engine as absent.
GET /health
Section titled “GET /health”A liveness check. No auth. The engines map must be returned (an empty object if there are none) — the admin LiveOps panel reads engine status here.
Response (200):
{ "ok": true, "service": "your-service-name", "engines": { "p:42": "running", "p:43": "paused" }}In the engines map, the key is the bucketKey (p:N or g:N) and the value
is the status string. Returning ok: false or HTTP 5xx marks the service
unhealthy, and the platform stops sending traffic.
POST /internal/engines/:engineId/pause and /resume
Section titled “POST /internal/engines/:engineId/pause and /resume”(v1.1) Admin-triggered per-engine lifecycle actions. The resulting state is
owned by the service: the pause state lives in the service’s own database;
the platform only triggers the action and reads actual state back via
GET /internal/engines.
Headers: x-internal-control-secret: <secret>. :engineId is URL-encoded.
Request body:
{ "reason": "Runtime profile refresh", "adminId": 17 }reason(optional) — free-text reason; the service persists it for audit. Not shown to the player.adminId(optional) — the admin user ID that triggered the action; persisted for audit. Opaque on the wire.
Service semantics (MUST):
- Persist
(engineId, paused_at, paused_by_admin_id, pause_reason)to the service DB (canonical tableengine_pause_state, keyed byengine_id). - Move the engine
running → pausing. Let the active round finish — do not cut it off. - When the round ends (or immediately if none), the engine becomes
paused. - While
paused: start no new round, and do not callround-start-checkfor this engine. - On restart: read the persisted pause state and instantiate the engine as
paused(emit no rounds). It returns torunningonly on the next/resume.
Response (200): { "ok": true, "status": "pausing" } (or "paused" when
there is no active round).
Resume
Section titled “Resume”Request body:
{ "adminId": 17 }Service semantics (MUST):
- Delete the persisted pause row for this
engineId. - Move the engine
paused → running. - Return to the normal round loop (
round-start-checkresumes like any round).
Response (200): { "ok": true, "status": "running" }.
Failure modes (both endpoints)
Section titled “Failure modes (both endpoints)”403 Forbidden— missing / wrongx-internal-control-secret.404 Not Found— theengineIdis not currently registered (for example desired-state has not delivered it yet).409 Conflict— pausing an already-paused engine, or resuming an already-running one. The service returns the current status in the body so the platform can re-sync without retrying.5xx— a service-internal error (DB write failure, …). The action is idempotent on retry.