Config + Ledger
Config + Ledger is Surface 2: Service → Platform over HTTP. These are platform endpoints that the game service calls as a client. Auth depends on the endpoint:
| Endpoint | Header |
|---|---|
GET /internal/runtime-profiles/:id | x-internal-config-secret |
GET /internal/bet-templates | x-internal-config-secret |
POST /internal/consolidated/tx | x-internal-wallet-secret |
POST /internal/consolidated/tx/settle | x-internal-wallet-secret |
POST /internal/engines/round-start-check | x-internal-control-secret |
GET /internal/runtime-profiles/:id
Section titled “GET /internal/runtime-profiles/:id”The full contents of a runtime profile. The service calls it when bringing an engine up or each round, and caches the result (so a temporarily-unreachable platform does not stall rounds).
This is distinct from the desired-state heartbeat: desired-state is pushed to
the service every 60s (“which engines should run”); runtime-profiles/:id is
pulled by the service at the frequency of its own round logic.
Headers: x-internal-config-secret: <secret>
Response (200):
{ "ok": true, "protocolVersion": "1.1", "profile": { "id": 4, "name": "Default", "service_id": 3, "bot_min_count": 3, "bot_max_count": 7, "bot_bet_currency": "FUN", "demo_currency": "FUN", "demo_start_balance": 10000, "save_bot_bets": 1, "save_demo_bets": 0, "config_json": { "rtpTarget": 0.96, "maxCards": 5, "bettingMs": 6000 } }, "botCurrencies": { "source": "default_mix", "codes": ["AZN", "BRL", "EUR", "GBP", "USD"], "fallback": "FUN", "reportingCurrency": "EUR", "ratesToReporting": { "USD": 0.92, "BRL": 0.18, "GBP": 1.17 }, "asOf": 1716800000000 }}Top level:
| Field | Description |
|---|---|
ok | true on success. On error: { ok: false, message: "..." }. |
protocolVersion | Currently "1.1". v1.0 services ignore it. |
profile | The runtime-profile row, as raw snake_case DB columns. |
botCurrencies | Platform-managed currency mix for bot bets. |
profile fields (DB column → JSON snake_case):
| Field | Description |
|---|---|
id, name, service_id | Record metadata. |
bot_min_count, bot_max_count | Range of bots to seat per round (fake players that make a round feel live). |
bot_bet_currency | The currency bots bet in (usually FUN; with a mix, chosen from botCurrencies). |
demo_currency, demo_start_balance, save_demo_bets | Demo-mode behavior. save_* are SMALLINT (0/1) in the DB, integers in JSON. |
save_bot_bets | Whether bot bets are written to the DB. |
config_json | Engine-opaque config. Undefined to the platform; the keys are defined and validated by the service (Cloud1x2’s convention is camelCase). The platform only stores and returns it. |
botCurrencies fields:
| Field | Description |
|---|---|
source | How the list was computed (default_mix = currencies flagged in_default_mix). |
codes | Currency codes a bot may pick (uppercase, 3-letter). |
fallback | Used when codes is empty / selection fails (FUN). |
reportingCurrency | The currency the platform aggregates into. |
ratesToReporting | Each code → reporting-currency rate (FX). |
asOf | Epoch-ms timestamp of the latest rate snapshot. |
Naming note: profile carries snake_case (a raw DB row, pre-CGSP schema);
config_json keys are camelCase (engine-owned); botCurrencies is camelCase
(the CGSP wire convention).
Errors: 401 (wrong config secret), 404 (no profile with this id).
POST /internal/consolidated/tx
Section titled “POST /internal/consolidated/tx”Opens a ledger record when a bet is placed. The record is written to the
platform’s transactions table and is the single source of truth.
Idempotent on (engineType, serviceBetUid) — re-posting the same pair
returns the existing record.
Headers: x-internal-wallet-secret: <secret>
Minimum body:
{ "engineType": "wp9dvdgp4wfqmxq4", "serviceBetUid": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "gameExternalId": 9000, "status": "open"}Full body (production):
{ "engineType": "wp9dvdgp4wfqmxq4", "serviceBetUid": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "gameExternalId": 9000, "partnerId": 1, "operatorId": "cx-12", "playerToken": "d315fa12770e65eb4af38c589aed0d", "customer": "3773", "currency": "EUR", "betAmount": 10.00, "winAmount": 0, "reportingCurrency": "EUR", "rateToReporting": 1.0, "debitTrxId": "debit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "creditTrxId": null, "rollbackTrxId": null, "status": "open", "isDemo": false, "walletB2bLog": { "kind": "debit", "entry": { "request": {}, "response": {}, "requestTimestamp": "...", "responseTimestamp": "..." } }}| Field | Description |
|---|---|
engineType | The service’s service_uid (the wire identity). |
engineType + serviceBetUid | The idempotency key. Re-posting returns the existing record. |
gameExternalId | The bet’s game (canonicalGameExternalId, or the real game in the bucket). |
partnerId, operatorId | The partner/operator side (from verify-launch). |
playerToken, customer | Player identity (from the operator). |
currency, betAmount | The bet’s currency and stake. |
winAmount | 0 at open; updated on settle. |
reportingCurrency, rateToReporting | Reporting normalization; usually equal to currency. |
debitTrxId | The trxId from the wallet debit. |
creditTrxId, rollbackTrxId | null at open. |
status | A CHECK-constraint enum: 'open' | 'won' | 'lost' | 'cancelled' | 'rolled_back'. 'open' at open. Any other string → 500 (DB constraint). |
isDemo | Whether this is a demo bet. |
walletB2bLog | Optional wallet-call audit log (request + response); appended to transactions.wallet_api_log. |
Response (200):
{ "ok": true, "id": 12345 }id is the transactions bigint primary key.
Errors: 400 (missing engineType/serviceBetUid), 401 (wrong secret),
500 (DB error — retry after a few seconds).
Demo mode: with isDemo: true the record is still written and flagged
is_demo; if save_demo_bets is false on the profile, the platform skips it
(still returning 200).
POST /internal/consolidated/tx/settle
Section titled “POST /internal/consolidated/tx/settle”Updates the ledger record when a bet settles.
Headers: x-internal-wallet-secret: <secret>
Request body:
{ "engineType": "wp9dvdgp4wfqmxq4", "serviceBetUid": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "status": "won", "winAmount": 19.50, "creditTrxId": "credit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "rollbackTrxId": null, "walletB2bLog": { "kind": "credit", "entry": { "request": {}, "response": {}, "requestTimestamp": "...", "responseTimestamp": "..." } }}| Field | Description |
|---|---|
engineType + serviceBetUid | The same pair as open; locates the record. |
status | A terminal value: 'won' | 'lost' | 'cancelled' | 'rolled_back' (note the underscore — 'rolled_back', not 'rollback'). |
winAmount | The win (0 on a loss). |
creditTrxId | The credit trxId. A loss also calls credit (amount=0) to keep the debit/credit pair symmetric for operator reconciliation — so creditTrxId is set even on a loss, with winAmount: 0. |
rollbackTrxId | Set if rolled back. |
walletB2bLog | The credit/rollback audit log. |
Response (200): { "ok": true }.
Idempotent: settling the same bet twice makes the second call a no-op — safe when a round crashes and retries.
POST /internal/engines/round-start-check
Section titled “POST /internal/engines/round-start-check”(v1.1) A per-round pull of the platform’s suspend lists. The service calls it
at the start of each round, before broadcasting round-start, and applies the
returned lists at round end (so an operator-suspend decision lands on a round
boundary). A paused engine does not call it (it has no round to gate).
Headers: x-internal-control-secret: <secret>
Request body:
{ "serviceUid": "wp9dvdgp4wfqmxq4", "engineId": "wp9dvdgp4wfqmxq4:p:42"}serviceUidmust equal the caller’s UID (as received indesired-state.serviceUid); a mismatch returns403so a misconfigured service cannot query another service’s engines.engineIdmust be one previously delivered via desired-state; an unknown engine returns404(likely a race — the engine was just removed), which the service treats as “do not start a new round” and trusts the next heartbeat.
Response (200):
{ "suspendedPartners": [1001, 1002], "suspendedOperators": [ { "partnerId": 1, "operatorId": "op1" } ]}suspendedPartners—organizationIdvalues; active players whose partner is in this list are disconnected at the current round’s end. New sessions are already rejected byverify-launch.suspendedOperators— same semantics at(partnerId, operatorId)granularity.
The two lists are additive: a player is disconnected if their partner is in
suspendedPartners or their operator is in suspendedOperators. The response
covers only platform-owned dynamic suspends — game-level suspend
(memberGames[].suspended) comes via desired-state, and engine-level pause is
service-owned (see Control Plane). Player identity
never leaves the service; the service categorizes its own sockets.
Failure modes:
403— missing/wrong secret, orserviceUiddoes not own the engine.400— malformed body (missingserviceUidorengineId).404—engineIdnot registered; treat as “do not start a round,” trust the next heartbeat.5xx/ network — keep using the last successful response and continue the round. Cold-start fallback: empty lists (keep serving) — platform access problems must never silently stop round emission.
Optional: skipping this endpoint disables suspend-kick but does not crash the service; v1.0 services do not know about it.
GET /internal/bet-templates
Section titled “GET /internal/bet-templates”(v1.1) A side-effect-free bet-template fetch. The service calls it at the
start of each round (per player) and reflects betMin / betMax / maxWin / quickBets to active sockets for the next round. This is the canonical endpoint
for the per-round bet-template refresh.
It is the bet-template sub-clause of verify-launch, but without the side
effects: it does not call operator wallet authenticate (no TOKEN_TIMEOUT
risk), does not upsert the player, and does not rebuild partner config. It is a
pure DB lookup using the same resolution chain (operator-game override →
operator-service pin → partner default).
Headers: x-internal-config-secret: <secret> (shared with runtime-profiles —
both are read-only config endpoints).
Query parameters:
| Field | Type | Required | Description |
|---|---|---|---|
serviceUid | string | ✓ | The caller’s 16-char wire identifier (services.service_uid) |
partnerId | integer | ✓ | The operator’s partner (partners.id) |
operatorId | string | ✓ | The operator’s external id (operators.operator_id) |
currency | string | ✓ | ISO 4217, uppercase (e.g. "EUR") |
gameExternalId | integer | optional | For per-game override lookup; omitted = service default |
Example:
GET /internal/bet-templates?serviceUid=wp9dvdgp4wfqmxq4&partnerId=1&operatorId=cx-12&gameExternalId=9000¤cy=EURx-internal-config-secret: <secret>Response (200):
{ "ok": true, "betMin": 1, "betMax": 100, "maxWin": 10000, "quickBets": [1, 5, 10, 50]}betMin,betMax,maxWin— currency-aware bet limits (maxWinmay benull= no cap).quickBets— preset bet buttons (usually four).
Call pattern: once per active player at round start (caching is allowed per
unique operator+currency); write the result to the socket and reflect it to the
client; on 5xx/network, keep the last successful snapshot (the per-session
snapshot from verify-launch is also a valid fallback).
Errors: 403 (wrong secret), 400 (bad query), 404 (serviceUid not an
active service), 5xx (DB error).
Optional — verify-launch already returns per-session limits; use this to
refresh them per round without the wallet side effects.