Arquitetura do conector

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

O conector é o serviço do seu lado que fala com o Dracma. Esta página mostra como desenhá-lo para que uma falha de rede, um reinício ou uma queda do Dracma nunca dupliquem nem percam dinheiro, e nunca derrubem o seu produto.

O que é

O conector é o único serviço do operador que guarda a credencial do Dracma e chama a API. O resto do seu produto (front, portal, carteira) fala com o conector, nunca com o Dracma.

flowchart LR J["Jogador"] --> P["Seu produto: front e portal"] P --> C["Conector"] C <--> L["Seu livro: carteira do jogador"] C -->|"chamada assinada com Idempotency-Key"| D["API v1 do Dracma"] D -->|"webhook assinado"| C C -->|"fetch-back, GET /events, GET /reconciliation"| D

O conector é dono de quatro coisas:

  1. A credencial (chave e segredo), guardada num arquivo legível só pelo usuário do serviço (Segredos e vazamento).
  2. O outbox: toda chamada de dinheiro ao Dracma nasce como uma linha gravada antes da chamada.
  3. O aplicador de estado: um único caminho de código que transforma "o Dracma diz que isto aconteceu" em efeito no seu livro.
  4. Os jobs: recuperação de eventos, reconciliação diária e alertas.

Quando usar

Use este desenho em qualquer produto que mova saldo de jogador com base no Dracma. Se o seu produto já tem um serviço de pagamentos, ele é o conector: acrescente o outbox e o aplicador único em vez de criar um segundo serviço.

Como funciona

Outbox e reserva na mesma transação

O Dracma garante que a mesma chave nunca cria dois objetos (a resposta de 2xx e 4xx fica guardada por 30 dias; 5xx e 429 liberam a chave). Mas só você sabe qual chave usar de novo depois de um reinício. Por isso, o intento é gravado antes da chamada:

Coluna sugerida Conteúdo
external_id dep_<uuidv7> ou wdr_<uuidv7>; é também a Idempotency-Key
kind deposit ou withdrawal
player_ref, amount_minor, currency O que foi pedido. É contra isso que o fetch-back é conferido
body Os bytes exatos do corpo enviado. A repetição manda os mesmos bytes
state pending, acknowledged, rejected ou unknown
attempts, next_attempt_at, last_trace_id Controle de repetição e diagnóstico (Dracma-Trace-Id)
dracma_id, dracma_status, dracma_version Último estado aplicado: id do Dracma e version monotônica
stateDiagram-v2 [*] --> pending: gravado na transação do pedido (e da reserva, no saque) pending --> acknowledged: 2xx ou Idempotent-Replayed pending --> rejected: 4xx conclusivo pending --> pending: timeout, 5xx, 429 ou idempotency_in_progress, com backoff e a mesma chave pending --> unknown: tentativas esgotadas sem resposta conclusiva unknown --> acknowledged: GET por external_id achou o objeto unknown --> needs_decision: GET por external_id inconclusivo rejected --> [*] acknowledged --> [*]

Regras do outbox:

  • No saque, a reserva do saldo do jogador e a linha do outbox entram na mesma transação do seu banco. Assim não existe saque chamado sem reserva, nem reserva sem registro de que o saque deve ser chamado.
  • A reserva só volta ao jogador em estado conclusivo: rejected, failed ou cancelled lido por fetch-back, ou 4xx conclusivo na criação. Timeout, 5xx e 429 não devolvem reserva: você não sabe se o saque foi criado. Devolver nesse caso paga o jogador duas vezes.
  • Backoff exponencial com jitter, respeitando o Retry-After. Cada tentativa é uma requisição nova (timestamp, nonce e assinatura novos), com a mesma chave e os mesmos bytes. Veja o helper de repetição em Exemplos executáveis.
  • Um worker que reinicia retoma do outbox com a mesma chave. Nunca duplica.
  • Esgotou as tentativas: consulte GET /v1/deposits?external_id= ou GET /v1/withdrawals?external_id=. Achou, siga pelo objeto. Não achou ou inconclusivo, abra uma decisão manual (Política de exceções de dinheiro). Nunca decida às cegas.

Se o Dracma cair, o seu produto continua

A indisponibilidade do Dracma é uma situação normal de operação, não uma falha do seu produto. O conector tem de subir e funcionar sem o Dracma, e o resto do produto (login, jogo, carteira) tem de continuar de pé.

