Meta Games · Integração

Integração Meta Games

Tudo que o seu cassino precisa para plugar os jogos do RGS Meta Games usando o modelo seamless wallet com assinatura HMAC.

Para quem é este guia Times de engenharia de cassinos parceiros. Ao final você terá implementado os 4 endpoints de wallet, validado a assinatura HMAC das nossas chamadas e conseguido abrir um jogo real para um jogador.

Introdução

O Meta Games RGS (Remote Game Server) executa a matemática dos jogos, controla o ciclo de vida de cada rodada e conversa com a carteira (wallet) do seu cassino a cada aposta e prêmio. Os jogos são clientes web embarcáveis via <iframe>.

No modelo seamless wallet, o saldo do jogador mora no seu cassino — nós nunca guardamos dinheiro. A cada giro, nós chamamos a sua API para debitar a aposta e creditar o ganho. Sua wallet é a fonte única da verdade financeira.

Modelo
Seamless wallet
Auth
HMAC-SHA256
Valores
inteiros em centavos
Garantia
effectively-once

Arquitetura

Há duas direções de chamada. É essencial entender quem inicia cada uma:

┌─────────────┐ 1. launch (server→server) ┌──────────────────┐ │ │ ──────────────────────────────▶ │ │ │ Seu │ 4. débito/crédito (wallet) │ Meta Games │ │ cassino │ ◀────────────────────────────── │ RGS │ │ (wallet) │ nós chamamos VOCÊ │ │ └─────────────┘ └──────────────────┘ ▲ │ │ 2. abre o jogo no navegador (iframe ?token=) │ 3. spins │ ▼ ┌──────────┐ ┌────────────┐ │ Jogador │ ◀────── client do jogo (Pixi) ───── │ Game UI │ └──────────┘ └────────────┘
  1. Launch — seu backend chama POST /launch e recebe um sessionToken.
  2. Embed — você abre o jogo no navegador do jogador via iframe, passando o token.
  3. Spins — o cliente do jogo fala com o RGS (a aposta vem do jogador).
  4. Wallet — a cada aposta/prêmio, o RGS chama a sua wallet (assinado com HMAC).
A parte que VOCÊ implementa Os 4 endpoints de wallet (balance, bet, win, rollback) na sua callbackURL, e a verificação da assinatura HMAC. O resto (launch, jogo, math, snapshot) é nosso.

Conceitos-chave

Dinheiro em centavos

Todos os valores monetários são inteiros em centavos (menor unidade da moeda). R$ 12,45 = 1245. Nunca use ponto flutuante para dinheiro.

Idempotência

Toda chamada de wallet carrega um header X-Idempotency-Key único por intenção. Se você receber a mesma chave duas vezes, retorne o mesmo resultado sem aplicar o efeito de novo. Isso nos permite reenviar com segurança após timeouts de rede.

Effectively-once

Não prometemos "exactly-once" (é uma armadilha em sistemas distribuídos). Garantimos effectively-once via idempotência + retries + reconciliação. Sua wallet precisa ser idempotente para que isso funcione.

Atomicidade

O débito da aposta (checar saldo + debitar) precisa ser atômico no seu lado. É isso que impede um jogador de "duplicar dinheiro" abrindo várias abas. Trate cada bet sob uma trava/transação por jogador.

Ambientes & URLs

