A Tramitação do OlhoNaLei
OlhoNaLei · Projeto nº 571/2026Em tramitaçãoolhonalei.com

A tramitação do OlhoNaLei

Tecnologias, tropeços e otimizações de um projeto que lê o Congresso Nacional todos os dias.

Ementa
Dispõe sobre como acompanhar o Congresso com dados abertos, busca por sentido e inteligência artificial, gastando pouco, e dá outras providências.
Apresentação
30/04/2026, data do primeiro commit
Situação
Em tramitação. Números de 30/04 a 06/10/2026.
AberturaLeitura

Cada despacho segue o mesmo rito

  1. Problema

    O que precisávamos resolver.

  2. Tecnologia

    O que escolhemos, e por quê.

  3. Deu errado

    O que quebrou em produção.

  4. Solução

    Como medimos e corrigimos.

  5. Lição

    O que levamos para o próximo despacho.

Os números

São reais: tirados do banco de produção, do git, das faturas e dos logs. Inclusive custos, erros e robôs bloqueados.

Os erros

Os nossos e os do assistente de IA que ajudou a escrever o código. Transparência é premissa do projeto, então vale para nós também.

AberturaTecnologiaclique numa caixa para ver as conexões

Três nuvens e um banco no meio de tudo

AberturaEscalabanco de produção · 06/10/2026

O tamanho do que roda toda madrugada

172.307
proposições no banco, das duas casas. 46% são substantivas (projetos, PECs, MPs); o resto é requerimento, emenda e afins.
367.356
trechos de texto com vetor, prontos para a busca por sentido
13.611
proposições lidas por IA: resumo, temas, jabutis e impacto fiscal
33
tarefas agendadas no próprio banco; cerca de 54 mil disparos por mês
82.400
PDFs baixados das duas casas: 21,3 GB de inteiro teor
2.924
notícias dos últimos 30 dias, todas com vetor para achar a proposição de que falam
Despacho 01 · abr → jul 2026

O Congresso publica tudo. Só não publica pronto.

Duas APIs públicas, cada uma com seu formato, limites que ninguém documenta e PDFs que não são texto.

Despacho 01 · DadosTecnologiaingestion/ · supabase/migrations

Da API do Congresso até a página, em nove paradas

Despacho 01 · DadosTecnologiacommits 8d439c7 · 6b1964e

Nem tudo que tramita muda alguma coisa

-- coluna calculada pelo próprio Postgres
is_substantive boolean GENERATED ALWAYS AS (
  bill_type = ANY (ARRAY['PL','PLP','PEC','MPV',
    'PLV','PLN','PDL','PDC','PRC','PLC',
    'PLS','PRS','RCP'])
) STORED

O site lê só a view bill_substantive. Requerimento não ocupa a tela, não entra na fila de texto e não gasta IA.

Tropeço · 01/07

O id 163889 era o PL 914/2024 no Senado e a MSC 365/1989 na Câmara. Cada casa numera do seu jeito: a chave virou o par (casa, id).

Câmara, os quatro tipos mais comuns. Verde: substantivo. Cinza: fica fora do site. No total, 46% das 172.307 proposições passam no filtro.
Despacho 01 · DadosTecnologiamedição de 24/07 · commits 80d02b2 · 443cde5

Ninguém documenta o limite. Medimos.

API da Câmara: respostas “429, pedidos demais” conforme a taxa oferecida.
40/s
sem nenhum erro no Senado. Usamos 15/s.
  • O Senado devolve 400 a qualquer pedido sem User-Agent.
  • Os dois hosts são independentes: as casas rodam em paralelo.
  • O host de documentos aguenta 2/s.
Despacho 01 · DadosDeu erradocommits 5aca534 · 6cd99b0 · e7d9da2

Carregou sem erro. Não carregou tudo.

1º PL de 20262.599.887 último PL de 20252.622.750 PL 1396/2026 · id 2.611.850 · nunca carregado cursor = maior id já carregado2.641.462só anda para cá trecho que o cursor considera “já carregado”
A causa

Ler “só o que é novo” assumia que id maior é proposição mais nova. Não é. E o filtro de data da API é por tramitação, não por apresentação.

A prova

Contar banco contra API, mês a mês: março de 2024 tinha 264 no banco e 571 na API. Junho de 2025, 200 contra 468.

A correção

Fatias semanais por data de apresentação, num step-runner que avança um passo por vez e sobrevive a quedas.

25.810 → 62.614

substantivas da Câmara

Despacho 01 · DadosDeu erradocommits 025e30b · d978fc0 · 0994f50

