Servidor autoritativo (RGS) de um crash game provably fair com RTP de 97%. O servidor decide tudo — resultado da rodada, saques e auto-saque — o cliente apenas anima a curva. Não existe modo de forçar resultado em produção.
http://127.0.0.1:7777
WebSocket: ws://127.0.0.1:7777/jogar/crash-test
POST /api/session (demo anônima) ou POST /api/launch (cliente de operador). Formato <player_id>.<assinatura>, sem expiração (vale enquanto o AUTH_SECRET do servidor não mudar). Vai em ?token= no WebSocket.Authorization: Bearer <ADMIN_TOKEN> ou x-admin-token: <ADMIN_TOKEN>. O token é impresso no console ao iniciar o servidor (ou definido em ADMIN_TOKEN).hash = SHA256(`${serverSeed}-${clientSeed}-${nonce}`)
baseInt = int(hash[0..8], 16) // 32 bits
X = baseInt / 2^32 // uniforme em [0, 1)
baseInt % 50 == 0 -> crash = 1.00x // instant crash (2%)
senão -> crash = floor((100-edge)/(100-2) * 1/(1-X) * 100) / 100
mínimo 1.00x · teto max_multiplier (padrão 10.000x)ROUND_BETTING.GAME_CRASHED — qualquer pessoa recalcula o resultado com POST /api/verify.HOUSE_EDGE = 3, a escala 97/98 produz RTP de exatamente 97% para qualquer alvo de saque.| Fase | Duração | Descrição |
|---|---|---|
BETTING | 5 s | Janela de apostas (commit publicado) |
RUNNING | variável | Curva m(t) = e^(0,00006·t) truncada em 2 casas; tick a cada 100 ms |
CRASHED | 3 s (cooldown) | Seed revelada, apostas liquidadas, depois nova rodada |
Se o token enviado for válido, devolve o mesmo player_id; caso contrário emite um novo. A carteira demo começa com 10.000 BRL (saldo em memória).
{ "token": "usr_05efb035f603.YNL7HbmX8klNiMYMlOM_IzFfbHcRtGAm" }{
"player_id": "usr_05efb035f603",
"token": "usr_05efb035f603.YNL7HbmX8klNiMYMlOM_IzFfbHcRtGAm",
"ws_path": "/jogar/crash-test"
}Valida agente + cliente e emite o token interno do WebSocket para o jogador do operador, com o saldo real do cliente no RGS. Parâmetros no body ou na query string.
{
"agent": "agente-demo-749a0a", // chave do agente (obrigatório)
"user": "jogador1", // user_ref do cliente (obrigatório)
"token": "sess_1791909745_f641…", // session_token do cliente (obrigatório)
"operator": "real", "currency": "BRL", "lang": "pt"
}{
"player_id": "cli_cf7639bafc2e",
"token": "cli_cf7639bafc2e.CJuLqJdryZpMhy_tucB3vEgVBNP-K8nw",
"ws_path": "/jogar/crash-test",
"balance": 50, "currency": "BRL", "operator": "real",
"client": { "id": "461b4d00-…", "user": "jogador1", "name": "J1" },
"agent": { "key": "agente-demo-749a0a", "name": "Agente Demo" },
"min_bet": 1, "max_bet": 10000
}| Erro | Quando |
|---|---|
404 | agente inválido/inativo ou cliente não encontrado |
400 | user ausente |
403 | cliente inativo |
401 | session_token do cliente inválido |
const ws = new WebSocket(`ws://${location.host}/jogar/crash-test?token=${encodeURIComponent(token)}`);ERROR RATE_LIMIT.latency_ms nos logs).SESSION (estado completo: rodada atual, histórico, saldo, apostas próprias).CONNECTED → GRACE_PERIOD (GRACE_MS, padrão 5 s) → DISCONNECTED. Políticas: EMERGENCY_CASH_OUT (padrão: saca a aposta em voo no multiplicador atual) ou LOST_ON_CRASH (aposta aberta perde no crash). Auto-saque configurado sempre continua valendo no servidor.Só é aceito na fase BETTING. Uma aposta por slot por rodada. O débito é imediato e síncrono.
{
"action": "PLACE_BET",
"slot": "a", // "a" | "b" | "c" (slots ativos: 1–3, padrão a+b)
"amount": 10, // BRL, 2 casas, dentro dos limites
"currency": "BRL", // apenas BRL
"is_auto_cashout": true, // opcional
"auto_cashout_target": 1.5 // obrigatório se is_auto_cashout; 1.01–max_multiplier
}{
"event": "BET_ACCEPTED", "status": "SUCCESS", "timestamp": 1760640000000,
"round_id": "48310806-fb34-…", "bet_id": "3413956f-…",
"slot": "a", "amount": 10,
"is_auto_cashout": true, "auto_cashout_target": 1.5,
"balance": 9990
}Só é aceito na fase RUNNING. O multiplier_claimed é ignorado — vale o multiplicador do servidor no instante do recebimento (multiplicador validado, 2 casas).
{ "action": "CASH_OUT", "slot": "a", "multiplier_claimed": 500 }{
"event": "CASH_OUT_SUCCESS",
"round_id": "48310806-…", "multiplier_validated": 1.33, "payout_credited": 13.3,
"timestamp_settled": 1760640000000, "slot": "a", "bet_id": "3413956f-…",
"settlement": "CASH_OUT_SUCCESS", "wallet_status": "CREDITED", "balance": 10003.3
}wallet_status pode ser CREDITED (creditado na hora) ou QUEUED (carteira instável — o crédito entra na fila de emergência e chega depois em WALLET_CREDITED, garantindo idempotência por bet_id).{ "action": "PING", "timestamp": 123 }{ "event": "PONG", "timestamp": 1760640000000, "client_timestamp": 123 }{
"event": "SESSION", "player_id": "usr_3d0b17250c44", "balance": 10000, "currency": "BRL",
"house_edge": 3, "rtp": 97, "min_bet": 1, "max_bet": 10000, "max_multiplier": 10000,
"phase": "RUNNING",
"round": { "round_id": "…", "commit": "…", "client_seed": "…", "nonce": 1554,
"multiplier": 1.27, "ends_in_ms": 0 },
"bots": [{ "name": "Jo***10", "valor": 500, "cashed_at": null }],
"history":[{ "id": "…", "nonce": 1548, "commit": "…", "serverSeed": "…",
"clientSeed": "…", "crashPoint": 2.96, "rngRaw": 0.4705, "ts": "…" }],
"flow": [{ "id": "…", "apostas": 15, "ganhos": 19.5, "caixa": 99995.5, "crash": 1.69 }],
"totals": { "rodadas": 6333, "apostado": 13260, "pago": 3719.4, "caixa": 19540.6 },
"bots_config": { "min": 50, "max": 50 },
"my_bets": { "a": { "amount": 10, "cashed_at": null, "payout": 0, "settled": false } }
}my_bets permite reconectar no meio da rodada e restaurar apostas em andamento.
{
"event": "ROUND_BETTING", "round_id": "…", "commit": "ecd8cf4548e6744a…",
"client_seed": "0182672b…", "nonce": 1555, "ends_in_ms": 5000,
"house_edge": 3, "timestamp": 1760640000000,
"forced": false, "compensated": false,
"bots": [{ "name": "Jo***10", "valor": 500 }]
}{ "event": "ROUND_STARTED", "round_id": "…", "timestamp": 1760640005000 }{
"event": "GAME_TICK", "round_id": "…", "multiplier": 1.33, "timestamp": 1760640005200,
"cashouts": [{ "i": 3, "name": "Ca***13", "multiplier": 1.3, "payout": 650 }]
}{
"event": "GAME_CRASHED", "round_id": "…", "final_multiplier": 7.24,
"timestamp_crash": 1760640013000,
"server_seed": "c921b203f06950b8…(64 hex)", "commit": "8ef48cc9009c71f8…",
"client_seed": "0182672b…", "nonce": 1556,
"totals": { "apostado": 30, "pago": 19.5, "caixa": 99989.5 },
"bots": { "apostado": 25000, "pago": 9000, "ganhos": 18, "total": 50 },
"late_cashouts": [], "cooldown_ms": 3000
}server_seed + commit + client_seed + nonce + final_multiplier e confira em POST /api/verify.{
"event": "BETS_SETTLED", "round_id": "…", "final_multiplier": 1.86, "balance": 10004.5,
"bets": {
"a": { "bet_id": "…", "amount": 10, "cashed_at": 1.3, "payout": 13, "result": "CASH_OUT_SUCCESS" },
"b": { "bet_id": "…", "amount": 5, "cashed_at": null, "payout": 0, "result": "LOST_ON_CRASH" }
}
}{
"event": "WALLET_CREDITED", "bet_id": "…", "amount": 11, "balance": 10001,
"source": "admin" // presente quando o crédito veio do painel admin
}{ "event": "LIMITS_CONFIG", "min_bet": 2, "max_bet": 200, "max_multiplier": 300 }
{ "event": "BOTS_CONFIG", "min": 5, "max": 7 }
{ "event": "COMPENSATION_CONFIG", "enabled": false, "cycle": 1000 }
{ "event": "BANKROLL_CONFIG", "house_bankroll": 12345 }| Código | Ação | Motivo |
|---|---|---|
BETTING_CLOSED | PLACE_BET | janela de apostas fechada |
INVALID_SLOT | PLACE_BET / CASH_OUT | slot inexistente ou não ativo |
DUPLICATE_BET | PLACE_BET | aposta já feita neste slot/rodada |
INVALID_AMOUNT | PLACE_BET | valor inválido, >2 casas ou fora dos limites |
INVALID_CURRENCY | PLACE_BET | moeda não suportada (apenas BRL) |
INVALID_TARGET | PLACE_BET | alvo de auto-saque inválido (1.01–max_multiplier, 2 casas) |
WALLET_DEBIT_FAILED | PLACE_BET | saldo insuficiente |
NOT_RUNNING | CASH_OUT | saque fora da fase RUNNING |
NO_ACTIVE_BET | CASH_OUT | sem aposta ativa no slot |
TOO_LATE | CASH_OUT | servidor já passou do crash |
UNKNOWN_ACTION | qualquer | ação desconhecida |
RATE_LIMIT | qualquer | >20 mensagens/s |
{
"serverSeed": "33d74f821af3c4bd…(64 hex)", // revelado no GAME_CRASHED
"commit": "883f8775d882b9b4…(64 hex)", // publicado no ROUND_BETTING
"clientSeed": "0182672b7eb805c4…",
"nonce": 2571,
"crashPoint": 1.15
}{
"valido": true, "commitOk": true, "crashOk": true,
"crashRecalculado": 1.33, "rngRaw": 0.2566413190215826
}{
"valido": false, "commitOk": true, "crashOk": false,
"crashRecalculado": 1.16, "rngRaw": 0.14950600010342896,
"sorteio_deslocado": 0.1466156188,
"motivo": "rodada gerada com compensação de RTP ativa — sorteio deslocado da fórmula pública (a seed não foi adulterada)"
}{
"status": "ok", "db": "sqlite", "queue": "memory", "retry_queue_length": 0,
"phase": "RUNNING", "round_id": "67c1f64a-…", "multiplier": 1.11,
"ws_path": "/jogar/crash-test", "house_edge": 3
}{
"history": [{
"id": "c45330d8-…", "nonce": 1554, "commit": "6baa76f5…", "serverSeed": "…",
"clientSeed": "…", "crashPoint": 2.96, "rngRaw": 0.4705, "ts": "2026-10-13T17:08:04.713Z"
}]
}{
"count": 100,
"rounds": [{
"round_id": "…", "server_seed": "…", "client_seed": "…", "nonce": 1554,
"rng_raw_result": 0.4705288903, "crash_multiplier": 2.96,
"house_edge_percentage": 3,
"timestamp_started": "…", "timestamp_crashed": "…"
}]
}{
"rodadas": 6331, "totalApostado": 13260, "totalPago": 3719.4,
"rtpMedido": 28.05, "rtpTeorico": 97, "crashMedio": 9.42, "caixa": 19540.6
}Devolve o JSON completo do tema (cores, imagens, textos, layout, faixas de cor do multiplicador, opções).
Authorization: Bearer <ADMIN_TOKEN> ou x-admin-token: <ADMIN_TOKEN>. Sem/come token inválido → 401.{
"house_edge": 3,
"bots": { "min": 50, "max": 50 },
"policy": { "graceMs": 5000, "disconnect": "EMERGENCY_CASH_OUT" },
"db": "sqlite", "queue": "memory", "wallet_down": false,
"compensation": { "enabled": false, "cycle": 1000, "count": 0, "rounds_left": 1000, "debt": 0, "mean_x": 0.5, "rtp": { "2": null, "5": null, "10": null, "1.5": null }, "forced_next": null },
"limits": { "min_bet": 1, "max_bet": 10000, "max_multiplier": 10000 },
"house_bankroll": 100000
}{ "admin_token": "zKBLS0cf8Y3g…" }{
"logs": [{
"bet_id": "…", "round_id": "…", "player_id": "usr_…",
"bet_amount": 5, "currency_code": "BRL", "is_auto_cashout": false, "auto_cashout_target": null,
"connection_status_at_action": "CONNECTED", "action_type": "CASH_OUT_SUCCESS",
"multiplier_achieved": 1.3, "payout_amount": 6.5,
"player_ip_address": "127.0.0.1", "latency_ms": 12,
"timestamp_action": "…", "parent_bet_id": "…" // liga CASH_OUT/LOST ao BET_PLACED
}]
}{ "min": 50, "max": 120 }{ "ok": true, "min": 50, "max": 120 }Bots são apenas simulação visual — não entram nas métricas nem no banco.
{
"totals": { "rodadas": 6333, "crashMedio": 4.46, "apostado": 15, "pago": 19.5 },
"flow": [{
"round_id": "…", "crash_multiplier": 1.69, "apostas": 15, "ganhos": 19.5,
"caixa": 99995.5, "rtp": 130,
"bots_apostado": 12625, "bots_pago": 4466.17, "bots_rtp": 35.4,
"bots_ganhos": 11, "bots_total": 50
}],
"botTotals": { "total_apostado": 63125, "total_pago": 22330.85, "rodadas": 5, "rtp_medio": 35.4 },
"house_bankroll": 12345
}{ "retry": [], "dead_letter": [] }{ "down": true }{ "ok": true, "down": true }Com a carteira "fora", créditos vão para a fila de emergência e o jogador recebe wallet_status: "QUEUED"; quando volta, o worker credita e envia WALLET_CREDITED.
{ "min_bet": 2, "max_bet": 200, "max_multiplier": 300 }{ "ok": true, "min_bet": 2, "max_bet": 200, "max_multiplier": 300 }Vale já para as próximas apostas; propagado em LIMITS_CONFIG. Se max_bet < min_bet, o máximo é corrigido para o mínimo.
{ "house_bankroll": 250000.5 }{ "ok": true, "house_bankroll": 250000.5 }Não altera resultados — apenas mantém o caixa exibido não-negativo.
{ "enabled": true, "cycle": 1000 }/api/verify (o verificador informa o motivo). Use apenas em teste/demonstração — em produção regulada mantenha desligado. Todo uso fica registrado no log de auditoria do admin.{ "crash": 50.25 } // ou { "clear": true }forced: true no ROUND_BETTING) e não verifica no provably fair. Auditoria registra quem forçou.{
"colors": { "green": "#00ff00" },
"images": { "logo": "https://… ou data:image/… ou /theme-assets/…" },
"texts": { "brand": "Minha Marca" },
"layout": { "feedW": 270, "headerH": 40, "gap": 6, "stageMaxW": 0 },
"multColors": [{ "min": 1, "max": 2, "color": "#913ef8" }, { "min": 2, "max": null, "color": "#34b4ff" }],
"options": { "showAdmin": true, "betButtons": 2, "showSpinner": true },
"rawCss": ".stage{border-radius:24px}",
"reset": false
}{ "mime": "image/png", "data": "<base64>" } // png/jpg/gif/webp/svg/avif, até 4MB
→ { "ok": true, "url": "/theme-assets/a1b2c3d4….png" }Tudo é sanitizado: cores hex, imagens com esquema seguro, CSS sem <. betButtons clampa 1–3.
{
"name": "Meu Cassino", // obrigatório
"key": "meu-cassino", // opcional (gera slug se omitir)
"webhook_url": "https://…", // opcional, armazenado para referência
"token": "…", "secret": "…", // credenciais externas, armazenadas
"currency": "BRL", "active": true
}{
"ok": true,
"agent": {
"id": "7f7238e7-…", "key": "meu-cassino", "name": "Meu Cassino",
"webhook_url": null, "token": null, "secret": null,
"currency": "BRL", "active": true,
"link_template": "http://127.0.0.1:7777/?agent=meu-cassino&user={user}&token={token}&operator=real¤cy=BRL&lang=pt"
}
}link_template usa os marcadores {user} e {token} — o seu sistema substitui pelo user_ref e session_token do jogador. No modo padrão (wallet_mode: "rgs") o webhook_url é apenas armazenado e o saldo vive no RGS (ver Integração). No modo seamless ele recebe os callbacks de carteira (ver Seamless).{ "name": "Novo Nome", "active": false, "currency": "BRL" }Excluir um agente remove também todos os seus clientes. 409 se a nova key já existir.
GET /admin/api/clients?agent_id=7f7238e7-…
{
"agent_id": "7f7238e7-…", // obrigatório
"user_ref": "jogador1", // ID único do jogador no sistema do operador
"name": "Jogador 1",
"session_token": "sess_…", // opcional (gera um se omitir) — usado no link/launch
"initial_balance": 50, // BRL
"currency": "BRL", "active": true
}{
"ok": true,
"client": {
"id": "461b4d00-…", "pid": "cli_cf7639bafc2e", "agent_id": "7f7238e7-…",
"user_ref": "jogador1", "name": "J1", "session_token": "sess_1791…",
"balance_cents": 5000, "currency": "BRL", "active": true,
"link": "http://127.0.0.1:7777/?agent=meu-cassino&user=jogador1&token=sess_1791…&operator=real¤cy=BRL&lang=pt"
}
}O link pronto abre o painel já logado no jogador. 409 se o user_ref já existir no agente.
{ "user_ref": "novo", "name": "…", "session_token": "…", "currency": "BRL", "active": true }{ "add": 8, "note": "bônus boas-vindas" } // ou { "set": 100 }{ "ok": true, "client_id": "461b4d00-…", "balance": 58 }Se o jogador estiver conectado, recebe WALLET_CREDITED na hora (com source: "admin").
{
"tx": [{
"id": "c201cf7f-…", "client_id": "461b4d00-…", "kind": "admin_credit",
"amount_cents": 800, "balance_after_cents": 5800,
"ref": null, "note": null, "created_at": "2026-10-13T16:42:25.478Z"
}]
}kind possível: bet, win, admin_credit, admin_set. ref traz a chave de idempotência (ex.: credit:<bet_id>).
key e o link_template.user_ref do SEU sistema, um session_token gerado por você e o saldo inicial → recebe o link pronto do jogador.POST /api/launch sozinho, valida o session_token e conecta no WebSocket com o saldo do RGS./admin/api/clients/:id/tx) e pelos logs (/admin/api/bets?player_id=cli_…). Créditos/débitos por rodada ficam no RGS com idempotência por aposta.wallet_tx + lançamentos ligados por parent_bet_id) e não dispara callbacks para o operador. O campo webhook_url do agente fica armazenado para referência/contato. Se precisar espelhar saldos no seu sistema, use os endpoints de extrato e logs.POST /admin/api/clients/:id/balance com add — é idempotente por lançamento, notifica o jogador online e cai no extrato com note para conciliação.Agente criado com "wallet_mode": "seamless" (exige webhook_url e secret): o saldo do jogador fica na plataforma do operador. Cada aposta e cada prêmio viram uma chamada assinada para o webhook_url; o RGS só guarda uma cópia do último saldo informado e o lançamento em wallet_tx para auditoria. Ajuste manual de saldo no RGS é recusado (409) para esses clientes.
X-GoDds-Agent: <key> e X-GoDds-Signature: sha256=<hex HMAC-SHA256(secret, corpo bruto)>. O corpo JSON leva ts (epoch ms); diferença maior que 5 min é recusada.Cria o cliente na primeira vez (por user) e gera um session_token novo a cada chamada (o anterior deixa de valer).
{ "user": "7", "name": "Jogador", "currency": "BRL", "ts": 1792170000000 }{
"ok": true, "player_id": "cli_3f9a…", "wallet_mode": "seamless",
"url": "https://jogo…/?agent=godds-club&user=7&token=sess_…&operator=real¤cy=BRL&lang=pt",
"query": "agent=godds-club&user=7&token=sess_…&operator=real¤cy=BRL&lang=pt",
"session_token": "sess_…"
}Erros: 404 INVALID_AGENT, 401 INVALID_SIGNATURE, 400 USER_REQUIRED, 403 CLIENT_INACTIVE.
{ "action": "balance", "user": "7", "game": "aviador", "ts": … }
{ "action": "debit", "user": "7", "amount": 10, "txn_id": "debit:<bet_id>", "round_id": "…", "bet_id": "…", "game": "aviador", "ts": … }
{ "action": "credit", "user": "7", "amount": 15, "txn_id": "credit:<bet_id>", "round_id": "…", "bet_id": "…", "multiplier": 1.5, "game": "aviador", "ts": … }
{ "action": "rollback", "user": "7", "amount": 10, "txn_id": "rollback:<bet_id>", "ref_txn_id": "debit:<bet_id>", "round_id": "…", "bet_id": "…", "game": "aviador", "ts": … }{ "ok": true, "balance": 105.00 }
{ "ok": false, "error": "INSUFFICIENT_FUNDS", "balance": 4.20 }
{ "ok": false, "error": "BET_REFUSED", "message": "limite de jogo responsável" }txn_id é a chave de idempotência: a mesma chamada repetida deve devolver o saldo sem lançar de novo.ok. Recusa vira WALLET_DEBIT_FAILED ou BET_REFUSED no WebSocket.txn_id) até confirmar.ALLOW_RESULT_CONTROL=1 (uso em teste). Sem a variável, uma compensação que estava ligada é desligada ao iniciar.Serve legacy/panel.legacy.html.
event: crash
data: {
"roundId": "…", "crashPoint": 2.96, "serverSeed": "…", "commit": "…",
"clientSeed": "…", "nonce": 1554, "wallet": 10004.5,
"apostas": { "a": { "valor": 10, "cashedAt": 1.3, "pagamento": 13 }, "b": null },
"totalApostado": 30, "totalPago": 19.5, "deltaCaixa": 10.5, "caixa": 99990.5,
"botsGanhos": 18, "botsTotal": 50, "forced": false, "cooldownMs": 3000
}{ "slot": "a", "valor": 10 } // /api/bet
{ "slot": "a" } // /api/cashout{ "ok": true, "slot": "a", "wallet": 9990, "bet": 10, "bet_id": "…" }
{ "ok": true, "slot": "a", "cashoutEm": 1.3, "pagamento": 13, "wallet": 10003 }Erros voltam como { "erro": "…" } com HTTP 200 (mesma semântica da versão antiga).
POST /api/bots {min, max} — exige admin token.POST /api/config {edge} — exige admin token; 403 se HOUSE_EDGE estiver definido no ambiente (trava de compliance).POST /api/force {crash} — só existe com NODE_ENV=test; em produção cai no 404.| Variável | Padrão | Descrição |
|---|---|---|
PORT / HOST | 7777 / 127.0.0.1 | Endereço HTTP + WS |
DATABASE_URL | — | PostgreSQL; sem ela usa SQLite em data/godds_crash.db |
REDIS_URL | — | Fila de failover no Redis; sem ela, em memória |
AUTH_SECRET / ADMIN_TOKEN | gerados | Segredos — gerados e salvos no banco se não definidos |
HOUSE_EDGE | 3 | Edge em % (RTP = 100 − edge); definido no env trava alteração em runtime |
DISCONNECT_POLICY | EMERGENCY_CASH_OUT | ou LOST_ON_CRASH |
GRACE_MS | 5000 | Tempo em GRACE_PERIOD antes de DISCONNECTED |
WALLET_FAILURE_RATE | 0 | 0–1 — simula carteira instável (teste de failover) |
ALLOW_RESULT_CONTROL | 0 | 1 libera forçar resultado / compensação de RTP (só teste — quebra o provably fair) |
PUBLIC_URL | host da requisição | URL pública do jogo usada nos links do jogador (ex.: quando o operador chama o RGS por um endereço interno) |