# Deep audit DICT: mapa, conformidade com o manual do BACEN e desenho do monitoramento

Data: 26/07/2026. Escopo: familia DICT inteira da cabine PIX, confrontada com
tres fontes independentes, mais a avaliacao do port do Monitoramento DICT do
`coreproviders`.

## 1. Fontes usadas, e o peso de cada uma

| Fonte | O que vale | Onde esta |
|---|---|---|
| **Manual Operacional do DICT 8.2** | verdade normativa, numeros citaveis | `owem/evidence/bacen-2026-06-04-claude-handoff/manual-operacional-dict-oficial-bcb.txt` |
| **Manual de Seguranca do PIX** | obrigacao do participante (secao 6.6) | `monetarie/md/Manual_de_Seguranca_PIX.md` |
| **Fixtures oficiais do BACEN** | forma exata de request e response | `pix/backend/apps/shared/test/fixtures/dict/v2.10` e `v2.11` (64 arquivos) |
| **LegadoPIX decompilado** | regras de negocio que rodaram em producao | `ilspycmd` sobre `DICT.Core.{Application,Infrastructure,General,Domain}` e `DICT.Web.Api.Simulador` |
| **Banco de producao** | o que de fato existe e o que nunca foi usado | RPC nas tasks ECS |
| **`coreproviders`** | referencia de implementacao, nao de norma | `backend/lib/fluxiq/{workers,use_cases/pix/dict_monitoring}` |

Regra que usei o tempo todo: **simulador nao e fonte**. O `LoadListPolicies()`
do simulador do legado carrega numeros que eu ja citei por engano como se
fossem do BACEN, e isso foi retratado. Numero so entra no codigo com manual,
fixture ou resposta assinada do proprio BACEN atras.

## 2. Superficie de operacoes: completa

Cruzei as 36 operacoes que o simulador do legado expoe (que e a API do DICT como
ele a implementou) contra as 41 funcoes publicas do nosso `DictClient`.

| Familia | Operacoes | Status |
|---|---|---|
| Entries | Create, Get, Update, Delete | completo |
| Claims | Create, List, Get, Acknowledge, Confirm, Cancel, Complete | completo |
| Infraction reports | Acknowledge, Cancel, Close, Get, List | completo |
| Fraud markers | Create, Get, List, Cancel | completo |
| Refunds | Create, Get, Close, Cancel, List | completo |
| Funds recovery (MED 2.0) | Create, Get, Refund, Cancel, TrackingGraph | completo |
| Reconciliacao CID | CreateSyncVerification, CreateCidSetFile, GetCidSetFile, ListCidSetEvents, GetEntryByCid | completo |
| Keys | Check | completo |
| Policies | List, Get | completo |
| Statistics | por chave, por pessoa | completo |

Duas ausencias sao **corretas e documentadas**, nao lacunas:

* `POST /infraction-reports/` (create) esta morto no BACEN desde a 2.11.0_RC1
  (410 Gone). A notificacao de infracao nasce dentro da Recuperacao de Valores.
* `PUT /infraction-reports/{id}` existe no legado, que e anterior a MED 2.0.
  Nao ha fixture nem chamada viva; fica registrado como nao verificado, sem
  implementar por palpite.

## 3. Regras de negocio do legado: 81 confrontadas

O legado carrega 81 codigos de erro de validacao. Confrontei todos. O resultado
util:

### 3.1 Implementadas hoje (26/07)

| Regra | Prova de que faltava | Medicao antes de ligar |
|---|---|---|
| Limite de chaves e **por conta**, nao por pessoa | `ValidaLimiteContas -> ListQtdContaDict(tpPessoa, tpConta, nrAgencia, nrConta, nrSpbParticipante)`, sem documento no filtro | 0 contas acima do teto |
| Chave CPF/CNPJ tem que ser o documento do titular | `ValidarCPFCNPJ`: "a chave deve ser igual o CPF/CNPJ da pessoa" | 0 divergentes em 306 |
| Sem data de abertura, nao monta o XML | `ValidarDataAberturaConta` + a data esta em toda fixture de `<Account>` | 0 chaves sem data |

O defeito do limite era real e silencioso: uma PF com duas contas e tres chaves
em cada era barrada na sexta, sendo que o teto e cinco **em cada** conta. Nosso
proprio comentario no simulador ja dizia "por conta transacional"; so a
implementacao divergia.

### 3.2 Deliberadamente NAO copiadas

| Regra do legado | Por que nao |
|---|---|
| Rejeitar chave EVP informada na inclusao | nos geramos o UUID e o BACEN aceita; 152 chaves EVP vivas em PRD provam. Copiar quebraria o que funciona |
| Nome com teto de 150 caracteres | 150 e numero do legado, nao do BACEN. Sem fonte citavel, validar seria inventar um limite que pode recusar nome legitimo |

## 4. Conformidade com o item 13 do manual

### 4.1 Item 13.1, baldes do BACEN

**Defeito 1: a tabela de categorias estava errada em 7 das 8 linhas.**