Metade do Congresso estava invisível

357 × 28.850
matérias do Senado contra as da Câmara, em 24/07. Só entrava no banco o que já tinha sido votado.
  • A Lei 15.474/2026, do spray de pimenta, não era achável no site.
  • Catálogo novo, por sigla e ano: 10.915 matérias em 62 s, zero falhas.
  • Métrica nova: conta texto com conteúdo, não registro. Hoje, 81%.
Senado: % de matérias com texto, por ano. O buraco antes de 2018 é da fonte, não nosso.
Despacho 01 · DadosDeu erradocommits 0f58a0a · c993933 · d48f5e6

Um PDF cheio de texto pode não ter nenhuma palavra

A busca por “estepe” devolvia um projeto sobre contribuição social.

PDFs com fontes sem mapa de caracteres viram uma sopa de glifos. Passava no teste de quantidade de texto, e o vetor do lixo ficava “parecido” com qualquer busca.

if imprimiveis(texto) < 0.80:    # glifos sem mapa
    texto = ocr(pdf)                 # Tesseract
if parece_lixo(trecho):
    descartar(trecho)               # 2.785 trechos
Lixo na origem

A Câmara servia uma página do Diário Oficial no lugar de 33 MPs e mensagens. O “jabuti” com confiança 0,97 da MPV 1374/2026 era um acordo aéreo entre Brasil e Jamaica.

Quarentena

36 proposições com fonte inválida saem da busca e da IA. Os casos viraram um relatório de erros para a Câmara.

~13%
dos PDFs do Senado precisam de OCR. O documento de arquitetura dizia 70%: ninguém tinha medido.
Lição

“Carregou sem erro” não é “carregou tudo”. Confira contra a fonte, e documente o limite dela.

Despacho 02 · jun → jul 2026

Ninguém procura uma lei com as palavras da lei.

A pessoa digita “saúde mental nas escolas”. A proposição fala em “bem-estar psicológico de estudantes”. Como uma acha a outra?

Despacho 02 · BuscaProblemacommits 8dde629 · d51f622

Procurar pedaço de texto não é procurar palavra

O primeiro ILIKE '%saúde%' só achava a grafia exata, com acento e tudo, no meio de qualquer palavra.

-- antes: pedaço literal de texto
WHERE title ILIKE '%spray pimenta%'  -- 0

-- depois: full-text em português
WHERE search_vector @@ websearch_to_tsquery(
        'portuguese', 'spray pimenta')   -- 3

O full-text guarda o radical de cada palavra, sem acento e sem “de”, “a”, “o”. Um índice GIN aponta, para cada radical, as proposições que o contêm.

9 → 3.793
resultados para “saúde” na API, em 21/06: substring contra full-text
0 → 3
“spray pimenta” no site, em 24/07: “spray de pimenta” tinha um “de” no meio
Despacho 02 · BuscaTecnologiavoyage-3.5-lite · pgvector

Cada trecho vira um ponto num mapa de sentido

Despacho 02 · BuscaTecnologiacommits 17ae204 · 9c62562

O texto inteiro vira trechos, e cada trecho vira um vetor

Inteiro teorPDF da proposiçãoaté 80 mil caracteres corta Trechos ~2.000 caracteres200 de sobreposição… embeda Vetor[0,012 · −0,087 · 0,231 · …]1.024 númerosVoyage · input_type=document indexa Postgrespgvector + HNSWgrafo de vizinhosacha os parecidos semcomparar com todos
Hoje: 367.356 trechos de texto, 12.655 resumos gerados por IA e 2.924 notícias, todos com vetor. Custo total de embeddings desde junho: US$ 6,93.
Por que trechos

Uma proposição de 80 páginas fala de muitas coisas. Um vetor só para tudo mistura os assuntos.

Por que sobreposição

Uma frase cortada ao meio ainda aparece inteira no trecho vizinho.

O primeiro tropeço

O plano grátis da Voyage aceitava 3 requisições por minuto: a ingestão quebrava e ganhou retry.

Despacho 02 · BuscaTecnologiafrontend/src/app/buscar · supabase/functions/busca

Palavra-chave primeiro; sentido só quando a palavra não basta

Despacho 02 · BuscaDeu erradocommits c04fd5e · d66ca48

O índice existia. A consulta é que não usava.

14/07 · busca semântica em 46 s

O vetor da consulta chegava como texto. A conversão de ~7 KB era refeita em cada linha, nos ~120 mil trechos, e o banco lia a tabela inteira.

