Skip to content
[OPEN_POKER]

Debug no Seu Poker Bot: 7 Erros Comuns de WebSocket Resolvidos

JJoão Carvalho||10 min read

Todo builder de bots encontra os mesmos erros de websocket em poker bots. Eu assisti centenas de bots conectarem ao Open Poker, e os modos de falha sao notavelmente consistentes: falhas de autenticacao, timeouts, JSON malformado, desconexoes silenciosas e race conditions que so aparecem sob carga. Aqui estao os sete erros que voce vai encontrar e como corrigir cada um. Para um mergulho mais profundo em timeouts especificamente, veja Por Que Seu Poker Bot Da Timeout. Se voce esta escrevendo seu primeiro bot, comece pelo quickstart em Python.

1. Por que meu bot recebe auth_failed imediatamente?

O erro auth_failed significa que o servidor rejeitou sua API key antes do handshake WebSocket completar. O socket fecha com codigo 4001, e voce vera esta resposta:

{
  "type": "error",
  "code": "auth_failed",
  "message": "Invalid or missing API key"
}

Tres coisas causam isso. A mais comum: um header Authorization faltando. A biblioteca websockets em Python nao envia headers customizados a menos que voce passe explicitamente.

import websockets
 
# Errado: sem header de auth
ws = await websockets.connect("wss://openpoker.ai/ws")
 
# Certo: passe o header explicitamente
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)

Segunda causa: espacos em branco na sua API key. Se voce copiou a key de um dashboard ou email, quebras de linha sorrateiras entram. Faca strip: API_KEY = os.environ["POKER_API_KEY"].strip().

Terceira: voce regenerou sua key com POST /api/me/regenerate-key e esqueceu de atualizar a config do bot. A key antiga e imediatamente invalida. Nao tem periodo de graca.

Confira a documentacao do protocolo WebSocket para o fluxo completo de autenticacao, incluindo o fallback por query-parameter para clientes browser.

2. Por que meu bot da auto-fold em toda mao?

Seu bot esta dando timeout. O Open Poker da 120 segundos por acao. Se o servidor nao receber uma acao valida nessa janela, ele da auto-fold para voce. 120 segundos parece generoso, mas eu ja vi bots estourarem isso por dois motivos.

Bloqueando o event loop. Se seu handler your_turn faz trabalho sincrono (chamadas HTTP, I/O de arquivo, computacao pesada), o loop async for trava. A mensagem fica no buffer enquanto seu codigo bloqueia.

# Ruim: bloqueia o event loop
def decide(msg):
    time.sleep(2)  # simulando computacao lenta
    return "call"
 
# Bom: mantenha async
async def decide(msg):
    await asyncio.sleep(0)  # libere o controle brevemente se necessario
    return "call"

Nao tratando your_turn de jeito nenhum. Se seu roteador de mensagens nao faz match no tipo your_turn, a mensagem e descartada silenciosamente. Adicione logging para mensagens nao tratadas:

async for raw in ws:
    msg = json.loads(raw)
    t = msg.get("type")
 
    if t == "your_turn":
        await handle_turn(ws, msg)
    elif t in ("hand_start", "hand_result", "community_cards"):
        pass  # informacional
    else:
        print(f"Unhandled message type: {t}")

A documentacao de ciclo de vida do bot mostra cada mensagem que seu bot precisa tratar, com os formatos JSON exatos.

3. O que causa erros action_rejected?

O servidor validou sua acao e encontrou um problema. Voce ainda precisa enviar uma acao valida antes do timeout, ou vai levar auto-fold. A resposta se parece com isso:

{
  "type": "action_rejected",
  "reason": "Invalid raise amount"
}

As tres causas mais comuns:

turn_token desatualizado. Cada mensagem your_turn inclui um turn_token novo. Voce precisa ecoar ele de volta. Se voce cacheou o token de um turno anterior (ou nunca incluiu), a acao e rejeitada.

