# Trilha 01: Conectividade e recepção. OnZ CloudPIX (AVIV) vs BACEN direto (Monetarie)

Data: 2026-07-09
Escopo: mapeamento read-only de como a AVIV (`/Users/luizpenha/coreproviders`, branch aviv-hml, OTP app `:fluxiq`) conversa com o provedor OnZ CloudPIX, e de como a Monetarie (`/Users/luizpenha/monetarie`) conversa com o BACEN diretamente (DICT v2.11.0 + SPI/ICOM v5.12.1). Toda afirmação traz evidência arquivo:linha. Nenhuma chamada AWS foi feita; prova nesta trilha é código e documento, não runtime.

Convenção: "o código faz X" = evidência em código vivo com caminho de referência confirmado; "o doc afirma X" = claim de CLAUDE.md, relatório ou comentário, sem prova independente aqui.

---

## 1. Como a AVIV faz (OnZ CloudPIX)

### 1.1 Contexto do participante

- A AVIV é participante PIX INDIRETO: ISPB `04838403`, liquidante ISPB `05684234`. Fonte (doc): `coreproviders/CLAUDE.md:3`. Config de código: `backend/config/dev.exs:105-106` e `backend/config/test.exs:77-78` (`institution_ispb "04838403"`).
- O transporte com a OnZ é HTTP puro na porta 80 via VPC peering, sem TLS e sem mTLS. Fonte (doc): `coreproviders/CLAUDE.md:36` (tabela "PIX Provider | OnZ CloudPIX | Plain HTTP port 80 via VPC peering (NO TLS, NO mTLS)"), `CLAUDE.md:57` (peering `pcx-0fc4623bc2fd613c8`, host `http://internal.planhmg.cloudpix.onz.software`), `CLAUDE.md:86` (regra 10: nunca setar `ONZ_CERTIFICATE_PATH`/`https://`). Corroborado empiricamente em auditoria anterior: `docs/coreproviders/04-orientacao-claude-pos-auditoria-2026-05-24.md:78` (TCP/80 abriu via peering) e `:200` ("Manter NATS_ENABLED=false; OnZ esta por HTTP direto via peering").
- Endpoint de produção (doc): `infrastructure/aws/tenants/aviv-prd/secrets.example.env:24` ("OnZ endpoint (estatico): http://internal.planner.cloudpix.onz.software"). O default de código é `http://localhost:4040` (simulador): `backend/config/runtime.exs:956`.
- Não existe broker de mensagens: NATS foi descomissionado, a integração é HTTP direto (doc: `CLAUDE.md:31`, tabela Messaging "NATS decommissioned/removed").

### 1.2 Cliente HTTP da OnZ (`Onz.Client`)

Arquivo: `backend/lib/fluxiq/services/pix_providers/onz/client.ex`.

- Construído sobre Req. `request/6` monta URL (`build_url`, l.257-260), busca token (`fetch_token`, l.414-427), monta headers `ONZ-Token` + `ONZ-PayerId` (l.262-273; a OnZ espera CNPJ no PayerId, não ISPB, l.275-280).
- Timeout default GLOBAL de 1.500 ms por requisição (`build_req_opts`, l.283-289: `receive_timeout: 1_500`).
- Retry externo com backoff `[200, 500, 1_000]` ms (l.27, `execute_with_retry` l.433-463: retenta 5xx e erro de transporte; 2xx-4xx retornam direto).
- Gotcha documentado no próprio código: o retry interno do Req é independente do externo, e caminhos com orçamento de latência precisam passar `retry: false`, senão os retries se multiplicam (l.138-153: "the 2026-06-10 54s cash-out responses were 4 outer x 4 inner attempts").
- 401: refresh transparente de token com um único reenvio (l.95-96 e `handle_401_retry` l.465-499).
- 429: em rotas de consulta DICT, alimenta o `DictBucketGateway.handle_provider_throttle` (l.98-106; predicado `dict_bucket_throttle_path?` l.129-136).
- Suporte a mTLS por PFX existe no código (extração PEM via openssl com cache ETS, l.302-394), mas quando `certificate_path` está vazio segue sem mTLS (l.314-317). Com a OnZ CloudPIX o doc manda operar sem mTLS (CLAUDE.md:86); o pool mTLS de 25x4=100 conexões (`finch_pool.ex:26-38`) só é usado quando há transport_opts; sem elas o Req usa `Req.Finch` (`finch_pool.ex:11`).
- Auditoria outbound: log do `ONZ-PayerId` literal mascarado + E2E (l.62-79, l.515-529) e arquivamento durável de todo POST com body map (`maybe_archive_outbound`, l.617-638) via `Fluxiq.Infra.MessageArchive`.
- Instrumentação PERF: timestamps HTTP_START/HTTP_END em microssegundos por requisição (l.71-90).

### 1.3 Autenticação com a OnZ (`Onz.Auth`)

Arquivo: `backend/lib/fluxiq/services/pix_providers/onz/auth.ex`.

- Login: `POST /v2/login` com body `{"usuario", "senha"}` (l.19 `@login_path "/v2/login"`, l.264-268). Credenciais vêm de `ONZ_USERNAME`/`ONZ_PASSWORD` (`runtime.exs:959-960`).
- Token com TTL de 55 min em ETS local (l.17-18) e compartilhado entre pods via Redis com TTL 3.300 s (`store_token`, l.326-336).
- Lock distribuído de login (Redis `SET NX EX 30`, l.195-248) para impedir cascata de logins concorrentes entre pods; token "fresco" = TTL Redis > 2.700 s (l.393-408).
- Refresh agendado com jitter de 0-5 min para desalinhamento entre pods (l.380-391).
- Um GenServer Auth por entidade, iniciado por `PixProviders.Supervisor` (supervisor.ex:44-48).