-- converte UMA vez, numa variável tipada
v := p_embedding::vector(1024);
46 s → 0,45 s
28/07 · palavra-chave em 156 ms

Um OR entre dois campos de texto, através de um LEFT JOIN, impedia o planejador de usar os dois índices GIN: varria 48.914 proposições.

-- dois braços, cada um no seu índice
SELECT … WHERE search_vector    @@ q
UNION ALL
SELECT … WHERE ai_search_vector @@ q
156 → 17,5 ms56.552 → 1.338 buffers

Efeito colateral: antes da correção, 2.008 erros de timeout no Sentry. A busca híbrida tinha média de 1,85 s e máximo de 7,99 s, encostando no limite de 8 s.

Despacho 02 · BuscaDeu erradocommit 2f94bdf · search_query_cache

Um robô clicou em todas as ordens possíveis

143.512
linhas no cache para só 3.060 conjuntos de temas
36,3%
de acerto no cache (13/08)
65.308
chamadas à Voyage em 12/08 (eram 7 por dia)
99,0%
de acerto hoje, com os temas ordenados
Lição

Toda chave montada por concatenação precisa de ordem estável. Todo índice precisa de um EXPLAIN que prove que ele é usado.

Despacho 03 · jul → ago 2026

Ninguém lê 79 mil proposições. Um modelo lê, por meio centavo cada.

Resumo, temas, jabutis e impacto fiscal: o que dá para pedir numa chamada só, e o que sai caro.

Despacho 03 · IATecnologiaingestion/analyze.py · llm.py

Uma chamada, um formulário que o modelo é obrigado a preencher

# um lote por noite, um pedido por proposição
client.messages.batches.create(requests=[{
  "custom_id": id_da_proposicao,
  "params": {
    "model": "claude-haiku-4-5",
    "system": [{"type": "text",
      "text": PROMPT,
      "cache_control": {"type": "ephemeral"}}],
    "messages": [{"role": "user",
      "content": texto[:80_000]}],
    "tools": [REGISTRAR_ANALISE],
    "tool_choice": {"type": "tool",
      "name": "registrar_analise"},
  }} for ... in lote_de_150])
  • Batch API: responde em até 24 h, com 50% de desconto. Ninguém precisa do resumo às 3 da manhã.
  • Ferramenta forçada: o modelo não conversa, só preenche o formulário: resumo, título curto, 3 a 6 pontos-chave, temas, leis citadas, jabutis e impacto fiscal.
  • Uma leitura, sete saídas: o texto inteiro é pago uma vez só.
0,004
dólar por proposição, no máximo 0,005
13.611
analisadas até 06/10
1.648
têm impacto fiscal sem dizer de onde vem o dinheiro
Despacho 03 · IADeu erradocommits 4ac1bf2 · 873850f · 9f65bb8

O lote de 5.000 que acabava com o mês em dois dias e meio

Dias até esgotar o teto de US$ 50 por mês da análise.
O lote órfão

Um deploy no meio da espera perdia o id do lote. A Anthropic cobrava, e a próxima rodada pagava de novo: ~US$ 2,20 por lote. Agora o id vai para a tabela llm_batch antes de esperar.

O preço da paciência

13.611 de 79.187 substantivas têm análise (17%). Analisar o resto custaria uns US$ 250 de uma vez; a 150 por noite, leva mais de um ano. A fila prioriza o que virou lei e o que as pessoas visitam.

Despacho 03 · IADeu erradocommit 428a092 · 12/07 → 07/08

O esquema pedia uma lista. Às vezes vinha uma string.

// o esquema pedia: lista de strings
"key_points":
  "<item>Cria o...</item><item>..."
# converte E conta, em vez de pular calado
pontos, coagido = _as_str_list(bruto)
if coagido:
    resumo["coerced_lists"] += 1

Três camadas de defesa tratavam o caso pulando o campo, sem avisar ninguém. O erro visível virou perda silenciosa.

26 dias
sem ninguém notar
1.768
linhas reparadas
10.623
temas de volta à busca

Ainda acontece: de 10 a 24 vezes por lote de 150, em outubro. A diferença é que agora aparece no resumo de cada rodada.

Despacho 03 · IAProblemacommits 611a8aa · cb7549a · 03/08

Jabuti ou não? Vote antes de a carta virar.

Jabuti é o trecho estranho ao tema que pega carona num projeto. “Jabuti não sobe em árvore: se está lá, alguém pôs.”

Despacho 03 · IADeu erradocommit 152cc6a · 06/07 → 01/08

Um corte fixo mede o tamanho do documento, não o desvio

Despacho 03 · IASoluçãocommits 1fe1205 · fb70c65 · 3552022