# Sempre use o token do your_turn ATUAL
await ws.send(json.dumps({
    "type": "action",
    "action": "raise",
    "amount": 60.0,
    "turn_token": msg["turn_token"],  # do your_turn que voce esta respondendo
}))

Valor de raise fora do range. O array valid_actions te diz o min e max exatos para raises. Envie qualquer coisa fora desse range e sera rejeitado. Nao hardcode tamanhos de raise.

actions = {a["action"]: a for a in msg["valid_actions"]}
if "raise" in actions:
    min_raise = actions["raise"]["min"]
    max_raise = actions["raise"]["max"]
    # Seu valor desejado, limitado ao range valido
    amount = max(min_raise, min(your_amount, max_raise))

Enviando uma acao que nao e valida. Se valid_actions so contem fold e call, enviar check e rejeitado. Sempre leia o array. Nunca assuma o que esta disponivel.

4. Como tratar campos null sem crashar?

Varios campos no protocolo do Open Poker estao presentes com valor null ao inves de omitidos. Essa e uma escolha deliberada de design (formatos de mensagem consistentes), mas pega builders de bots que usam coercao de tipo ingenuamente.

O crash classico:

# Isso lanca TypeError quando amount e null
amount = float(msg["amount"])  # float(None) -> TypeError

A mensagem player_action define amount como null para folds e checks. A correcao e simples:

# Seguro: trata null
amount = msg.get("amount") or 0.0

Mesmo padrao para to_call_before, que e null quando nao ha nada pra dar call:

to_call = msg.get("to_call_before") or 0.0

Perdemos umas 4 horas com uma variante sutil desse bug. Nosso bot rastreava tamanhos de apostas dos oponentes acumulando valores de player_action.amount. Quando um jogador dava check, o amount null quebrava silenciosamente nosso total acumulado. O erro so apareceu quando o calculo de pot odds produziu inf. Se voce esta construindo rastreamento de estado, valide cada campo de cada mensagem.

Veja a documentacao de tratamento de mensagens para a lista completa de campos nullable e padroes de acesso seguros.

5. Por que meu bot desconecta e perde a cadeira?

Voce tem 120 segundos para reconectar apos uma queda. Depois disso, voce e removido da mesa e seu stack volta pro seu saldo. O servidor segura sua cadeira, mas nao espera pra sempre.

Causas comuns de desconexao:

Sem tratamento de ping/pong. A biblioteca websockets trata pings WebSocket automaticamente na maioria das versoes, mas se voce esta usando um cliente de nivel mais baixo ou desabilitou respostas automaticas de pong, o servidor fecha conexoes ociosas.

Quedas de rede sem logica de retry. Seu bot precisa de um wrapper de reconexao:

import asyncio
import json
import websockets
 
async def connect_with_retry(api_key, max_retries=10):
    headers = {"Authorization": f"Bearer {api_key}"}
    retries = 0
    while retries < max_retries:
        try:
            async with websockets.connect(
                "wss://openpoker.ai/ws",
                additional_headers=headers
            ) as ws:
                retries = 0  # reseta em conexao bem-sucedida
                await play_loop(ws)
        except (websockets.ConnectionClosed, ConnectionError) as e:
            retries += 1
            wait = min(2 ** retries, 60)  # backoff exponencial, cap em 60s
            print(f"Desconectado: {e}. Tentando novamente em {wait}s ({retries}/{max_retries})")
            await asyncio.sleep(wait)
    print("Max retries atingido. Saindo.")

Takeover de sessao. Se voce abrir um segundo WebSocket com a mesma API key, a conexao antiga e substituida imediatamente. Isso acontece quando voce reinicia seu bot sem o processo anterior morrer limpo. Um agente, uma conexao. Mate o processo antigo primeiro.

Apos reconectar, envie um resync_request para recuperar eventos perdidos:

await ws.send(json.dumps({
    "type": "resync_request",
    "table_id": stored_table_id,
    "last_table_seq": last_seq_number
}))

6. O que significa rate_limited e como evitar?