| Cat | Manual 8.2 (tamanho/incremento) | Estava no codigo |
|---|---|---|
| A | 50.000 / 25.000 | 50.000 / 5.000 |
| B | 40.000 / 20.000 | 25.000 / 2.500 |
| C | 30.000 / 15.000 | 10.000 / 1.000 |
| D | 16.000 / 8.000 | 5.000 / 500 |
| E | 5.000 / 2.500 | 2.000 / 200 |
| F | 500 / 250 | 1.000 / 100 |
| G | 250 / 25 | 250 / 25 |
| H | 50 / 2 | 50 / 5 |

Por acaso a nossa categoria real e a G, a unica certa: o `GET /policies/` do
BACEN devolveu 250/25 para o nosso ISPB. Em qualquer outra categoria o espelho
estaria errado. A mesma tabela errada estava duplicada no plug de rate limit.
Corrigidas as duas.

**Defeito 2: o balde zerava em vez de ficar negativo.** O manual e explicito:
usuario com 5 fichas que leva um 404 (penalidade de 20) fica com "saldo devedor
de 15 fichas" e so volta a consultar depois de 8 minutos. Zerar liberava
consulta que o BACEN ainda estaria bloqueando.

**Confirmacoes**: o custo do balde do participante (1 valida / 3 invalida) que ja
estava no codigo bate com o manual. E o BACEN publica a capacidade real em
`GET /policies/`, entao nao ha por que chutar: `calibrate_all_from_bacen/1`
calibra todos os baldes conhecidos numa chamada.

### 4.2 Item 13.2, obrigacoes do participante

| Item | Exigencia | Nossa situacao |
|---|---|---|
| 13.2.1 | autenticidade do consultante; consulta so em ambiente logado; `PI-PayerId` tem que ser cliente real | **atendido**: `get_entry` falha fechado sem `PI-PayerId`, e o IB exige login + MFA por operacao |
| 13.2.2 | politica interna igual ou mais restritiva; **nao repassar ao DICT** consulta que estoure o balde | **ATENDIDO EM PARTE, ver 4.3** |
| 13.2.3 | monitoramento permanente, VCD/EOS e NOT_FOUND, janelas curtas e longas | **implementado hoje**, ver secao 5 |

### 4.3 O ponto que depende de decisao sua

O 13.2.2 e taxativo: *"se o limite de consultas de um usuario e 20, o
participante nao deve enviar ao DICT a vigesima primeira consulta"*.

Hoje, em producao, `DICT_BUDGET_MODE` esta **nulo**, o que resolve para
`advisory`: quando o balde estoura, a cabine **avisa e manda assim mesmo**. O
mesmo vale para o balde novo do usuario (`DICT_USER_ANTISCAN_MODE`).

O mecanismo de bloqueio existe e esta testado nos dois. Ligar e mudar duas
variaveis de ambiente. **Nao liguei sozinho**: em producao nao existe teste, e
enforce passa a recusar consulta de cliente. E decisao sua, e a recomendacao e
ligar, porque advisory nao cumpre o 13.2.2.

## 5. O balde do usuario e o monitoramento, implementados

### 5.1 Balde antiscan por usuario pagador

Faltava inteiro. So existia o espelho do balde do participante. Um cliente
varrendo chaves gastava 20 fichas por 404 no balde dele no BACEN e 3 no nosso,
sem freio local nenhum, ate a instituicao inteira tomar 429.

A tabela `monetarie_dict.user_buckets` ja existia com a forma exata
(`payer_id`, `payer_type`, `tokens_phone_email`, `tokens_other`) e **zero
linhas**: o desenho estava feito, o codigo nunca foi escrito.

Parametros, verbatim do manual 13.1:

| | pessoa natural | pessoa juridica |
|---|---|---|
| balde telefone/e-mail | 100 fichas | 1.000 fichas |
| balde CPF/CNPJ/aleatoria | 100 fichas | 1.000 fichas |
| consulta valida | -1 | -1 |
| consulta invalida | -20 | -20 |
| apos a ordem chegar ao SPI | +1 | +2 |
| incremento temporal | 2/min | 20/min |

Sao dois baldes por usuario. A restituicao esta amarrada ao `EndToEndId` da
consulta e dispara quando a pacs.008 liquida: sem isso o pagador legitimo
drenaria o proprio balde, que seria um defeito novo introduzido pela correcao.

### 5.2 Monitoramento 13.2.3

Tambem nao existia. As tabelas `query_correlations` e `query_metrics` estavam
criadas com as colunas exatas e **zero linhas** — o mesmo padrao do
`user_buckets`.

Agora cada consulta grava uma linha (inclusive as que **nao** acham, que sao o
principal indicador de ataque), e a liquidacao da pacs.008 marca a consulta como
"virou ordem de pagamento". Dai saem os dois indicadores obrigatorios, com os
limiares do manual: 7% institucional, 20% por usuario com no minimo 100
consultas, razao VCD/EOS de 2.

Uma decisao de desenho que vale registrar: sem nenhuma ordem no periodo, a razao
VCD/EOS e **indefinida**, nao infinita. Um dia sem pagamento nao pode virar
alarme de ataque.

## 6. Avaliacao do port do "Monitoramento DICT" do coreproviders