Um detector para não deixar passar, outro para não acusar à toa

1 · não deixar passar texto inteiro (até 80 mil) Haiku, dentro da análisecusto extra: zero suspeito 2 · não acusar à toa trechos com vetor os 6 mais estranhosordenação, não corte fixo Sonnet 5 confirmaem lote, 20 por noite jabuti
12× mais achados por proposição

Confirmar suspeitos: 48 jabutis em 153 proposições (31%), por US$ 2,21. Varrer às cegas: 4 em 150 (2,7%), por US$ 3,97.

Os limites, sem enfeite

Precisão estimada em ~55%, numa amostra de 9. O recall nunca foi medido. Hoje, 419 proposições têm o selo.

NULL vem primeiro
ORDER BY recente DESC  -- NULL no topo!
ORDER BY coalesce(recente, false) DESC

0 das 20 primeiras da fila eram recentes, até 05/10.

Lição

Troque o limiar por ordenação e segunda opinião. E toda economia precisa de alguém de olho no gatilho de voltar atrás.

Em 01/08, por custo, paramos de varrer proposições nunca checadas, com um gatilho combinado: “se a imprensa noticiar um jabuti que não pegamos”. Em 03/08, de 10 jabutis noticiados, tínhamos pegado zero. O ramo só voltou em 24/08.

Despacho 04 · jul 2026

O que é importante no Congresso hoje?

Em 12/07, o “Em destaque” do site era uma fila de renovações de concessão de rádio.

Despacho 04 · RelevânciaDeu erradocommits 6c12cec · 6c04d88 · dbec258

Sinal de procedimento mede movimento, não importância

13,6 → 55,0
nota da PEC 221/2019, fim da escala 6x1
nº 12.135
posição do PDL de rádio mais bem colocado hoje, entre 22.066

A IA lê só a ementa e classifica o alcance. Média da nota por tipo: estruturante 0,88 · setorial 0,59 · rotineiro 0,23 · honorífico 0,04. Custo: US$ 0,0008 por ementa.

Despacho 04 · RelevânciaTecnologiaingestion/relevance.py

Uma soma ponderada que guarda o porquê

nota = Σ peso × sinal     # sinal entre 0 e 1
     × 0,5 ^ (dias / meia_vida)

meia_vida = 243 × (1 + 2 × importância)
# de 243 a 729 dias; "dias" conta
# desde a última atividade
-- cada nota vem com a explicação
signals = {"midia_hot": 1, "importancia_llm": 0.95,
           "tipo": 1, "coautoria": 0.9, ...}

Mídia em alta: toda segunda, o Sonnet busca na web os projetos em discussão, o Haiku extrai o número de cada um, e o número só vale depois de conferido no banco ou na API.

Os 12 pesos reais. Verde: sinais de conteúdo, que entraram depois de 12/07. Cinza: sinais de procedimento.
Lição

O fácil de medir era movimento. O que importava era conteúdo, e para isso alguém precisa ler.

Despacho 05 · jul → ago 2026

A busca estava certa. Só estava lenta.

Duas causas que nenhum EXPLAIN mostra: memória e geografia.

Despacho 05 · LatênciaDeu erradocommit 89295da · EPIC 11–13/08

O índice maior que a memória, duas vezes

Despacho 05 · LatênciaSoluçãomigration 20260729180000 · 60 consultas reais

1 bit por número para achar, o vetor inteiro para ordenar

RepresentaçãoRecall@20Índice
meia precisão (halfvec)100%438 MB · não cabe
512 dimensões80%menor
binário, sem reordenar49,8%69 MB
binário + reordenar 40099,3%69 MB

Recall@20: dos 20 resultados da busca exata, quantos a busca rápida encontra. Medido em 60 consultas reais antes de escolher.

-- 1) 400 candidatos, distância de Hamming
SELECT chunk_id FROM bill_chunk_half
ORDER BY embedding_bin
      <~> binary_quantize(v)
LIMIT 400
-- 2) reordena os 400 pelo cosseno,
--    com o vetor em meia precisão
  • Preencher a coluna nova com UPDATE: 5 linhas/s, 9 horas. Um INSERT em tabela à parte: 3.333 linhas/s.
  • O assistente de IA sugeriu cortar para 512 dimensões antes de medir. Teria perdido 20% do recall.
  • Índice novo construído em 100 s. Busca híbrida: 8,4 ms de mediana.
Despacho 05 · LatênciaDeu erradocommits b634b60 · dd67ac3 · 12–14/08

Lenta por geografia, não por SQL