RecursoURLQuem chama
API (launch + jogo)https://game.muguet.dev/apiseu backend → nós
Client do jogo (iframe)https://game.muguet.dev/?token=…navegador do jogador
Painel administrativohttps://manager.muguet.devseu time de operação
Wallet (callback)a SUA URL (ex.: https://wallet.seucassino.com)nós → você
Ambiente de produção dedicado As URLs acima são do ambiente atual. Para produção em volume podemos provisionar um host de API dedicado (ex.: api.muguet.dev) e um ambiente de sandbox isolado — fale com o nosso time no onboarding.

Onboarding

Antes de qualquer chamada você precisa das credenciais do operador, geradas no nosso painel administrativo:

  1. Nosso time cria o seu operador no painel e te entrega operatorId, hmacKeyId e hmacSecret.
  2. O hmacSecret é exibido uma única vez — guarde em local seguro (cofre de segredos). Se perder, rotacione.
  3. Você nos informa a sua callbackURL (base da sua wallet) e as moedas suportadas.
  4. Habilitamos os jogos para o seu operador (RTP, limites de aposta min/max, moedas).
operatorId
casino-demo
hmacKeyId
key-1
hmacSecret
••••••••••••••••

Assinatura HMAC

Toda requisição que nós enviamos para a sua wallet é assinada. Você deve verificar a assinatura e rejeitar requisições inválidas ou fora da janela de tempo.

Headers enviados em cada chamada

HeaderDescrição
X-Meta-KeyIDIdentificador da chave HMAC corrente (suporta rotação). Ex.: key-1.
X-Meta-TimestampUnix timestamp em segundos. Usado para proteção contra replay.
X-Meta-SignatureAssinatura hex (ver algoritmo abaixo).
X-Idempotency-KeyChave de idempotência única por intenção (bet/win/rollback).
Content-Typeapplication/json

Algoritmo

A assinatura é o HMAC-SHA256, em hexadecimal minúsculo, da string timestamp + "." + corpo_bruto, usando o hmacSecret como chave:

fórmula
signature = hex( HMAC_SHA256( hmacSecret, timestamp + "." + rawBody ) )
Use o corpo BRUTO, byte a byte Assine e verifique sobre os bytes exatos do corpo recebido — não re-serialize o JSON antes de validar. Reordenar campos ou re-formatar quebra a assinatura.

Verificação (exemplos)

const crypto = require("crypto");

// Express: capture o corpo BRUTO antes do JSON.parse
app.use(express.raw({ type: "application/json" }));

function verify(req, secret) {
  const ts  = req.get("X-Meta-Timestamp");
  const sig = req.get("X-Meta-Signature");
  if (!ts || !sig) return false;

  // janela de replay: 5 minutos
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const raw = req.body;                       // Buffer (corpo bruto)
  const mac = crypto.createHmac("sha256", secret)
                    .update(ts + ".")
                    .update(raw)
                    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig));
}
<?php
function verify(string $secret): bool {
  $ts  = $_SERVER['HTTP_X_META_TIMESTAMP'] ?? '';
  $sig = $_SERVER['HTTP_X_META_SIGNATURE'] ?? '';
  if ($ts === '' || $sig === '') return false;
  if (abs(time() - (int)$ts) > 300) return false;   // replay window

  $raw = file_get_contents('php://input');           // corpo bruto
  $mac = hash_hmac('sha256', $ts . '.' . $raw, $secret);
  return hash_equals($mac, $sig);
}
import hmac, hashlib, time

def verify(headers, raw_body: bytes, secret: str) -> bool:
    ts  = headers.get("X-Meta-Timestamp", "")
    sig = headers.get("X-Meta-Signature", "")
    if not ts or not sig:
        return False
    if abs(time.time() - int(ts)) > 300:           # replay window
        return False
    msg = ts.encode() + b"." + raw_body
    mac = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, sig)
func verify(h http.Header, raw []byte, secret string) bool {
    ts  := h.Get("X-Meta-Timestamp")
    sig := h.Get("X-Meta-Signature")
    if ts == "" || sig == "" {
        return false
    }
    n, _ := strconv.ParseInt(ts, 10, 64)
    if math.Abs(float64(time.Now().Unix()-n)) > 300 { // replay window
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(ts)); mac.Write([]byte(".")); mac.Write(raw)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(sig))
}

Verificando nossas chamadas

Para cada requisição recebida na sua wallet, valide nesta ordem e rejeite com HTTP 401 em qualquer falha:

  1. X-Meta-KeyID corresponde a uma chave conhecida do operador.
  2. X-Meta-Timestamp está dentro da janela de ±300s (proteção de replay).
  3. X-Meta-Signature bate com o HMAC recalculado sobre o corpo bruto.