Situação O que o conector faz O que o jogador vê
Dracma responde 503, 429 ou não responde, na criação de depósito Grava o intento, repete com a mesma chave, devolve ao front um estado de "criando" "Gerando seu PIX..." e polling do estado local
Dracma indisponível na criação de saque Mantém a reserva, repete com a mesma chave "Saque em processamento."
Webhook não chega O job de GET /v1/events recupera (Recuperação com GET /events) Nada; o estado só atrasa
O conector reinicia no meio de uma chamada O worker retoma do outbox Nada
Operação Real ainda sem credencial (antes do go-live) O conector sobe, e as rotas de dinheiro respondem "indisponível" do seu produto "Depósito indisponível no momento."
  • Nunca faça o boot do conector depender de uma chamada ao Dracma. Valide a configuração, abra o banco e suba; carregue GET /v1/currencies e GET /v1/rules em cache com tolerância a falha.
  • Falha ao chamar o Dracma: persista, registre com o trace_id, alerte e retome. Não perca o intento e não trave os outros pagamentos por causa de um.
  • O polling do front lê o estado local do conector. Ele não é um proxy do Dracma: o front nunca bate no Dracma.

Você pode ensaiar tudo isso com a queda simulada (Receita 9).

Jobs

Job Cadência O que faz Detalhe
Receptor do webhook Contínuo Verifica, deduplica, faz o fetch-back e aplica Receptor passo a passo
Poller de GET /v1/events A cada 5 minutos Recupera o que o webhook perdeu, pelo mesmo aplicador Exemplos executáveis
Worker do outbox Contínuo Repete as chamadas pendentes com a mesma chave Acima
Reconciliação diária Uma vez por dia, depois do fechamento do dia UTC Compara o relatório do Dracma com o seu livro Reconciliar com o seu livro
Verificação de relógio Periódica GET /v1/ping e alerta com desvio acima de 5 s Alertas do conector

Os quatro primeiros terminam no mesmo aplicador de estado: dedup por id do evento, fetch-back, conferência com o intento, version maior que a aplicada, efeito idempotente por external_id. Um só lugar para a regra de dinheiro evita que o webhook e o job discordem.

Regras de bônus ficam fora da API

O Dracma não conhece bônus, rollover, promoção nem cashback. O corpo de POST /v1/deposits e de POST /v1/withdrawals leva só o que o Dracma precisa decidir e mover: valor, jogador, método e destino. Campo desconhecido é recusado com 400 validation_failed e reason: unknown_field.

  • Se o seu front manda uma opção de bônus no depósito (por exemplo, "aceitar bônus"), guarde-a no seu intento e não a envie ao Dracma.
  • Aplique a regra de bônus no seu livro depois que o depósito estiver paid e creditado, como um segundo lançamento seu, idempotente pelo mesmo external_id.
  • Regras de saque ligadas a bônus (rollover, saldo bloqueado) são checadas por você antes de reservar. Se quiser que o Dracma mande o saque para revisão por desconfiança sua, use review_requested: true, que só torna a decisão mais restritiva (Decisão, limites e regras).

Quem garante o quê

Garantia Quem
Não criar duas cobranças nem dois saques para a mesma chave Dracma, desde que você repita com a mesma chave
Decidir saque automático, revisão ou bloqueio Dracma
Creditar uma vez, e só em paid lido por fetch-back Você
Reservar antes e devolver só em estado conclusivo Você
Não perder evento Os dois: o Dracma reentrega e mantém GET /v1/events; você roda o job
Provar que os dois livros batem Os dois: reconciliação diária com digest assinado

Exemplos

Depósito, do pedido do jogador ao crédito:

sequenceDiagram autonumber participant J as Jogador participant C as Conector participant O as Outbox e livro participant D as Dracma J->>C: pede depósito de 50,00 C->>O: grava intento (external_id, valor em centavos) na transação C->>D: POST /v1/deposits (Idempotency-Key = external_id) D-->>C: 201 pending com instructions.pix C-->>J: PIX para pagar D->>C: webhook deposit.paid (assinado) C->>C: verifica assinatura e deduplica C->>D: GET /v1/deposits/{id} (fetch-back) D-->>C: status paid, valor e jogador C->>O: confere com o intento e credita uma vez, na mesma transação do dedup C-->>D: 200

Os exemplos prontos, com retry, parser de erro, paginação e poller, estão em Exemplos executáveis. Um caso real, com este desenho completo, está na seção Casos.

Erros e casos de borda

Situação Tratamento
O processo morre entre o commit do intento e a chamada O worker repete do outbox com a mesma chave: 1 objeto
A resposta 201 se perde A repetição devolve a mesma resposta com Idempotent-Replayed: true
409 duplicate_external_id O objeto já existe: siga pelo existing da resposta
409 idempotency_key_reused Bug do conector (mesma chave, corpo diferente): abra decisão manual e alerte; não devolva reserva nem reenvie
409 idempotency_in_progress Outra execução segura a chave: repita depois do Retry-After
4xx de validação O objeto não existe: marque rejected, devolva a reserva (saque) e corrija. Nova tentativa depois da correção usa um novo external_id
Estado desconhecido no fetch-back Não move dinheiro; registre e trate como "em análise"

Veja também