Duas armadilhas: medir a distância até o banco pelo hostname deu 3 ms, mas quem respondia era a borda da Cloudflare; pela porta do Postgres, eram 184 ms. E no deploy a busca por sentido falhava calada e caía na palavra-chave, respondendo 200: a chave da Voyage só vale em outro endereço. Hoje a resposta avisa quando está degradada.

Lição

Meça antes de otimizar: recall antes de cortar, latência na porta certa. Às vezes, a otimização certa é pagar por RAM.

Despacho 06 · jul → out 2026

Ninguém aperta o botão. 33 crons apertam.

De madrugada, o banco acorda a API, que acorda o worker, que acorda a IA. Quem confere se alguém acordou?

Despacho 06 · CronsTecnologiacron.job · lido em 06/10

Um dia na vida do sistema, em horário de Brasília

Despacho 06 · CronsTecnologiapg_cron + pg_net + FastAPI

O cron mede se o pedido saiu, não se o trabalho foi feito

pg_cron33 agendasdentro do banco gatilho SQLtem fila?o diário já rodou? net.http_postassíncrono: nãoespera a resposta API · FastAPIuma tarefa por casa202 aceito · 409 não ingestion_runrunning → fimpulso a cada 30 s resultadolinhas novasno banco até aqui, o pg_cron já registrou “succeeded” 409: o disparo se perde,sem erro em lugar nenhum o trabalho sóexiste aqui

Por que o banco agenda tudo: o primeiro agendador rodava no Railway e pulou um dia depois de uma rajada de deploys. Agenda dentro do banco não reinicia com o deploy da API.

Despacho 06 · CronsDeu erradomigration 20261005150000 · 20/09 → 05/10

Uma corrida decidida por segundos

A trava

O gatilho do detector de jabuti não disparava se houvesse qualquer tarefa rodando. A atualização de textos termina por volta de 01:40.

O placar

Rodou em 16 de 66 dias. O pg_cron registrou “succeeded” nos 66. Casamento perfeito com o horário em que a outra tarefa terminava.

O diagnóstico errado

Em agosto, o assistente de IA culpou um 409 da API, e liberamos a API. O pedido nem saía do banco: eram duas travas, e soltamos só uma.

Despacho 06 · CronsSoluçãocommits 097efa1 · eb93508 · 9f65bb8

Todo push mata o que está rodando

um run de texto, visto do banco pulso a cada 30 s git push → redeployprocesso morto no banco, continua “running” reaper, todo minuto3 min sem pulso → failed filasegue
45 → 3 min
para destravar a fila depois de uma queda
98
zumbis no histórico
52 de 70
tinham um commit em main nos 20 min anteriores
80%
dos ~54 mil disparos mensais do pg_cron são o reaper
Despacho 06 · CronsDeu erradocommits f324e55 · 4d030bf · a856e90

Uma trava global onde a regra era por casa

Uma tarefa pesada por vez, para não estourar a API. Mas a trava era global: um backfill da Câmara de 4 dias bloqueou o Senado e a ingestão diária, a única fonte de proposições novas.

O diário lia 2 dias para trás. Ficou 3 dias bloqueado: só não perdemos dado porque caiu num fim de semana.

  • Uma trava por casa: os hosts são independentes.
  • O diário reserva prioridade por 2 h e insiste a cada 10 min.
  • Janela de 2 para 4 dias. Desde 29/07, nenhum dia sem diário.
Duração média da ingestão diária, por semana (minutos).
Lição

“O cron rodou” e “o trabalho foi feito” são perguntas diferentes.

Foram cinco corridas de cron no projeto. Todas com o painel verde.

Despacho 07 · ago → out 2026

O mecanismo diz que está tudo bem. O resultado diz a verdade.

Em vez de vigiar 33 crons, passamos a vigiar 8 resultados.

Despacho 07 · AlertasTecnologiaSentry · /health · ingestion_run · expectativas

Quatro camadas de olho, do erro ao resultado

1 · Erro

Sentry

No site e na API, com source maps. Foi ele que mostrou os 2.008 timeouts da busca. O rastreio de desempenho amostra 2% das visitas.

2 · Saúde

/health

O site responde se está vivo. A API responde ok, velho ou degradado: velho é dado com mais de 36 h.

3 · Execução

ingestion_run

Cada tarefa vira uma linha com início, pulso e fim. O painel admin cruza disparo com run e mostra os 409.

4 · Resultado

Expectativas

Oito perguntas sobre o dado, não sobre o processo. Violou? E-mail, três vezes por dia.