Comparação em tempo constante Sempre compare assinaturas com função de tempo constante (hmac.Equal, hash_equals, timingSafeEqual) para evitar ataques de timing.

Idempotência

Derivamos a chave de idempotência por intenção a partir da chave do round. Para o mesmo round você verá:

IntençãoSufixo da chaveExemplo
Aposta:beta1b2c3…:bet
Prêmio:wina1b2c3…:win
Estorno:rba1b2c3…:rb

Regra de ouro: persista o resultado por X-Idempotency-Key. Numa repetição, devolva exatamente a mesma resposta (mesmo balance e txId) sem mover saldo de novo.

Rotação de chave HMAC

Em emergência (vazamento) ou rotina, o hmacSecret pode ser rotacionado pelo painel. Ao rotacionar, geramos um novo hmacSecret + hmacKeyId e o antigo é invalidado na hora. Use o header X-Meta-KeyID para saber qual chave usar na verificação se você mantiver mais de uma ativa durante a transição.

Wallet API — visão geral

Implemente estes 4 endpoints na base da sua callbackURL. Todos recebem POST com JSON e os headers HMAC. Responda 200 em sucesso; 4xx com { code, message } em rejeições de negócio; reserve 5xx apenas para erros transitórios (nós damos retry em 5xx).

EndpointQuando chamamosEfeito esperado
/wallet/balanceao abrir o jogoretorna o saldo atual
/wallet/beta cada girodebita a aposta (atômico)
/wallet/winquando há prêmiocredita o ganho
/wallet/rollbackfalha após débitoestorna a aposta

POST/wallet/balance você

Consulta de saldo do jogador. Chamado ao abrir o jogo.

Request

JSON
{
  "playerId": "player-1",
  "currency": "BRL"
}

Response 200

JSON
{
  "balance": 1245800      // centavos (R$ 12.458,00)
}

POST/wallet/bet você

Debita a aposta do jogador. Deve ser atômico e idempotente.

Request

JSON
{
  "playerId": "player-1",
  "roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
  "amount": 2000,
  "currency": "BRL",
  "gameId": "copa-da-sorte",
  "idempotencyKey": "a1b2c3…:bet"
}

Response 200

JSON
{
  "balance": 1243800,           // saldo APÓS o débito
  "txId": "tx-9c1f…"            // id da transação no seu sistema
}
Saldo insuficiente Se o jogador não tiver fundos, responda HTTP 402 com { "code": "INSUFFICIENT_FUNDS" }. Não debite nada.
Atomicidade obrigatória Faça "checar saldo + debitar" dentro de uma transação/trava por jogador. Sem isso, apostas concorrentes (várias abas) podem deixar o saldo negativo.

POST/wallet/win você

Credita o prêmio. Mesmo corpo do bet (com idempotencyKey terminando em :win). Só chamamos quando há ganho > 0.

Request

JSON
{
  "playerId": "player-1",
  "roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
  "amount": 1680,
  "currency": "BRL",
  "gameId": "copa-da-sorte",
  "idempotencyKey": "a1b2c3…:win"
}

Response 200

JSON
{
  "balance": 1245480,           // saldo APÓS o crédito
  "txId": "tx-c934…"
}

POST/wallet/rollback você

Estorna uma aposta já debitada quando algo falha entre o débito e a conclusão do round. Idempotente: se já estornou esse round, apenas confirme ok.

Request

JSON
{
  "roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
  "idempotencyKey": "a1b2c3…:rb",
  "reason": "win_credit_failed"
}

Response 200

JSON
{ "ok": true }
Estorne pelo dono do round Resolva o estorno pelo roundId e credite de volta no jogador que apostou. Se o round não existir ou já tiver sido estornado, responda { "ok": true } (idempotente).

Códigos de erro

Em rejeições de negócio, responda com status 4xx e um corpo { "code": "…", "message": "…" }. Os códigos que reconhecemos:

codeHTTP sugeridoSignificado
INSUFFICIENT_FUNDS402Saldo insuficiente para a aposta.
PLAYER_NOT_FOUND404Jogador desconhecido.
SESSION_INVALID401Sessão/credencial inválida.
DUPLICATE409Conflito de idempotência inesperado.
4xx vs 5xx 4xx = decisão final, não damos retry. 5xx = transitório, fazemos até 3 tentativas com backoff exponencial. Não retorne 5xx para regras de negócio.

GET/games nós

Catálogo de jogos para o seu cassino sincronizar o lobby. Chame este endpoint para descobrir quais jogos estão disponíveis, com seus limites de aposta, moedas e estado de manutenção. Chamada server-to-server.

  • GET /games — todos os jogos da plataforma (metadados de engine, sem limites por operador).
  • GET /games?operatorId=SEU_ID — apenas os jogos habilitados para você, já com minBet, maxBet e currencies do seu contrato.
Como sincronizar Faça um GET /games?operatorId=… periodicamente (ex.: a cada poucos minutos) ou sob demanda. Use o gameId como chave estável no seu lobby e respeite o flag maintenance para esconder/desabilitar um jogo temporariamente.

Request

cURL
curl https://game.muguet.dev/api/games?operatorId=casino-demo

Response 200

JSON
{
  "operatorId": "casino-demo",
  "count": 1,
  "syncedAt": "2026-05-31T01:56:32Z",
  "games": [
    {
      "gameId":      "copa-da-sorte",
      "name":        "Copa Da Sorte",
      "version":     "0.1.0",
      "variant":     "rtp_96",
      "reels":       5,
      "rows":        3,
      "ways":        243,
      "minBet":      100,           // centavos (operator-scoped)
      "maxBet":      100000,        // centavos (operator-scoped)
      "currencies":  ["BRL"],
      "enabled":     true,
      "maintenance": false
    }
  ]
}
CampoDescrição
gameIdIdentificador estável do jogo. Use no /launch e como chave no seu lobby.
nameNome de exibição padrão (você pode sobrescrever no seu lobby).
version / variantVersão da engine e variante de RTP ativa.
reels / rows / waysLayout do jogo (grade e linhas de pagamento).
minBet / maxBetLimites de aposta em centavos. Presentes apenas no modo operator-scoped.
currenciesMoedas habilitadas para você naquele jogo.
enabledSe o jogo está ativo para você.
maintenanceEm manutenção: oculte ou bloqueie a abertura temporariamente.
Sem credencial = catálogo só seu No modo operatorId retornamos apenas os jogos que você contratou e habilitou. Sem o parâmetro, devolvemos o catálogo global da plataforma (sem limites, pois limites são por operador).

POST/launch nós

Seu backend chama este endpoint para abrir uma sessão de jogo e obter um sessionToken (JWT de curta duração). Chamada server-to-server.

Request

cURL
curl -X POST https://game.muguet.dev/api/launch \
  -H 'Content-Type: application/json' \
  -d '{
    "operatorId": "casino-demo",
    "playerId":   "player-1",
    "gameId":     "copa-da-sorte",
    "currency":   "BRL",
    "locale":     "pt-BR"
  }'

Response 200

JSON
{
  "sessionId":      "40c37dc4-2c08-4796-8bb8-0c272bc59ed0",
  "sessionToken":   "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "serverSeedHash": "356cc19e7e0a451d55b7f3e9…",
  "expiresAt":      "2026-05-31T01:28:41Z"
}
CampoObrigatórioDescrição
operatorIdsimSeu identificador de operador.
playerIdsimID do jogador no SEU sistema (usado na wallet).
gameIdnãoDefault copa-da-sorte.
currencyrecom.Moeda ISO (ex.: BRL).
localenãoDefault pt-BR.

Erros de validação

O /launch valida o jogo contra o seu catálogo (mesma fonte do GET /games):

codeHTTPQuando
GAME_NOT_AVAILABLE404Jogo inexistente ou não habilitado para o operador.
GAME_MAINTENANCE503Jogo em manutenção para o operador.
CURRENCY_NOT_SUPPORTED422Moeda não habilitada para o jogo.

Embed via iframe

