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.
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.
Arquitetura
Há duas direções de chamada. É essencial entender quem inicia cada uma:
- Launch — seu backend chama
POST /launche recebe umsessionToken. - Embed — você abre o jogo no navegador do jogador via iframe, passando o token.
- Spins — o cliente do jogo fala com o RGS (a aposta vem do jogador).
- Wallet — a cada aposta/prêmio, o RGS chama a sua wallet (assinado com HMAC).
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
| Recurso | URL | Quem chama |
|---|---|---|
| API (launch + jogo) | https://game.muguet.dev/api | seu backend → nós |
| Client do jogo (iframe) | https://game.muguet.dev/?token=… | navegador do jogador |
| Painel administrativo | https://manager.muguet.dev | seu time de operação |
| Wallet (callback) | a SUA URL (ex.: https://wallet.seucassino.com) | nós → você |
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:
- Nosso time cria o seu operador no painel e te entrega
operatorId,hmacKeyIdehmacSecret. - O
hmacSecreté exibido uma única vez — guarde em local seguro (cofre de segredos). Se perder, rotacione. - Você nos informa a sua
callbackURL(base da sua wallet) e as moedas suportadas. - Habilitamos os jogos para o seu operador (RTP, limites de aposta min/max, moedas).
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
| Header | Descrição |
|---|---|
X-Meta-KeyID | Identificador da chave HMAC corrente (suporta rotação). Ex.: key-1. |
X-Meta-Timestamp | Unix timestamp em segundos. Usado para proteção contra replay. |
X-Meta-Signature | Assinatura hex (ver algoritmo abaixo). |
X-Idempotency-Key | Chave de idempotência única por intenção (bet/win/rollback). |
Content-Type | application/json |
Algoritmo
A assinatura é o HMAC-SHA256, em hexadecimal minúsculo, da string timestamp + "." + corpo_bruto, usando o hmacSecret como chave:
signature = hex( HMAC_SHA256( hmacSecret, timestamp + "." + rawBody ) )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:
X-Meta-KeyIDcorresponde a uma chave conhecida do operador.X-Meta-Timestampestá dentro da janela de ±300s (proteção de replay).X-Meta-Signaturebate com o HMAC recalculado sobre o corpo bruto.
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ção | Sufixo da chave | Exemplo |
|---|---|---|
| Aposta | :bet | a1b2c3…:bet |
| Prêmio | :win | a1b2c3…:win |
| Estorno | :rb | a1b2c3…: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).
| Endpoint | Quando chamamos | Efeito esperado |
|---|---|---|
/wallet/balance | ao abrir o jogo | retorna o saldo atual |
/wallet/bet | a cada giro | debita a aposta (atômico) |
/wallet/win | quando há prêmio | credita o ganho |
/wallet/rollback | falha após débito | estorna a aposta |
POST/wallet/balance você
Consulta de saldo do jogador. Chamado ao abrir o jogo.
Request
{
"playerId": "player-1",
"currency": "BRL"
}Response 200
{
"balance": 1245800 // centavos (R$ 12.458,00)
}POST/wallet/bet você
Debita a aposta do jogador. Deve ser atômico e idempotente.
Request
{
"playerId": "player-1",
"roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
"amount": 2000,
"currency": "BRL",
"gameId": "copa-da-sorte",
"idempotencyKey": "a1b2c3…:bet"
}Response 200
{
"balance": 1243800, // saldo APÓS o débito
"txId": "tx-9c1f…" // id da transação no seu sistema
}{ "code": "INSUFFICIENT_FUNDS" }. Não debite nada.POST/wallet/win você
Credita o prêmio. Mesmo corpo do bet (com idempotencyKey terminando em :win). Só chamamos quando há ganho > 0.
Request
{
"playerId": "player-1",
"roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
"amount": 1680,
"currency": "BRL",
"gameId": "copa-da-sorte",
"idempotencyKey": "a1b2c3…:win"
}Response 200
{
"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
{
"roundId": "8f6eff92-8b67-4c7f-bc1b-134985f06426",
"idempotencyKey": "a1b2c3…:rb",
"reason": "win_credit_failed"
}Response 200
{ "ok": true }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:
| code | HTTP sugerido | Significado |
|---|---|---|
INSUFFICIENT_FUNDS | 402 | Saldo insuficiente para a aposta. |
PLAYER_NOT_FOUND | 404 | Jogador desconhecido. |
SESSION_INVALID | 401 | Sessão/credencial inválida. |
DUPLICATE | 409 | Conflito de idempotência inesperado. |
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á comminBet,maxBetecurrenciesdo seu contrato.
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 https://game.muguet.dev/api/games?operatorId=casino-demoResponse 200
{
"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
}
]
}| Campo | Descrição |
|---|---|
gameId | Identificador estável do jogo. Use no /launch e como chave no seu lobby. |
name | Nome de exibição padrão (você pode sobrescrever no seu lobby). |
version / variant | Versão da engine e variante de RTP ativa. |
reels / rows / ways | Layout do jogo (grade e linhas de pagamento). |
minBet / maxBet | Limites de aposta em centavos. Presentes apenas no modo operator-scoped. |
currencies | Moedas habilitadas para você naquele jogo. |
enabled | Se o jogo está ativo para você. |
maintenance | Em manutenção: oculte ou bloqueie a abertura temporariamente. |
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 -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
{
"sessionId": "40c37dc4-2c08-4796-8bb8-0c272bc59ed0",
"sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"serverSeedHash": "356cc19e7e0a451d55b7f3e9…",
"expiresAt": "2026-05-31T01:28:41Z"
}| Campo | Obrigatório | Descrição |
|---|---|---|
operatorId | sim | Seu identificador de operador. |
playerId | sim | ID do jogador no SEU sistema (usado na wallet). |
gameId | não | Default copa-da-sorte. |
currency | recom. | Moeda ISO (ex.: BRL). |
locale | não | Default pt-BR. |
Erros de validação
O /launch valida o jogo contra o seu catálogo (mesma fonte do GET /games):
| code | HTTP | Quando |
|---|---|---|
GAME_NOT_AVAILABLE | 404 | Jogo inexistente ou não habilitado para o operador. |
GAME_MAINTENANCE | 503 | Jogo em manutenção para o operador. |
CURRENCY_NOT_SUPPORTED | 422 | Moeda 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:
<iframe
src="https://game.muguet.dev/?token=SESSION_TOKEN"
allow="autoplay; fullscreen"
style="width:100%;height:100%;border:0">
</iframe>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 — 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/rollbackda aposta. - settled — estado terminal e imutável.
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
5xxe timeouts: até 3 tentativas com backoff (100ms → 300ms → 700ms + jitter), reusando a mesmaX-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.
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
serverSeedsó é 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:
- Suba sua wallet com os 4 endpoints e a verificação HMAC.
- Registre a sua
callbackURLconosco. - Faça um
/launche confirme que recebeusessionToken. - Abra o jogo com o token e dê alguns giros — observe as chamadas
bet/winchegando na sua wallet. - Force um saldo insuficiente e confirme o
402 INSUFFICIENT_FUNDS. - Teste idempotência: reenvie a mesma
X-Idempotency-Keye confirme que o saldo não muda.
Checklist de go-live
| Item | Por quê |
|---|---|
| Verificação HMAC sobre corpo bruto + tempo constante | Segurança da integração |
| Janela de replay de ±300s aplicada | Anti-replay |
bet atômico (trava/transação por jogador) | Impede saldo negativo / "duplicar dinheiro" |
| Idempotência em bet/win/rollback | Effectively-once em retries |
| 402 para saldo insuficiente; 4xx para negócio; 5xx só transitório | Comportamento correto de retry |
| Valores sempre em centavos (inteiros) | Sem erro de arredondamento |
hmacSecret em cofre de segredos | Proteção da credencial |
| Logs/auditoria de cada chamada de wallet | Conciliaçã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).