Nenhuma das três primeiras camadas avisou sobre as corridas do capítulo anterior: em nenhuma delas houve erro.

Despacho 07 · AlertasSoluçãomigration 20260808090000

Cada expectativa diz quando algo aconteceu e quando é tarde

with expectativa(nome, ultimo, max_horas)
as (values
  ('news',
   (select max(published_at) from news), 6),
  ('tema-em-alta',
   (select max(discovered_at) from bill_hot),
   192),
  ...)
select nome from expectativa
 where ultimo < now()
       - make_interval(hours => max_horas)
  • O limite sai do ciclo de cada coisa: horário tolera 6 h; diário, 36 a 48 h; semanal, 8 dias.
  • Silêncio de 12 h por alerta: alerta que vira ruído ninguém lê.
  • A consulta mora no corpo da função, não numa tabela: SQL guardado e executado com privilégio de dono é porta de injeção.
ExpectativaLimiteÚltimo alerta
news6 h19/09
embed-news12 hnunca
analise-llm36 h06/10
tramitacao36 h05/10
texto-proposicao48 h10/08
rascunho-social96 h05/10
tema-em-alta8 dias24/09
tema-popular8 dias28/08
Despacho 07 · AlertasSoluçãocommits 5725cd2 · cbc566f · ingestion_run

A rede de segurança redispara sozinha

Temas em alta descobertos por rodada. Segunda, 03:37, é o horário agendado. Os disparos das 04:41 em outros dias são a rede de segurança: “o tema mais novo tem mais de 8 dias? Dispara de novo.”

Antes da rede, a descoberta ficou três semanas muda com o painel verde: perdeu a corrida do minuto :00, depois bateu no teto de orçamento, depois cortou a resposta no limite de tokens e gravou zero. Ainda há rodadas que pagam, acham zero e terminam como sucesso. A causa não está confirmada.

Despacho 07 · AlertasDeu erradoe-mails de 05/10 e 06/10

O alerta é tão bom quanto a coluna que ele lê

05/10 · “tramitacao há 3 dias”

Mediu o Congresso, não a gente

A coluna é o último evento no Congresso. No fim de semana do 1º turno, houve zero tramitações. Na segunda, 184. E o Senado tem essa coluna vazia em todas as proposições: o alerta só cobre a Câmara.

06/10 · “analise-llm há 72 horas”

A coluna só muda no INSERT

A análise rodou nas duas noites: 150 por noite. Mas ela atualiza linhas que outro estágio cria, e esse outro estágio não rodou. A data lida é a de criação da linha.

E quando acertou

17/08: tema em alta 10 dias sem dado novo. 18/08: rascunho social parado, que levou à corrida do detector de jabuti. 05/10: rascunho social de novo, que revelou três bugs reais, assunto do capítulo 9.

Lição

Vigie o resultado, tire o limite do ciclo e desconfie da coluna que você está lendo.

Despacho 08 · jul → set 2026

A maior conta não veio da IA. Veio dos robôs.

Em agosto: 171 visualizações de página feitas por gente, em 8 dias. E 355.823 execuções de função, em 24 horas.

Despacho 08 · CustosProblemaVercel FOCUS · 28/07 → 04/10

A fatura da hospedagem, semana a semana

Uso semanal da Vercel em US$, sem a assinatura de US$ 20 por mês.
US$ 114,93
1º ciclo, 28/07 a 27/08
US$ 34,43
2º ciclo, 28/08 a 27/09

Para comparar, a IA (Claude) por mês: julho US$ 67,95, agosto US$ 39,40, setembro US$ 45,73. Em agosto, a hospedagem custou quase o triplo da IA.

Despacho 08 · CustosTecnologiaanatomia de 24/08 · Fluid Compute

Invocação é barata. Paga-se tempo e bytes.

O 1º ciclo até 23/08, por item. A observabilidade, cobrada à parte, somava mais US$ 23,49.

É a mesma coisa medida de três jeitos.

2,22 mi execuções × ~27 KB      ≈ 59 GB
2,22 mi execuções × ~1 s × 2 GB ≈ 1,2 mil GB·h
~70% do tempo pago = esperar banco e rede

Página lenta custa dinheiro mesmo sem fazer nada: no Fluid Compute, a memória é cobrada pelo tempo de relógio. Quando o robô sobrecarregava o banco, cada página esperava mais e custava mais.

Despacho 08 · CustosDeu erradocommit aabc416 · 08–09/08

Declarar cache não é ter cache