Com o sessionToken em mãos, abra o jogo no navegador do jogador passando o token na query string:

HTML
<iframe
  src="https://game.muguet.dev/?token=SESSION_TOKEN"
  allow="autoplay; fullscreen"
  style="width:100%;height:100%;border:0">
</iframe>
Tokens são de curta duração O sessionToken expira (TTL padrão de 30 minutos). Gere um novo /launch a cada abertura de jogo. Nunca reaproveite tokens entre jogadores.

Ciclo de vida do round

Cada giro percorre este caminho. Você participa nas etapas de wallet:

open ──▶ bet (debita) ──▶ math ──▶ win (credita, se houver) ──▶ settled │ ▲ └────── falha ──▶ rollback (estorna) ──▶ rolled-back ┘
  • open — round criado, snapshot reproduzível gravado.
  • bet — chamamos /wallet/bet. Se falhar, o round é abortado (sem estorno, pois nada foi debitado).
  • win — havendo prêmio, chamamos /wallet/win. Se falhar, chamamos /wallet/rollback da aposta.
  • settled — estado terminal e imutável.
Política aplicada a cada giro A cada spin revalidamos a config do operador: a aposta precisa estar entre minBet e maxBet (senão BET_OUT_OF_RANGE, HTTP 422) e o jogo não pode estar em manutenção (GAME_MAINTENANCE, HTTP 503). Sincronize esses limites no seu lobby via GET /games para evitar rejeições.

Retries & reconciliação

Como nos comportamos diante de falhas — e por que sua idempotência é essencial:

  • Retry automático em respostas 5xx e timeouts: até 3 tentativas com backoff (100ms → 300ms → 700ms + jitter), reusando a mesma X-Idempotency-Key.
  • Reconciliação: um processo nosso varre rounds que ficaram presos (ex.: crash entre o débito e a conclusão) e resolve — estornando a aposta (idempotente) ou finalizando o round se o prêmio já tinha sido creditado.
O que isso exige de você Apenas que bet, win e rollback sejam idempotentes por X-Idempotency-Key. Com isso, reenvios e estornos repetidos nunca duplicam nem perdem dinheiro.

Provably-fair (auditoria)

Cada round guarda um snapshot completo (seeds, versões, grade, ways, win) que permite reproduzir a rodada bit a bit em caso de disputa. Usamos commit-reveal:

  • serverSeedHash = SHA256(serverSeed) é publicado antes da rodada (no /launch).
  • O serverSeed só é revelado após o encerramento/rotação da sessão.
  • Qualquer disputa é resolvida re-executando o snapshot — resultado determinístico.

Teste & sandbox

Fluxo mínimo para validar sua integração ponta a ponta:

  1. Suba sua wallet com os 4 endpoints e a verificação HMAC.
  2. Registre a sua callbackURL conosco.
  3. Faça um /launch e confirme que recebeu sessionToken.
  4. Abra o jogo com o token e dê alguns giros — observe as chamadas bet/win chegando na sua wallet.
  5. Force um saldo insuficiente e confirme o 402 INSUFFICIENT_FUNDS.
  6. Teste idempotência: reenvie a mesma X-Idempotency-Key e confirme que o saldo não muda.

Checklist de go-live

ItemPor quê
Verificação HMAC sobre corpo bruto + tempo constanteSegurança da integração
Janela de replay de ±300s aplicadaAnti-replay
bet atômico (trava/transação por jogador)Impede saldo negativo / "duplicar dinheiro"
Idempotência em bet/win/rollbackEffectively-once em retries
402 para saldo insuficiente; 4xx para negócio; 5xx só transitórioComportamento correto de retry
Valores sempre em centavos (inteiros)Sem erro de arredondamento
hmacSecret em cofre de segredosProteção da credencial
Logs/auditoria de cada chamada de walletConciliação e disputas

Suporte

Dúvidas de integração, credenciais, novas moedas ou jogos: fale com o nosso time de integração. Tenha em mãos o seu operatorId e, para problemas de round específico, o roundId e o horário (UTC).