Modo de falha estrutural desse desenho: o token é um segredo de aplicação com expiração e a história do repositório registra cascatas de 401 (poller.ex:49-54, gotchas #237/#487/#498). Não existe equivalente na Monetarie porque a autenticação com o BACEN é mTLS por certificado, sem token de sessão de aplicação.

### 1.4 Mecânica do long-poll (o coração da recepção)

Arquivos: `poller.ex` (1.919 linhas), `lp_client.ex`, `application.ex`, `runtime.exs`.

Slots e limite BACEN:

- Até 6 threads de long-poll simultâneas por participante (BACEN SPI seção 2.2.2.10, citado em `runtime.exs:974-983` e validado em boot: `ONZ_LP_MAX_POLLERS` precisa estar em 1..6, `runtime.exs:1079-1082`).
- Cada slot é uma lease Redis `SET NX PX` na chave `onz:lp:slot:{entity}:{slot}` (poller.ex:446-459, slot_key l.499). Pods disputam; quem não pega slot fica standby re-checando a cada 3 s (default `@default_standby_check_ms 3_000`, l.81). Design de produção documentado: 6 pods x 1 poller por pod = 6 GenServers, 1 slot por pod (`runtime.exs:977-983`; `pollers_per_pod` default 1 em supervisor.ex:40-42).
- Heartbeat da lease em Task supervisionada FORA da mailbox do GenServer (poller.ex:76-78, 580-611), lease TTL 15 s, heartbeat 5 s, com validação de boot lease >= 3x heartbeat (`runtime.exs:1036-1037`). Perda de heartbeat manda o poller para standby imediato (poller.ex:201-207).

Cadência e fase:

- Scheduling absoluto por slot: slot N faz poll em múltiplos de `cycle_ms` (default 5.000 ms) com offset `(N-1) * stagger` (default 833 ms), mais jitter anti-thundering-herd de até 50 ms e um segundo jitter de até 17 ms para desalinhar do tick de 60 s do Oban (poller.ex:85-120, 670-678).
- Consequência empírica documentada no código: com 6 slots defasados de 833 ms, 16,7% das mensagens caíam numa "janela morta" de 833 ms (caso Santander 2026-05-05, poller.ex:94-104); a mitigação recomendada (cycle 4.167/stagger 694) ficou como canary configurável.
- `receive_timeout` do GET LP: 10 s, calibrado empiricamente (pior caso observado 7.061 ms; reduções para 2 s/1,5 s/350 ms causaram cascatas de 401 e de cursor stale, poller.ex:41-60).
- Após receber mensagem há um delay pós-mensagem default de 500 ms (`@default_post_msg_delay_ms 500`, poller.ex:83; `runtime.exs:1014`).

Protocolo poll-and-ack e afinidade TCP:

- `GET /v3/lp/{cursor}` com cursor inicial literal `start` (poller.ex:28, 696-697). A resposta 200 com body entrega a mensagem e o header `ONZ-Pull-Next` É o cursor de ACK: o próximo GET com esse cursor confirma a entrega (poller.ex:747-752; lp_client.ex:10-13). Resetar para `start` abandona o ACK e a OnZ reentrega a mesma mensagem até ~5 vezes.
- O cursor é STATEFUL por conexão TCP: o cursor devolvido só vale na mesma conexão que emitiu o GET anterior. Isso forçou 6 pools Finch dedicadas `:onz_lp_pool_1..6` com `size=1 count=1` HTTP/1.1, uma por slot (application.ex:251-273; lp_client.ex:51-55). Caso empírico da causa: pool única size=10 fazia o slot usar TCP diferente entre ACK e poll seguinte, e a OnZ retinha a mensagem na conexão antiga (gap de 2.502 ms num PIX real, application.ex:252-262).
- Shutdown gracioso: `DELETE /v3/lp/{cursor}` para finalizar o stream server-side e evitar reentrega duplicada pós-restart (lp_client.ex:117-167; chamado no terminate do poller, poller.ex:374-384).
- Incidente estrutural registrado: pool LP criada dentro de Task.async foi destruída por `Task.shutdown(:brutal_kill)` e degradou para 87% de falha de PIX-IN com HTTP 422 "Transação não está pendente" (ACSP chegou tarde demais), application.ex:224-231.

Pipelining e consumo:

- Poller V2: o GET LP roda em `Task.start` fora do GenServer; a mailbox continua processando heartbeat e controle enquanto o LP está em voo. Antes, o handler síncrono limitava a ~72 msg/min com 6 slots, comprovado insuficiente em 2026-05-25 (37/100 PIX-IN dropados com concorrência 1), poller.ex:214-249.
- Modo `:processor` (default em HML/PRD, doc do próprio código): persistir a mensagem na inbox durável `onz_lp_inbox` ANTES de avançar o cursor, enfileirar um job Oban por mensagem e só então ACK (poller.ex:738-757, 1297-1335; worker `OnzLpInboxProcessor`, queue `:onz_lp_process`, concorrência default 6 via `ONZ_LP_PROCESS_CONCURRENCY`, runtime.exs:757). Modo `:sync` legado processa inline.
- Otimização de localidade: `LpLocalProcessor` (flag `ONZ_LP_LOCAL_PROCESSOR`, default false, lp_local_processor.ex:35-38) processa o money path no MESMO pod que recebeu, porque a fila Oban compartilhada concentrava ~96% do consumo no nó co-localizado com o writer Aurora (vencia a corrida `FOR UPDATE SKIP LOCKED` por menor RTT), lp_local_processor.ex:5-16. Overflow faz spill para a fila Oban.
- Poison policy P0-D: contador Redis por message_id; após 5 falhas consecutivas a mensagem vai para DLQ + linha `poisoned` na inbox + alerta CRÍTICO, e o cursor avança (retry limitado, não drop silencioso), poller.ex:31-39, 1168-1190.

Observabilidade dedicada (tudo no caminho vivo, com workers Oban registrados):

- `LagWindow`: janela ETS rolante do lag de entrega fim a fim (`end2end_ms`), lida pelo cron `OnzLagMonitor` para p50 e alerta (lag_window.ex:1-22).
- `OnzPollerLivenessMonitor`, `OnzDlqDepthMonitor`, `OnzLpInboxDepthMonitor`, `OnzLpInboxRecovery` (sweep de 1 min re-enfileira linhas `received` órfãs), todos em `backend/lib/fluxiq/workers/`.
- Logs estruturados com `rid` correlacionando REQ/RESP/ERR do mesmo GET, cursor completo, `inter_poll_gap`, qlen (lp_client.ex:27-44; poller.ex:228-236, 691-694).

### 1.5 Como o status de transação chega (push ou pull)

- Tudo é PULL via o mesmo long-poll: pacs.008 recebida, pacs.002 (ACSP/ACCC/RJCT) de PIX-out, pacs.004 etc. chegam como mensagens no LP e são classificadas por `MsgDefIdr` (poller.ex:1514, 1591-1614). O fluxo PIX-in é two-phase: pacs.008 -> nossa pacs.002 ACSP -> pacs.002 ACCC da OnZ, e o depósito só ocorre em ACCC (poller.ex:1556-1562; contexto do mandato).
- Não há webhook inbound da OnZ: o router da aplicação só expõe `scope "/api/webhooks"` para webhooks administrativos próprios (`backend/lib/fluxiq_web/router.ex:40`); nenhuma rota recebe callback da OnZ.
- Fallback de consulta ativa: `GET /v3/icom/mgmt/{e2e}` e `GET /v3/icom/mgmt/list` (endpoints/icom.ex:136-156). Usado inclusive no caminho `acsp_rejected_with_mgmt_fallback`: se nosso ACSP chega tarde e a OnZ responde ONZ_0422, consulta-se o mgmt e deposita-se se CONCLUIDA (poller.ex:56-59).
- Resposta pacs.002 outbound: `POST /v3/icom/pacs/002` com receive_timeout escopado de 60 s (recomendação OnZ; o default global de 1.500 ms gerava timeouts falsos e acumulava DLQ), `runtime.exs:965-971` e endpoints/icom.ex:36-57. ACSP vai sem retry (um ACSP atrasado é pior que nenhum); RJCT com retry (endpoints/icom.ex:45-55).

### 1.6 Contrato da API OnZ (via cloudpix_simulator)

O simulador replica a API real em `/v2/*`, `/v3/*` (`cloudpix_simulator/lib/cloudpix_simulator/cloudpix_api/router.ex:4`). Rotas relevantes (router.ex:14-245):

- Auth: `POST /v2/login`.
- Long-poll: `GET /v3/lp/:pull_next`.
- ICOM: `POST /v3/icom/pacs/{002,004,008}`, `pain/{009,011,012,012/confirmed,013,014}`, `camt/{060,055,029}`, `pibr/001`, `reda/{014,022,031}`; consultas `GET /v3/icom/mgmt/{message/:msg_id,list,:end_to_end_id}`.
- DICT com caminhos SINGULARES (`/v3/dict/chave`, `/v3/dict/reivindicacao`, `/v3/dict/infracao`), guardados em runtime por `SingularPathGuard` (`onz/singular_path_guard.ex`, usado em endpoints/dict.ex:13 e icom.ex:10). O simulador aceita também os plurais legados (`/v3/dict/chaves`, `reivindicacoes`, `infracoes`).
- Consulta de chave: `GET /v3/dict/chave/{key}?tipoChave=...`; a AVIV NÃO envia `idFimAFim` na consulta; o `EndToEndId` canônico vem da RESPOSTA da OnZ e é obrigatório na pacs.008 subsequente (endpoints/dict.ex:27-33, 43-95). Orçamento de latência da consulta síncrona: `receive_timeout` 2.500 ms, retry interno OFF, 1 retry externo de 300 ms, pior caso ~5,3 s (endpoints/dict.ex:171-183).

### 1.7 Rate limit DICT client-side (dict_bucket)

- `DictBucket.Guard` (GenServer por entidade, `onz/dict_bucket/guard.ex`): espelho client-side do token bucket do DICT com matriz de acesso por prioridade x modo (normal/warning/critical/emergency permitem essential/important/optional de forma decrescente, guard.ex:16-22), scripts Lua em Redis, sincronização a cada 5 s, e downgrade após 2 respostas 429 consecutivas (guard.ex:7-13).
- Custos configurados: sucesso 1, not_found 3, invalid 3, suspect 20; limite por cliente 120/min (`runtime.exs:940-952`). Tamanho de bucket default 15.000/60 s (`runtime.exs:962-963`).

### 1.8 ONZ Accounts (produto separado, com mTLS)

- `onz-accounts-mtls/` na raiz contém material de certificado (`tls.key`, `OWENMTLS.7z`).
- É outra API da OnZ (conta de liquidação), com OAuth2 client_credentials e Bearer token, distinta do CloudPIX `/v2/login`: `onz/accounts/config.ex:1-56` (`ONZ_ACCOUNTS_BASE_URL`, `/api/v2/oauth/token`, `/api/v2/accounts/balances/`, `/api/v2/accounts/transactions/`), transporte Req com verify toggle e mTLS opcional montado de `/etc/onz-accounts-mtls/` (accounts/http.ex:1-50; runtime.exs:1152-1167). Consumida por workers de conciliação de saldo do liquidante (`workers/settlement_balance_poller.ex`, `settlement_balance_comparator.ex`).
- Ou seja: o dinheiro (SPI) trafega por HTTP sem TLS via peering; a consulta de saldo/extrato da conta na OnZ trafega por OAuth2+mTLS. São dois produtos com posturas de segurança diferentes.

### 1.9 Latências e estados presos documentados

- Latência fim a fim de PIX-IN em produção AVIV, medida em regime (doc no código): P50 471 ms, P95 561 ms, P99 737 ms (PRD 2026-04-20, 1.256 tx/60 min), poller.ex:106-108. O P50 cabe no alvo BACEN de 500 ms; P95/P99 estouram.
- `pixout-onz-status-2026-06-01.xlsx` (raiz do repo, extraído nesta trilha das worksheets XML): lote de PIX-out em HML minhakonta, todos aceitos pela API própria (HTTP 202) e pelo OnZ, mas no mgmt: 116 `AGUARDANDO_AUTORIZACAO` contra 3 `CONCLUIDA`; a própria planilha anota "AGUARDANDO_AUTORIZACAO=parada no OnZ (familia ONZ_0412); CANCELADA=EVP rejeitada (AG03 destino)". Evidência do modo de falha "preso no provedor" que não existe quando se fala direto com o BACEN. (Contexto: lote de teste HML, não tráfego de produção.)
- Arquivo `voided-12-pendente-onz-URGENTE.csv` na raiz corrobora a classe de incidente "pendente na OnZ" (nome de arquivo; conteúdo não analisado nesta trilha).

---

## 2. Como a Monetarie faz (BACEN direto)

### 2.1 Contexto do participante

- Participante PIX DIRETO: ISPB `46026562`, SCD (CLAUDE.md raiz, seção Instituição). Cabine própria em `pix/backend` (umbrella: `shared`, `spi_service`, `dict_service`, `settlement_service`; `ls pix/backend/apps`).
- Endpoints BACEN reais por ambiente em `pix/backend/config/runtime.exs:436-495`: DICT prod `https://dict.pi.rsfn.net.br:16422/api/v2`, DICT-NP `:16432/api-np/v2`, ICOM primário `https://icom.pi.rsfn.net.br:16422`, secundário `icom-sec:17422`; homolog nas portas 165xx/175xx.

### 2.2 Transporte mTLS: pools Finch dedicados + sidecar HSM em produção

Pools (HML, mTLS direto no BEAM): `pix/backend/apps/shared/lib/shared/application.ex`:

- Um pool Finch por host BACEN, todos `count: 1` para reuso real de conexão: SPI primário `size: 6`, SPI secundário `size: 6`, DICT `size: 12`, ARQ `size: 4` (application.ex:89-108). O comentário registra o bug corrigido: `count: 10` round-robinava requisições entre 10 pools, cada um abrindo TCP próprio (application.ex:55-58).
- mTLS vive nas `conn_opts` do pool, montado no boot por `build_bacen_transport_opts` (verify_peer, cacerts ICP-Brasil, customize_hostname_check, application.ex:78-87, 203-211). NUNCA passar transport_opts por requisição: abre conexão avulsa sem partial_chain e trava no gateway RSFN, validado empiricamente 2026-06-23, pool-only = 200 em 153 ms (client.ex:457-461).
- `conn_max_idle_time` 30 s: o gateway RSFN fecha keep-alive ocioso sem o pool perceber, e o reuso de conexão morta devolvia `TransportError :closed` (application.ex:184-191).

Sidecar de produção (`pix/services/bacen_mtls_sidecar/main.go`):

- Proxy reverso Go por upstream: pix-api fala HTTP local com o sidecar; o sidecar origina o mTLS com o BACEN. A chave privada PIC NUNCA existe em disco nem no processo; a assinatura do CertificateVerify do handshake TLS é feita no HSM RTM via `POST /v1/kmip/{vhsm}/sign-rsa` (RSAHASH = PKCS#1 v1.5 sobre SHA-256), main.go:1-12, 86-130.
- TLS 1.2 fixo (o HSM não faz PSS), main.go:132-140, 292-297.
- Upstreams default: `dict=9101`, `dict_np=9102`, `icom=9103`, `icom_sec=9104`, `arq=9105` (main.go:369-371; `SIDECAR_UPSTREAMS` sobrescreve o conjunto INTEIRO, gotcha em memória).
- Ajustes de compatibilidade ICOM: `Host` sem porta (Manual Canal Secundário 4.1.2/4.2.1), supressão do `X-Forwarded-For` via nil map (Go issue 38079), `DisableCompression: true` para nunca injetar Accept-Encoding (main.go:255-291).
- Transport: `MaxIdleConns: 32`, `IdleConnTimeout: 90s`, `KeepAlive: 30s`, `TLSHandshakeTimeout: 15s` (main.go:286-291).
- ACHADO DESTA TRILHA: o `tls.Config` do sidecar NÃO configura `ClientSessionCache` (verificado por grep em main.go: nenhuma ocorrência). Em Go, sem `ClientSessionCache` não há session resumption; toda conexão TCP nova faz handshake completo, e cada handshake completo custa uma viagem `sign-rsa` ao HSM. O keep-alive de 90 s mitiga, mas rajadas que abrem conexões novas pagam HSM no caminho crítico.
- O roteamento pelo sidecar é opt-in por env, default = conexão direta: `DICT_BASE_URL` e `ICOM_PRIMARY_URL`/`ICOM_SECONDARY_URL` apontando para `http://localhost:PORT` (runtime.exs:440-443, 471-473).

### 2.3 Loop de GET no ICOM (recepção)

Subsistema: `pix/backend/apps/spi_service/lib/spi_service/icom/`.

- Ligado por `INBOUND_SOURCE=icom_http` (application_supervisor.ex:1-120; default `:bacen_mock_nats` = zero children). A task definition viva do ECS seta `icom_http` (`infra/aws/greenfield/ecs.tf:119`).
- Dois canais independentes, cada um com coordinator próprio: CPM (financeiro) e CSM (não financeiro). Cada coordinator disputa um advisory lock PostgreSQL cluster-wide (`pg_try_advisory_lock`, cpm/coordinator.ex:278) e, se líder, sobe um DynamicSupervisor com 6 workers (slot_id 1..6), teto do BACEN Manual das Interfaces v1.12 seção 4.6 (coordinator.ex:89 `@max_slots 6`; `ICOM_{CPM,CSM}_MAX_SLOTS` só pode REDUZIR, application_supervisor.ex:110-120).
- Protocolo por worker (cpm/worker.ex:1-62): `GET /api/v1/out/{ispb}/stream/start` abre a sessão; em seguida `GET` no header `PI-Pull-Next` de cada resposta (client.ex:296-331; normalize_pull_next_url l.368-382). O GET seguinte É o ACK do lote anterior (mesma semântica poll-and-ack da OnZ, mas com o BACEN direto).
- Cadência: resposta 200 com corpo multipart -> parse, persist, publish e PRÓXIMO TICK IMEDIATO (zero sleep, worker.ex:411-412); 204 -> re-poll em 200 ms no CPM (worker.ex:78 `@cpm_idle_sleep_ms 200`) e 3.000 ms no CSM (csm/worker.ex:78). `receive_timeout` do GET: 60 s (`@icom_default_timeout`, client.ex:57, 393-401).
- Lote multipart: uma resposta 200 carrega N mensagens (RFC 1521, `MultipartParser`, worker.ex:337-346). Diferença chave contra a OnZ, que entrega tipicamente 1 mensagem por poll (poller.ex:850-853 aceita lista, mas o protocolo é 1 ACK por cursor).
- Invariante pré-ACK: o próximo GET só sai depois de `AckTracker.persist_batch/2` gravar as mensagens em `IcomReceived` no Aurora; falha de persistência NÃO avança o cursor e o BACEN retransmite; dedup por MessageId absorve o replay (worker.ex:53-61, 350-435).
- Ponte para o pipeline: `NatsBridge.publish_batch` publica cada parte em `monetarie.spi.inbound.{msg_type}` (nats_bridge.ex:33-36); falha de publish não bloqueia o cursor, linhas ficam com `nats_published_at NULL` e `republish_pending/1` drena no próximo boot (worker.ex:369-406).
- Consumo: `InboundProcessor` é consumer JetStream push do stream `MONETARIE_SPI`, filter `monetarie.spi.inbound.>`, `batch_size 100`, `max_concurrency 100` (inbound_processor.ex, bloco `use Shared.Workers.BaseWorker`; BaseWorker spawna Task por mensagem quando `max_concurrency > 1`, base_worker.ex:150-155).
- Watchdog: `HealthMonitor` por canal, tick de 60 s, julga vitalidade pela idade do heartbeat ETS (escrito antes e depois de cada request), mata worker preso com `Process.exit(:kill)` e o DynamicSupervisor recria retomando a sessão aberta; a versão anterior com `GenServer.call` matava slots saudáveis bloqueados em long-poll legítimo (health_monitor.ex:1-44).
- Backoff de erro: exponencial jittered `2^n * 100ms +-25%`, cap 30 s, alarme após 10 erros consecutivos (worker.ex:42-45, 79-81).

### 2.4 DICT direto

- `dict_request/4` com orçamento client-side ANTES da chamada: `Shared.Bacen.DictBudget.check` debita a reserva, `settle` acerta pelo resultado real (client.ex:85-107).
- `DictBudget` (dict_budget.ex:1-60+): espelho Redis do token bucket do BACEN (Manual de Segurança PIX 6.6 + Manual Operacional do DICT item 13): 200 custa 1 ficha, 404 custa 3, erros não são cobrados, e a liquidação da pacs.008 que usa o E2E da consulta DEVOLVE 1 ficha (`restitute_on_settled/1`). Calibração pelo endpoint AUTORITATIVO `GET /policies/{Policy}` do DICT (não existem headers X-RateLimit; o único sinal em banda é o Retry-After do 429). Modos `advisory` (default, só loga) e `enforce` (429 local); falha de Redis é sempre fail-open.
- Headers obrigatórios do participante direto: `PI-EndToEndId` (obrigatório no `GET /entries/{Key}` da v2.11.0; sem ele o BACEN responde 400) e `PI-RequestingParticipant` injetado por padrão (dict_client.ex:128-130, 1479-1483; client.ex:681-690).
- DICT-NP (não postergável, `POST /keys/check`, limite documentado de 70 req/min) roteia para host/porta/path-base próprios com fail-fast se não configurado (client.ex:120-150; runtime.exs:432-463).
- Rate limit INBOUND da própria API (espelho do manual v8.0): General 250 req/s burst 25, Special 50 req/s burst 10, agregado 1.000 req/s (doc: pix/CLAUDE.md, seção DICT Rate Limits).

### 2.5 Assinatura no caminho crítico (envio)

- Todo envio SPI passa por `OutboundSender` (consumer JetStream de `monetarie.spi.outbound.>`, outbound_sender.ex:34-35): assina XMLDSig fail-CLOSED (mensagem sem assinatura NUNCA vai ao BACEN, l.85-94, 168-178), valida contra o XSD oficial DEPOIS de assinar (l.293-295) e só então `Client.spi_request`.
- A assinatura usa HSM quando habilitado: `XmlSigner.sign_spi` -> `RtmHsm.sign_base64` (xml_signer.ex:359-362). O `RtmHsm` manda o hash SHA-256 cru para `sign-rsa` com `RSAHASH` e cacheia a sessão HSM em `persistent_term` por 10 min (rtm_hsm.ex:10-14, 33-34, 159).
- Custo de latência do HSM no envio: uma chamada HTTPS ao HSM por mensagem assinada (mais uma por handshake TLS novo no sidecar em produção). Números vivos do HSM não estão versionados no repo; ver seção 7 (NAO VERIFICADO).
- Logs PERF por fase (padrão portado da AVIV em 04/07): MSG_RECV na entrada, MSG_OK/MSG_FAIL no desfecho, `end2end_ms` medindo CreDtTm até o worker pegar a mensagem (outbound_sender.ex:50-62; inbound_processor.ex:160).

### 2.6 Modelo canônico do PIX-in (prova de produção 09/07)

Doc (CLAUDE.md raiz, estado 2026-07-09, e memória `monetarie-pix-in-credit-path-canonical`): o BACEN real LIQUIDA ANTES de entregar a pacs.008 ao recebedor e o recebedor NÃO recebe pacs.002; o caminho de crédito correto é o legado flag-OFF (`transaction.created` -> Core `handle_transaction_ledger`); `TWO_PHASE_PIX_IN` é PROIBIDO (o gatilho settled só existe no simulador). Isso elimina, no desenho Monetarie, a perna inteira de duas fases ACSP->ACCC que a AVIV é obrigada a executar com a OnZ.

### 2.7 Latências documentadas

- ICOM recepção: 24 a 28 ms de CreDtTm até received_at, prova de 03/07 em HML (doc: `docs/reports/2026-07-04-comparativo-aviv-coreproviders-monetarie.html`, trecho "24 a 28ms (CreDtTm até received_at, prova de 03/07)"; mesmo doc registra OnZ p50 471 ms, p95 561 ms, p99 737 ms).
- DICT GetEntry real BACEN homolog: 200 em 143 ms; `/cids/events` 200 em 36 ms; PIBR.001 echo 196 ms; camt.060 191 ms (doc: CLAUDE.md raiz, milestone 2026-06-23).
- Leitura da pacs.002 do nosso PIX-out em 25 ms (doc: memória `monetarie-pixout-read-latency-proof`, 03/07).
- ANS alvo do fluxo: 1,6 s, com `AnsMonitor` de 1.600 ms (doc: pix/CLAUDE.md).

---

## 3. Diferenças estruturais e por que existem

| Dimensão | AVIV (OnZ CloudPIX) | Monetarie (BACEN direto) |
|---|---|---|
| Papel no SPI | Indireto (ISPB 04838403, liquidante 05684234), CLAUDE.md:3 | Direto (ISPB 46026562) |
| Transporte | HTTP puro porta 80 via VPC peering, sem TLS (CLAUDE.md:36,86) | mTLS ICP-Brasil verify_peer; em prod handshake assinado no HSM via sidecar (application.ex:78-87; main.go:1-12) |
| Autenticação | Token de sessão (`/v2/login`, usuario/senha), TTL 55 min, Redis+lock (auth.ex) | Certificado cliente ICP-Brasil; sem token de aplicação |
| Recepção | Long-poll `GET /v3/lp/{cursor}`, 6 slots via Redis SETNX, cursor stateful por TCP, 1 pool size=1 por slot | Long-poll ICOM `stream/start` + `PI-Pull-Next`, 6 slots CPM + 6 CSM, líder por advisory lock PG, multipart em lote |
| ACK | Próximo GET com cursor `ONZ-Pull-Next` (reentrega ~5x se abandonar) | Próximo GET no `PI-Pull-Next` (BACEN retransmite se não ackar); invariante pré-ACK: persistir antes |
| Mensagens por poll | Tipicamente 1 (map) ou lista; delay pós-msg 500 ms default | N por multipart; re-poll imediato (0 ms) quando houve mensagem |
| Status de PIX-out | pacs.002 chega pelo MESMO LP + fallback `GET /v3/icom/mgmt/{e2e}` | pacs.002 chega pelo mesmo stream ICOM; enviada é lida em ~25 ms (doc) |
| PIX-in | Two-phase ACSP->ACCC, depósito só em ACCC; risco 422 se ACSP tardio | BACEN liquida ANTES da pacs.008; recebedor não emite/recebe pacs.002 (canônico 09/07) |
| Broker interno | Nenhum (NATS removido); durabilidade = tabela `onz_lp_inbox` + Oban | NATS JetStream (`monetarie.spi.inbound.>`) + tabela `IcomReceived` + outbox |
| Assinatura de mensagem | Nenhuma no app (a OnZ assina no lugar dela) | XMLDSig RSA-SHA256 fail-CLOSED por mensagem, via HSM (outbound_sender.ex:85-94) |
| Rate limit DICT | Espelho client-side (obrigatório porque o balde é do liquidante/provedor) | Espelho client-side + estado AUTORITATIVO via `GET /policies` + restituição on-settled |
| Latência de entrega documentada | p50 471 ms / p99 737 ms (poller.ex:106-108) | 24-28 ms CreDtTm->received (doc 07-04) |

Por que existem: a AVIV terceiriza a adesão ao SPI (certificados, XMLDSig, XSD, RSFN, HSM) para a OnZ e paga com (a) um salto de rede e de fila extra em TODA mensagem, (b) um protocolo LP proprietário mais frágil (cursor por TCP, token, reentrega), (c) estados intermediários presos no provedor (ONZ_0412), e (d) um teto de vazão dado pela mecânica 1-mensagem-por-ACK. A Monetarie paga o custo da adesão direta (mTLS, HSM, XMLDSig, XSD, dois canais, baldes DICT próprios) e recebe em troca menos saltos, lote multipart e latência de uma ordem de grandeza menor.

### 3.1 Saltos até o dinheiro (PIX-in)

AVIV (crédito ao cliente): BACEN -> infraestrutura OnZ (invisível para a AVIV) -> entrega LP quando um slot puxa (inclui a janela de fase do ciclo 5 s/833 ms) -> persist inbox + enqueue -> money path (TB via tb-gateway) -> `POST pacs/002 ACSP` para a OnZ -> OnZ -> BACEN -> pacs.002 ACCC volta pelo LP -> depósito. São no mínimo 3 round-trips HTTP AVIV<->OnZ por PIX-in (LP da pacs.008, POST ACSP, LP da ACCC), fora o que a OnZ faz com o BACEN.

Monetarie: BACEN liquida -> entrega pacs.008 no stream ICOM já aberto (1 GET em curso) -> persist `IcomReceived` -> NATS -> InboundProcessor -> evento `transaction.created` -> Core credita TB + espelho PG. 1 round-trip HTTP externo; os demais saltos são internos (NATS, Aurora, TB).

### 3.2 Pontos de falha

- AVIV: OnZ como SPOF externo (sem canal alternativo no código); Redis (slots, token, dedup, poison counter; degradação é fail-open com login direto, auth.ex:244-247); Aurora (inbox); Oban; cascatas de 401; afinidade cursor-TCP.
- Monetarie: gateway RSFN/mTLS (2 canais com failover, ChannelRouter, pix/CLAUDE.md); HSM (assinatura outbound fail-CLOSED = HSM fora derruba ENVIO, como visto em 03-06/07; recepção não depende de assinatura nossa); NATS (incidente 05-06/07: Core surdo por Connection presa, remediado com spawn_monitor + watchdogs); Aurora; TigerBeetle.

### 3.3 Vazão máxima teórica de cada desenho (análise, com premissas explícitas)

- AVIV: 6 slots; cada mensagem exige um round-trip GET de ACK; com o delay pós-mensagem default de 500 ms (runtime.exs:1014), o regime sustentado fica em torno de 2 msg/s por slot = ~12 msg/s, salvo entregas em lista (suportadas em poller.ex:850-853, frequência real NÃO VERIFICADA) e salvo tuning do delay para 0. Removendo o delay e assumindo RTT de ~50 ms por GET com backlog, o teto sobe para ~20 msg/s por slot = ~120 msg/s. O recorde de 1,53 M tx/dia (~17,7 tx/s médios) é compatível com esse envelope, mas indica que picos dependem de lote e de tuning.
- Monetarie: 6 slots CPM com re-poll imediato e lote multipart; com RTT documentado de 24-28 ms e N mensagens por resposta, o teto por slot é ~35-40 respostas/s x N mensagens. Com lotes médios de 10, um único slot passa de 350 msg/s e o canal de 2.000 msg/s; o gargalo desloca para persist_batch (Aurora), NATS e o consumidor (`max_concurrency 100`). Premissas: tamanho de lote do BACEN sob carga NÃO VERIFICADO empiricamente nesta trilha.

---

## 4. O que a AVIV tem de melhor (candidatos a port), com esforço

1. Distribuição dos slots de recepção entre pods (lease Redis por slot + heartbeat externo + standby takeover, poller.ex:174-207, 446-474). Na Monetarie o líder do advisory lock concentra os 12 workers (6 CPM + 6 CSM) numa única task ECS (coordinator.ex:40-41): hot spot de CPU/rede e failover dependente da queda da sessão PG. Port: mover a eleição de CPM/CSM para lease por slot (Redis já existe na cabine), 1-2 slots por task. Esforço: médio (3-5 dias, com teste de takeover).
2. Janela rolante de lag de entrega + alerta (LagWindow ETS + OnzLagMonitor, lag_window.ex:1-22). A cabine tem PERF logs por fase e AnsMonitor de 1.600 ms, mas não um p50/p95 rolante específico de "CreDtTm -> received_at" do ICOM com alarme. Esforço: baixo (1 dia; o telemetry `[:spi_service, :icom, ...]` já existe, worker.ex:342-390).
3. Classes de prioridade no orçamento DICT (matriz essential/important/optional x modos normal/warning/critical/emergency, guard.ex:16-22) e limitador por cliente (120/min, runtime.exs:949). O `DictBudget` da Monetarie tem advisory/enforce e restituição, mas não prioriza consultas críticas (ex.: MED) sobre consultas opcionais (ex.: pré-visualização de chave no IB) quando o balde aperta. Esforço: baixo-médio (2-3 dias).
4. Processamento node-local com spill (LpLocalProcessor, lp_local_processor.ex:5-16): lição de que fila compartilhada + writer Aurora numa AZ concentra consumo num nó. Na Monetarie o análogo é monitorar a distribuição do consumer group JetStream entre tasks; se concentrar, aplicar o mesmo padrão (processa no pod que recebeu, spill para a fila). Esforço: baixo para instrumentar, médio para portar o padrão.
5. Recovery sweep de inbox + monitores de profundidade dedicados (onz_lp_inbox_recovery.ex, onz_lp_inbox_depth_monitor.ex, onz_dlq_depth_monitor.ex + e-mails). A cabine tem `republish_pending` no boot do worker (worker.ex init), mas não um sweep periódico independente do ciclo de vida do worker para `IcomReceived` sem `nats_published_at`. Esforço: baixo (1-2 dias).
6. Higiene de jitter anti-alinhamento com cron (17 ms para quebrar cycle 5.000 divide 60.000, poller.ex:114-120). Na cabine o cadence de 200 ms também divide 60.000; se os PERF logs mostrarem spikes no tick de cron do Oban/Scheduler, portar o truque é trivial. Esforço: trivial (horas), condicionado a evidência.
7. Arquivamento outbound automático no client (maybe_archive_outbound, client.ex:617-638): registro durável de TODO POST no provedor num único choke point. A cabine audita via `audit_exchange` no client (client.ex:478-489) e XmlArchiver; paridade já razoável, port não prioritário.

Não portar da AVIV: o protocolo LP em si (cursor stateful por TCP, pool size=1, token de sessão), o delay pós-mensagem de 500 ms, e o two-phase de depósito (proibido no nosso modelo, CLAUDE.md 2026-07-09).

---

## 5. O que a Monetarie tem de melhor (não regredir)

1. Participação direta: latência de entrega 24-28 ms vs p50 471 ms (docs citados na seção 2.7); nenhum estado "preso no provedor" tipo ONZ_0412 (xlsx: 116/119 presos num lote HML) e nenhuma família de erro proprietária entre nós e o BACEN.
2. Lote multipart + re-poll imediato (worker.ex:411-412): vazão de recepção escala com o tamanho do lote, não com round-trips por mensagem.
3. Invariante pré-ACK com retransmissão do BACEN (worker.ex:53-61, 350-435): durabilidade garantida ANTES do ACK sem inventar inbox proprietária; dedup por MessageId.
4. Dois canais (CPM financeiro com cadência 200 ms, CSM não financeiro 3.000 ms) com roteamento e failover próprios (ChannelRouter), contra endpoint único do provedor na AVIV.
5. Segurança: mTLS ICP-Brasil verify_peer fim a fim, XMLDSig fail-CLOSED por mensagem, chave privada confinada ao HSM inclusive no handshake TLS (sidecar). A AVIV trafega o dinheiro em HTTP sem TLS pela peering e não assina nada (a OnZ assina por ela).
6. Sem token de sessão de aplicação no caminho do dinheiro: elimina por construção as cascatas de 401 que dominam o histórico de incidentes do poller AVIV (poller.ex:49-54).
7. Orçamento DICT com calibração autoritativa (`GET /policies`) e restituição on-settled (dict_budget.ex): o espelho da AVIV não tem fonte autoritativa equivalente no código visto (a política vem de `policy_client.ex`, mas o balde real é do liquidante).
8. HealthMonitor por heartbeat ETS que não mata worker legitimamente bloqueado em long-poll (health_monitor.ex:33-44), mais maduro que o par kill-por-call que a própria AVIV já sofreu.

---

## 6. Recomendações priorizadas (meta: mais volume, menos latência)

P0 (fazem diferença direta no primeiro gargalo de volume):

1. Medir e depois distribuir a recepção ICOM. Hoje 1 task ECS lidera e roda os 12 workers (advisory lock, coordinator.ex:40-41). Passo 1 (barato): carga sintética em HML medindo CPU da task líder, `persist_batch duration` (telemetry worker.ex:353-357) e lag NATS->InboundProcessor. Passo 2: se o líder saturar, portar o modelo de lease por slot da AVIV (seção 4.1) para espalhar slots entre tasks.
2. Habilitar session resumption TLS no sidecar de produção: adicionar `ClientSessionCache: tls.NewLRUClientSessionCache(N)` ao `tls.Config` (main.go:292-297) e conferir se o gateway RSFN aceita resumption. Cada handshake completo hoje custa uma viagem ao HSM (sign-rsa) no caminho crítico; sob rajada com conexões novas isso vira latência de cauda. Esforço: horas + validação em HML.
3. Fixar orçamento de fases com os PERF logs já portados: publicar p50/p95/p99 por fase (GET ICOM, persist, NATS publish, consume, credit TB) num painel PromEx e definir SLO interno < 500 ms fim a fim de cauda (o alvo que a AVIV estoura em P95). Instrumentação já existe (outbound_sender.ex:50-62, worker.ex:342-390); falta consolidar a leitura.

P1 (robustez sob volume):

4. Sweep periódico de `IcomReceived` não publicado (port do onz_lp_inbox_recovery): hoje a recuperação depende do boot do worker (worker.ex init, republish_pending). Um cron de 1 min fecha o gap sem depender de restart.
5. Janela rolante de lag de entrega ICOM com alerta (port do LagWindow/OnzLagMonitor): detectar degradação do gateway RSFN em minutos, não em auditoria.
6. Classes de prioridade no DictBudget (matriz da AVIV): garantir que MED/infração/consulta de pagamento nunca disputem balde com consultas opcionais quando em modo crítico.
7. Confirmar a distribuição do consumo JetStream entre tasks sob carga (lição LpLocalProcessor): se concentrar num nó, aplicar processamento node-local com spill.

P2 (higiene e prevenção):

8. Jitter anti-alinhamento de cron no tick de 200 ms do CPM, condicionado a evidência de spikes correlacionados com o tick de 60 s.
9. Runbook de probe empírico estilo xlsx da AVIV (lote controlado com POST/aceite/status por transação) para regressão de latência após cada deploy de cabine em HML.
10. Documentar formalmente o teto de lote multipart observado do ICOM em HML/PRD (hoje é a principal incógnita do envelope de vazão, seção 3.3).

---

## 7. Perguntas abertas / NAO VERIFICADO

1. NAO VERIFICADO: tamanho real dos lotes multipart do ICOM sob carga (quantas mensagens por resposta 200 o BACEN agrega). Determina o teto de vazão da recepção Monetarie. Falta: medição em HML com tráfego sintético (o simulador local pode não reproduzir o agrupamento do BACEN real).
2. NAO VERIFICADO: latência da chamada `sign-rsa` ao HSM RTM (por assinatura XMLDSig e por handshake do sidecar). Não há número versionado no repo; os PERF logs do OutboundSender permitem derivar. Falta: extração de logs de produção/HML (fora do escopo read-only desta trilha).
3. NAO VERIFICADO: se o gateway RSFN/BACEN aceita TLS session resumption (pré-requisito da recomendação P0-2). Falta: teste de handshake em HML.
4. NAO VERIFICADO: frequência com que a OnZ entrega listas (lote) no LP em produção AVIV; o código aceita (poller.ex:850-853, count_messages), mas os logs analisados historicamente falam de 1 mensagem por poll. Afeta a estimativa de vazão da seção 3.3.
5. NAO VERIFICADO: valores efetivos das ENV de tuning do LP na produção AVIV (ONZ_LP_CYCLE_MS/STAGGER/POST_MSG_DELAY etc.). Os defaults estão em runtime.exs:999-1014, mas o canary recomendado (4167/694) pode estar aplicado via env; sem acesso AWS (proibido nesta trilha) não dá para afirmar. As conclusões usam defaults declarados.
6. NAO VERIFICADO: comportamento do failover do advisory lock PG na cabine (tempo entre morte do líder e novo líder assumir os 12 workers). O código sugere que a liberação acompanha o fim da sessão PG (coordinator.ex:278, 305), mas o tempo real depende de TCP keepalive/timeout do Postgres. Falta: teste de caos em HML.
7. Pergunta aberta: vale consumir pacs.008 inline no spi_service (crédito síncrono, sem o hop NATS) para cortar a cauda, mantendo o outbox só para eventos não críticos? O modelo AVIV (sem broker) prova que dá para operar assim, mas o custo é perder o desacoplamento que hoje protege o Core; o incidente NATS de 05-06/07 corta para os dois lados (o hop é um risco, mas os watchdogs novos o mitigam). Precisa de decisão de arquitetura com números da recomendação P0-3.
8. Pergunta aberta: a AVIV usa `ONZ-PayerId = CNPJ` por exigência da OnZ (client.ex:275-280); num cenário multi-tenant da cabine Monetarie (outras instituições), qual é o análogo de multiplexação por participante no ICOM? O desenho atual é 1 ISPB por deploy (application_supervisor.ex usa `:institution_ispb` único).