Antes · a mesma página, três vezesx-vercel-cache: MISS x-vercel-cache: MISS x-vercel-cache: MISS cache-control: private, no-store
Depoisx-vercel-cache: HIT tempo até o 1º byte: 0,18–0,21 s
// sozinho, o revalidate é ignorado
export const revalidate = 86400;

// lista vazia: ISR sob demanda, sem
// pré-gerar 78 mil páginas no build
export async function generateStaticParams() {
  return [];
}
16 h de CPU
num único dia (08/08): US$ 9,26, e o crédito do ciclo acabou ali

O analytics viu 26 visualizações em 2 dias. Os logs, 45 renderizações em 4 minutos: robôs percorrendo as 16.838 URLs do sitemap. Os commits de julho diziam “ISR ligado”. Ninguém tinha conferido o cabeçalho.

Despacho 08 · CustosProblemacommits 57dd60b · ec3cd52 · regras de firewall

Três adversários, três eixos de bloqueio

Requisições por hora no /buscar, fase a fase. Vermelho: o robô sem bloqueio.
1 · nomemeta-externalagent/1.1Ignorava o robots.txt genérico. Bloqueado pelo nome, voltou com outro: meta-webindexer.
2 · classebot | crawl | spider | indexer | meta- | …Bloqueia qualquer um que se declare robô. A Meta não escapa trocando de crachá.
3 · endereço“Chrome 133” · 47.79.201.58 · Alibaba CloudDisfarçado de navegador, com referer falso do Google, a 7 por minuto: abaixo do rate limit. Só a faixa de IP denunciava.
Despacho 08 · CustosSoluçãovercel firewall · regras reais, em ordem

robots.txt é etiqueta. Firewall é porta.

Despacho 08 · CustosDeu erradocommits 6aa0835 · cacf2b9 · 31/08

O sitemap convidava os robôs para 26.507 páginas vazias

A consulta do sitemap checava se existia uma linha de análise, não se havia análise nela. 26.507 linhas estavam vazias: sem resumo, sem tema, sem jabuti.

36.385
URLs anunciadas
9.878
depois do filtro (−73%)
23%
da fatura do 2º ciclo era escrita de cache

Página sem análise ganhou noindex, follow, que se desfaz sozinho quando a análise chega.

Decisão do dono · 31/08

Não migrar para a Cloudflare

Ela não cobra transferência, mas a troca custaria semanas de porte do ISR e das imagens de prévia para economizar uns 10 a 20 dólares por mês. O custo era superfície criada pelo próprio projeto, e o projeto podia desfazê-la.

Lição

Paga-se tempo e bytes, não cliques. E toda URL que você anuncia é um convite pago.

Despacho 09 · jul → out 2026

Dado parado não fiscaliza ninguém. Precisa chegar nas pessoas.

Newsletter, Instagram, Facebook e Bluesky, sem ninguém apertando “publicar”.

Despacho 09 · AlcanceTecnologiaingestion/social.py · /social/autopublish

Do rascunho às três redes, sem clique humano

de manhã · gerar 07:04 · cronum formato por dia candidatosSQL ranqueado Sonnet 5 escreveaté 280 caracteres rascunhona fila, pendente e-mail ao adminsó pode vetar lê a fila o dia todo · publicar pg_crona cada 10 min planejador SQLslots de 12h e 19h post vencido?só então acorda a Vercel imagem + carrosselIA faz só o fundo Bluesky · IG · FBe relê o banco
Pular a revisão da Meta

Bluesky pelo protocolo aberto, de graça. Instagram e Facebook por um agregador, o Zernio, cujo app já é aprovado pela Meta.

Texto desenhado, não gerado

Modelo de imagem erra letra. A IA gera só o fundo; título e números são desenhados por código, com @vercel/og.

Barato por design

US$ 0,014 pelo texto de cada post. O planejador roda em SQL e só acorda a Vercel quando há post para sair.

Despacho 09 · AlcanceTecnologiasocial_auto_plan() · replay de 30/09 a 08/10

A primeira semana real da fila automática

Despacho 09 · AlcanceDeu erradocommit f01802c · 05/10

Limitar antes de filtrar: zero rascunhos, para sempre

Despacho 09 · AlcanceDeu erradocommit 0081376 · 05/10

O raciocínio do modelo come o orçamento da resposta

No Sonnet 5, o pensamento adaptativo sai do mesmo max_tokens da resposta. Pensou demais, o JSON veio cortado.

Mesma causa derrubou a canonicalização mensal de temas em 01/10, com o JSON cortado no caractere 3.212. Teto não é custo: paga-se o que o modelo usa. Depois da correção, o maior post gastou 1.343 tokens.

