Webhook, eventos e reconciliação na Nero

Para: Dev — Conferido em 2026-10-18.

Três rotinas mantêm o livro da Nero igual ao do Dracma: a rota do webhook, o job de GET /events e a reconciliação diária. Todas terminam no mesmo aplicador de estado.

O que é

flowchart LR W["Webhook: POST /api/payments/dracma/webhook"] --> A["Aplicador único"] E["Job a cada 5 min: GET /v1/events com after"] --> A R["Reconciliação diária às 02:30 UTC: GET /v1/reconciliation"] --> X["Comparação com o nero_ledger"] A --> FB["Fetch-back em /v1/deposits/id ou /v1/withdrawals/id"] FB --> L["nero_ledger: crédito, finalização ou devolução, idempotente por external_id"] X -->|"divergência"| I["Incidente e alerta"]

O aplicador é o ponto único da regra de dinheiro: dedup, fetch-back, coerência com o intent, versão e efeito idempotente. O desenho geral está em Arquitetura do conector.

Quando usar

Ao implementar o receptor e os jobs do nero_payments, e ao revisar o alerta de cada um.

Como funciona

Rota do webhook

POST /api/payments/dracma/webhook passa pelo portal sem autenticação de jogador. A defesa é:

  1. Assinatura verificada no nero_payments sobre o corpo cru (HMAC-SHA256 com o segredo de webhook, que é diferente do segredo de chamada).
  2. Fetch-back antes de mover dinheiro: um segredo de webhook vazado sozinho não cria dinheiro.
  3. Caddy da bet: o Dracma não publica lista de IPs de saída hoje; se um dia vier por contrato, aplique-a só nessa rota. É uma camada extra, não substitui as duas anteriores.

A ordem de verificação (13 passos) e a tabela "você responde X, o Dracma faz Y" estão em Receptor passo a passo. Na Nero:

  • Responda em até 10 segundos. Se o trabalho for pesado, grave o evento de forma durável e responda 200; um worker processa.
  • Nunca siga redirect nem responda 3xx: o Dracma trata como falha.
  • Tipo de evento ou estado desconhecido: 200, grava para dedup, não move dinheiro.
  • Métrica nero_dracma_webhook_verify_total{result} com os resultados ok, bad_signature, stale, mismatch_event_id e duplicate. Alerta em bad_signature maior que 0, que pode ser ataque ou rotação mal feita.

Os clientes Go e Node de Exemplos executáveis já implementam a verificação de assinatura e passam nos vetores.

Job de GET /events a cada 5 minutos

a cada 5 min (um único executor ativo; lock no banco):
  cursor = ler_cursor("dracma_events")          # último evt APLICADO; nulo no primeiro run
  repetir até 20 páginas:
    r = GET /v1/events?after={cursor}&limit=200  # sem after no primeiro run
    se r.status == 429: esperar Retry-After; continuar
    se r.status >= 500 ou erro de rede: parar (tenta no próximo ciclo)
    para ev em r.data:
      aplicar_evento(ev)                         # MESMO aplicador do webhook, a partir do dedup
      cursor = ev.id; salvar_cursor(cursor)      # só depois de aplicar
    se r.next_cursor == nulo: parar
  • after tem de ser um evento da operação da Nero; id desconhecido dá 400 com reason: unknown. Guarde o cursor no banco, nunca em memória.
  • GET /events está no balde de listagem (5 req/s por chave): 20 páginas de 200 eventos por ciclo é folgado.
  • Os eventos de /events não têm assinatura própria: a autenticidade vem da chamada autenticada da Nero.
  • Métrica nero_dracma_events_lag_seconds (agora menos o created_at do último aplicado). Alerta se o job não roda com sucesso há mais de 15 minutos.
  • O poller pronto, com cursor, repetição e teste, está em Exemplos executáveis.

Reconciliação diária

todo dia às 02:30 UTC, para D = ontem (UTC):
  itens = []; cursor = nulo
  repetir:
    r = GET /v1/reconciliation?date=D&currency=BRL&limit=200[&cursor=...]
    se r.status == 409 (day_open): reagendar para depois do Retry-After; parar
    itens += r.items; cursor = r.next_cursor
  até cursor == nulo
  exigir sha256(linhas ordenadas por (object_id, kind)) == r.digest          # senão incidente
  exigir Dracma-Signature da última página válida com o segredo de webhook   # senão incidente
  comparar com o nero_ledger:
    todo deposit paid tem exatamente 1 crédito com o mesmo external_id e valor
    todo crédito da Nero tem 1 deposit paid                                  # crédito sem par é o mais grave
    todo withdrawal paid corresponde a uma reserva finalizada, e vice-versa
  guardar o relatório e o header de assinatura como prova
  • Divergência é incidente: não "conserte" o livro para bater. Congele o que for preciso e investigue pelo trace_id e por GET /v1/events?object_id=.
  • Compare pelo settled_at (UTC) do Dracma, não pela data local de Brasília: itens perto da meia-noite UTC caem no dia seguinte do calendário local.
  • Digest e VerifyDigest já estão nos exemplos Go e Node.
  • Métrica nero_dracma_reconciliation_mismatch_total; alerta crítico em qualquer valor maior que 0.

Alertas mínimos da Nero

Os alertas genéricos e a razão de cada um estão em Alertas do conector. Os limiares da Nero:

Sinal Limiar Severidade
nero_dracma_outbox_pending com intent mais velho que 10 minutos P2
Webhook com assinatura inválida Mais de 0 em 5 minutos P2
401 ou 403 nas chamadas da Nero ao Dracma Mais de 0 P2 (credencial, relógio ou operação suspensa)
Desvio de relógio (server_time de GET /v1/ping) Mais de 5 segundos P2
Job de /events sem sucesso Mais de 15 minutos P2
Fila needs_decision com caso parado Mais de 4 horas aviso, mais de 24 horas crítico (D11) P2, depois P1
nero_dracma_fetchback_mismatch_total Mais de 0 P1
Reconciliação diária com divergência Mais de 0 P1

Exemplos

O poller e o parser de erro prontos estão em Exemplos executáveis. Para provar a rota do webhook, use a Receita 10 (reentrega) e a Receita 11 (webhook forjado).

Erros e casos de borda

  • O job e o webhook processam o mesmo evento: o dedup por id do evento é o que impede o crédito duplo.
  • Evento que chega fora de ordem: aplique só a versão maior que a guardada no intent.
  • Fetch-back responde 404: responda 200 sem efeito. Rede ou 5xx: responda 503, e o Dracma reentrega.

Veja também