Skip to content

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.

EndpointAuth
PUT /internal/engines/desired-statex-internal-control-secret
GET /internal/enginesnone (diagnostic)
GET /internal/engines/:engineId/statusx-internal-control-secret
GET /healthnone
POST /internal/engines/:engineId/pause · /resumex-internal-control-secret

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/json
x-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 }
]
}
]
}
FieldDescription
protocolVersionCGSP 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[].engineTypeThe owning service’s service_uid; must match the top-level serviceUid.
engines[].runtimeProfileIdUsed to fetch config via GET /internal/runtime-profiles/:id.
engines[].canonicalGameExternalIdThe smallest gameId in the bucket; used on ledger calls.
engines[].serviceUidSame as the top-level value (repeated per entry); prepended to bet UIDs.
engines[].memberGames[]The games under this engine.
memberGames[].gameExternalIdThe game’s external ID.
memberGames[].nameDisplay name (for logs).
memberGames[].skinKeySkin identifier.
memberGames[].suspendedWhen 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 missing x-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.

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).

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.

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):

  1. Persist (engineId, paused_at, paused_by_admin_id, pause_reason) to the service DB (canonical table engine_pause_state, keyed by engine_id).
  2. Move the engine running → pausing. Let the active round finish — do not cut it off.
  3. When the round ends (or immediately if none), the engine becomes paused.
  4. While paused: start no new round, and do not call round-start-check for this engine.
  5. On restart: read the persisted pause state and instantiate the engine as paused (emit no rounds). It returns to running only on the next /resume.

Response (200): { "ok": true, "status": "pausing" } (or "paused" when there is no active round).

Request body:

{ "adminId": 17 }

Service semantics (MUST):

  1. Delete the persisted pause row for this engineId.
  2. Move the engine paused → running.
  3. Return to the normal round loop (round-start-check resumes like any round).

Response (200): { "ok": true, "status": "running" }.

  • 403 Forbidden — missing / wrong x-internal-control-secret.
  • 404 Not Found — the engineId is 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.