Despacho 09 · AlcanceSoluçãomigration 20260930120000

Um UPDATE que casa zero linhas não é erro. Devia ser.

update public.social_post
   set posted = posted || ...,
       status = 'posted'
 where id = p_id
   and (public.is_admin()
        or auth.role() = 'service_role');
-- ↑ a correção: aceitar a chave de serviço

O robô publica com a chave de serviço. Nela não há usuário: is_admin() é falso, o UPDATE casa zero linhas e ninguém reclama.

O post sairia no Bluesky sem ficar registrado, e cada nova tentativa publicaria de novo. O Bluesky não tem proteção contra duplicata.

  • Depois de publicar, o robô relê o banco. Se não gravou, para tudo.
  • Pegar a tarefa é atômico: FOR UPDATE SKIP LOCKED, trava de 6 min, até 5 tentativas.
  • Achado antes de ir ao ar, no ensaio feito de dentro do banco.
Despacho 09 · AlcanceProblemabanco e feed público · 06/10

A automação está pronta. A audiência, ainda não.

0
seguidores no Bluesky: 14 posts, 2 curtidas no total
2
assinantes confirmados da newsletter, 11 edições compostas por IA
10
contas de usuário no site
10
posts automáticos até 06/10: 7 nas três redes, 3 só no Bluesky

O sistema publica sozinho, todo dia. O próximo problema não é de engenharia: é de distribuição.

Lição

Automatizar é fácil. Difícil é não duplicar, não sumir e não falhar calado.

O que ficaLiçãonove despachos, um resumo

Seis padrões que apareceram em quase todos os despachos

  1. 1Valide pelo resultado, não pelo mecanismo“Succeeded” não é “feito”: as cinco corridas de cron, a descoberta muda, o gerador de posts.
  2. 2Confira contra a fonteO cursor que pulava ids, o Senado invisível, o placar sem ausentes.
  3. 3Meça antes de otimizarEXPLAIN antes do índice, recall antes de cortar dimensão, latência na porta certa.
  4. 4Erro silencioso é o pior erroA lista que virou string, o UPDATE de zero linhas, a busca degradada que respondia 200.
  5. 5Configurar não é terISR declarado e desligado, índice que existia e não era usado, cache que ninguém conferiu.
  6. 6Toda economia precisa de um gatilho com donoO detector de jabuti mais barato, o teto de IA, a meta de voltar ao plano grátis.
O que ficaTransparênciaregistrados em commits, tasks e memória do projeto

O assistente de IA acelerou tudo. E errou com convicção.

436 dos 571 commits foram escritos em par com um assistente de IA (Claude). Alguns erros dele, todos registrados:

  • “O acervo dobra a cada 13 dias.” Era o backfill terminando.
  • “Corte para 512 dimensões.” Antes de medir. Perderia 20% do recall.
  • “Não é robô, é um monitor em loop.” O painel escondia a query string.
  • “ISR ligado.” Dito em dois commits, sem conferir o cabeçalho.
  • “É um 409 da API.” O pedido nem saía do banco.
  • “O cache expulsa o índice da RAM.” A medição mostrou 100% de acerto.
  • “Recentes primeiro.” Com NULL no topo, nunca funcionou.
  • Um grep numa lista de variáveis despejou uma chave privada no log local. Regra nova: nunca listar segredos.

IA acelera. A verificação continua sendo sua: todo diagnóstico precisa de um número que o confirme.

O que ficaEm abertoachados de 06/10, ao preparar esta palestra

Em tramitação: o que achamos preparando esta palestra

  • A última atividade congelouUma atualização cai por timeout desde ~20/07, sem alarde. Das 912 proposições novas desde agosto, só 375 entram no ranking.
  • Dois detectores no papel, um na prática334 dos 419 selos de jabuti vêm só do primeiro detector, sem a confirmação do segundo.
  • Cache de prompt configurado, nunca usadoZero leituras de cache em 46 mil chamadas do Haiku. Provável causa: o prefixo é menor que o mínimo do modelo.
  • Os robôs voltaramNova onda no /buscar, de outros endereços e com Chrome em versões rotativas. E um robô de SEO com 12 mil acessos por dia nas páginas de proposição.
  • Um estágio que nunca rodouA canonicalização mensal de temas nunca terminou: perdeu a vez em agosto e falhou em setembro e outubro. A próxima tentativa é em 01/11.
Situação: em tramitação

Obrigado. Perguntas?

Tudo o que você viu aqui roda num navegador: esta apresentação é um único arquivo HTML, com os dados reais embutidos.