### 6.1 O que eles tem

`DictMonitoringWorker` (cron de 15 minutos) + `use_cases/pix/dict_monitoring`
(commands e queries) + dois schemas (snapshot e alerta) + dois controllers +
telas no admin em cinco idiomas. Calcula sobre `dict_lookup_events` nas janelas
1h/24h/30d, persiste snapshot por instituicao e por conta, e emite alerta
deduplicado por `(entity_id, account_id, kind, window)`.

### 6.2 O que confirmei

Os limiares deles sao **exatamente** os do manual: 7,0 institucional; 20,0 por
usuario; piso de 100 consultas; VCD/EOS 2,0. O moduledoc cita o 13.2.3 e as
notas 14 e 15. E o worker **nao bloqueia**, so sinaliza, que e a leitura correta
do manual ("bloqueio so sob fundada suspeita").

Conclusao: a implementacao deles e bem fundamentada e serve de referencia.

### 6.3 Onde cada peca deve morar aqui, e por que

Aqui a topologia e diferente da deles. No `coreproviders` o provedor PIX e
externo (CloudPix OnZ) e quem chama o provedor e o core, entao faz sentido o
monitoramento viver la. Na Monetarie **a cabine e que fala com o DICT**.

O desenho que recomendo, e que ja comecou a ser implementado:

| Camada | Papel | Estado |
|---|---|---|
| **Cabine PIX** | MEDIR. E a unica que ve a consulta ao DICT, o `PI-PayerId`, o resultado 200/404 e o `EndToEndId`. Sem captura aqui nao existe dado nenhum | **feito hoje**: `query_correlations` gravada no `get_entry`, EOS marcado na liquidacao, `Shared.Bacen.DictMonitoring` calculando os indicadores |
| **Cabine PIX** | FREAR. O balde do usuario e do participante decidem no caminho da consulta e precisam ser atomicos entre pods (Redis) | **feito hoje** (`DictUserAntiscan`), falta ligar o modo enforce |
| **Core** | AGIR. O bloqueio do cliente sob fundada suspeita e relacao com o cliente: canal, comunicacao, alcada, trilha de auditoria. O Core e quem tem conta, usuario e alcada | **a fazer**: consumir o painel da cabine e abrir o fluxo de revisao com dono humano |
| **Admin PIX** | ver o painel regulatorio (indicadores, janelas, suspeitos) | **a fazer**: tela |
| **Snapshots e alertas persistidos** | historico para provar ao regulador que o monitoramento e permanente | **a fazer**: hoje o calculo e sob demanda; falta o worker periodico gravando snapshot e alerta |

Ou seja: **medir na cabine, agir no core**. Portar o worker e as telas deles
direto para o core seria errado, porque o core nao ve a consulta ao DICT; e
portar o bloqueio para a cabine tambem seria, porque a cabine nao tem a relacao
com o cliente.

### 6.4 O que falta para fechar o 13.2.3 com folga

1. Worker periodico (15 minutos serve) gravando snapshot por janela em
   `query_metrics` e abrindo alerta deduplicado. Hoje o indicador e calculado
   sob demanda; o manual pede acompanhamento **permanente**, e snapshot e o que
   prova isso depois.
2. Tela no admin da cabine com os dois indicadores, as tres janelas e a lista de
   suspeitos.
3. Fluxo de revisao no Core: alerta -> analise humana -> bloqueio do usuario,
   com trilha. O manual manda bloquear "se verificada fundada suspeita", e essa
   verificacao tem dono.
4. Ligar o enforce dos dois baldes (secao 4.3).

## 7. Frentes que seguem abertas, com o motivo

1. **`GET /entries/{Key}` recusado** quando o `PI-PayerId` somos nos ou o
   proprio titular: a conexao fecha e o erro fica invisivel. O caminho do
   dinheiro nao e afetado (usa pagador real). Reconciliacao deve usar
   `GetEntryByCid`.
2. **Corpo do erro 4xx do BACEN se perde** (`Finch.TransportError{reason:
   :closed}` + truncamento de 512 bytes no sidecar). E o que encarece todo
   diagnostico.
3. **`parse_cancel_funds_recovery_response` sem prova**: nao ha fixture e o
   legado nao implementa funds recovery.
4. **Modelo de `Refund` do legado e mais novo que o nosso**
   (`EffectiveRefundedAmount`, `FundsRecoveryId`, `MonitorAccount`,
   `RefundAccount`). Evidencia de versao, nao prova de defeito. Precisa do
   contrato 2.12.1 de devolucao.
5. **25 chaves com um zero a esquerda a menos** no numero da conta no BACEN.
6. **HML com DICT 400 de 2 em 2 minutos**, continuo, anterior aos deploys.
7. **Tabelas com schema e sem codigo** que sobraram deste levantamento:
   `protection_rules`, `participant_buckets`, `rate_limit_policies`,
   `key_statistics_cache`, `person_statistics_cache`, `med_requests`,
   `event_notifications`. Nenhuma e obrigacao normativa; ficam listadas para
   decidir entre implementar ou remover, em vez de seguirem como schema morto.