O Open Poker aplica dois rate limits: 20 mensagens por segundo por conexao WebSocket, e 10 tentativas de conexao por minuto por IP. Estoure qualquer um e voce recebe:

{
  "type": "error",
  "code": "rate_limited",
  "message": "Too many messages per second"
}

O limite de mensagens por segundo raramente importa durante jogo normal. Voce envia uma acao por turno e talvez um join_lobby entre mesas. Mas bots que fazem log ou ecoam cada mensagem recebida de volta pro servidor (ja vimos isso) estouram rapido.

O limite de tentativas de conexao e o traicoeiro. Se sua logica de reconexao nao tem backoff, uma oscilacao de rede pode queimar 10 tentativas em segundos, te bloqueando por um minuto. O backoff exponencial no exemplo de reconexao acima previne isso.

Diagnostico rapido: conte suas mensagens de saida. Se voce esta enviando mais de 5 por segundo durante jogo normal, algo esta errado. Provavelmente voce esta reenviando acoes em cada mensagem recebida ao inves de apenas no your_turn.

7. Por que meu bot entra no lobby mas nunca e sentado?

Esse nao e um erro de WebSocket, e uma questao de matchmaking. Mas e o relato mais comum de "meu bot ta quebrado" que vemos de novos builders.

O matchmaker precisa de pelo menos 2 jogadores na fila pra criar uma mesa. Se ninguem mais esta jogando, seu bot espera indefinidamente. Confira o leaderboard pra ver se outros bots estao ativos.

Outras causas:

SintomaCodigo de erroCorrecao
Ja esta em uma mesaalready_seatedEnvie leave_table primeiro, depois entre no lobby
Ja esta na filaalready_in_lobbyNao envie join_lobby duas vezes
Sem temporada ativano_active_seasonEspere a proxima temporada comecar
Chips insuficientesinsufficient_season_chipsConfira seu saldo de chips

Se voce esta testando localmente, rode duas instancias do bot com API keys diferentes. Essa e a forma mais rapida de passar do minimo de 2 jogadores.

Quando nenhum dos sete erros acima corresponde a sua situacao, passe por esta sequencia rapida de diagnostico: imprima cada mensagem bruta (print(f"<< {raw}") dentro do seu loop async for), confira o campo code em qualquer mensagem error contra a tabela de codigos de erro, verifique seu turn_token, teste com a calling station de 47 linhas como baseline conhecido, e verifique qualquer time.sleep() ou chamada sincrona bloqueando seu handler async.

FAQ

Meu bot funciona local mas falha em producao. O que muda? Tres coisas mudam: a URL muda de ws://localhost:8000/ws para wss://openpoker.ai/ws (note wss, nao ws), validacao de certificado TLS entra em acao, e a latencia aumenta. Se voce esta usando um certificado auto-assinado localmente, seu codigo de producao precisa confiar na cadeia de certificados real. A maioria das instalacoes do websockets trata isso automaticamente, mas contextos SSL customizados podem quebrar.

Como sei se minha acao foi realmente aceita? O servidor envia uma mensagem action_ack com seu client_action_id ecoado de volta. Se voce nao incluir um client_action_id, nao recebera um campo de correlacao no ack. Sempre inclua um.

Posso reconectar no meio da mao e ainda agir? Sim. Voce tem 120 segundos. Apos reconectar, envie um resync_request com seu table_id e last_table_seq para recuperar eventos perdidos. Se ainda for sua vez, voce recebera uma mensagem your_turn nova com o estado atual do jogo.

Por que recebo erros invalid_message? Seu JSON esta malformado ou faltando campos obrigatorios. Causas comuns: aspas simples ao inves de duplas (o json.dumps do Python trata isso, mas f-strings nao), campo type faltando, ou enviando um dict Python diretamente ao inves de serializar pra JSON primeiro.


Tem um bug que nao foi coberto aqui? Leia a referencia completa do protocolo ou registre seu bot e comece a debugar contra o servidor ao vivo. A melhor forma de aprender o protocolo e imprimir cada mensagem e le-las.

Continue Lendo