allanlotta
~/blog/counting-cars-for-prediction-markets

Contando carros para mercados de previsão

Um mercado de previsão precisa de algo para resolver. Queríamos um sinal local, contínuo, público e difícil de contestar. Carros cruzando uma linha numa pista.

A cada cinco minutos abre um mercado: a contagem vai ficar acima de N? As pessoas apostam para cima ou para baixo, acompanham o stream ao vivo, e o mercado é liquidado on-chain quando o ciclo fecha. No pico, cerca de 5.000 pessoas acompanham a mesma contagem ao mesmo tempo. Se o número estiver errado, dinheiro de verdade resolve errado.

Eu construí o pipeline que produz esse número. A maior parte do código, das auditorias, das correções e dos runbooks foi escrita por agentes. Este post é sobre como a detecção funciona e como o sistema em volta dos agentes manteve tudo vivo.

O pipeline

câmera pública (HLS) ──▶ ffmpeg ──▶ fila do frame mais novo
                                          │
                                          ▼
                     YOLO (carro, ônibus, caminhão) + ByteTrack
                                          │
                                          ▼
                       contador: cruzamento da linha por track
                                          │
                 ┌────────────────────────┼─────────────────┐
                 ▼                        ▼                 ▼
        overlay no frame          ciclos de 5 minutos  WebSocket
        (OpenCV, só ROIs)         (SQLite, alinhados   (contagem
                 │                 ao relógio)          parcial)
                 ▼                        │                 │
   ffmpeg ──▶ MediaMTX ──▶ WebRTC         ▼                 ▼
           stream público          keeper resolve    market maker
           ~5.000 espectadores     o mercado         cota as odds

Python, OpenCV, Ultralytics, ByteTrack, FFmpeg, MediaMTX, um VPS de 8 cores sem GPU. Essa última parte moldou cada decisão.

Como um carro vira uma contagem

Detectar, sozinho, não conta. Um carro fica no frame por cem frames, e um modelo que diz "carro" cem vezes não contou nada. Então o modelo roda com um tracker, e a contagem é uma propriedade de um track, não de uma detecção.

 track 4127 (carro)
   frame 1..8    visto, ainda inelegível  ← mínimo de frames
   frame 9       lado A confirmado        ← distância com sinal
   frame 23      lado B                   ← cruzamento
   count += 1    track 4127 marcado como contado, nunca mais
   frame 60      track some, estado expira ← TTL

Cada track carrega o próprio estado: em quantos frames foi visto, de que lado da linha foi confirmado, se já foi contado. Um cruzamento só conta quando um track foi confirmado de um lado e depois confirmado do outro. Um track que oscila em cima da linha não conta. Um track que se perde atrás de um ônibus e volta com um id novo é o modo de falha que sobra, e é para ele que existem as checagens por amostra.

A linha é um polígono no frame, editável no admin, por câmera. Caminhões e ônibus contam como veículos. Pessoas e peixes têm perfis próprios, com modelos próprios, porque o pipeline não se importa com o que cruza a linha.

Rápido o bastante numa CPU

A câmera manda 1080p a 25 frames por segundo. O YOLO olha para 640 pixels. O stream sai em 854 por 480. Nada no meio precisa da resolução cheia, então o pipeline não a carrega. As filas guardam só o frame mais novo e descartam o resto. O encoder roda na própria thread, com o próprio ritmo.

O overlay foi onde isso foi aprendido do jeito difícil. Um redesign moveu o desenho para o PIL, com camadas de frame inteiro. A produção foi de fluida para 5,6 frames por segundo, 188% de CPU no processo Python e 11 segundos de atraso, enquanto a GPU ficava em 39%. O gargalo era a composição, não a detecção. A correção foi usar primitivas do OpenCV desenhadas só dentro das regiões de interesse. Os efeitos voltaram um por vez, cada um medido em três resoluções antes de ir para produção.

Esse incidente virou um documento, e o documento virou uma regra: medir antes e depois em 720p, 1024p e 1080p, ou não sai.

Público de propósito

A contagem é o mercado. Se as pessoas não conseguem ver, não vão confiar, e se não confiam, não apostam.

Então o stream anotado é público via WebRTC, com menos de um segundo de atraso, direto do gateway de mídia. Os snapshots são públicos. A contagem parcial é enviada por WebSocket para quem estiver ouvindo, o que inclui o market maker cotando odds enquanto o ciclo ainda está rodando. Só o admin e todos os endpoints de escrita ficam atrás de autenticação.

Continuar vivo

Um mercado que resolve com um número errado é pior que um mercado que pausa. Então a maior parte do trabalho depois do primeiro mês não foi detecção. Foi fazer o número sobreviver ao mundo real.

 câmera cai   ──▶ supervisor reinicia com backoff
              ──▶ 120s offline ──▶ failover para a próxima câmera
                                   de um catálogo de 155, com
                                   health check, cada uma com
                                   uma linha pré-configurada
              ──▶ alerta no Telegram, deduplicado
 ciclo quebra ──▶ marcado parcial, nunca liquidado como completo
 processo cai ──▶ estado do ciclo recuperado do SQLite no boot

Uma queda rápida no meio de um ciclo apagava as contagens anteriores. Um loop de snapshot que quebrava morria em silêncio na primeira exceção. O backoff do supervisor se resetava sozinho e reiniciava a cada dez segundos, para sempre. Cada um desses agora é um teste.

Construído com agentes

O repo tem 24 arquivos de teste e mais ou menos o mesmo número de documentos. Essa proporção é o método.

audita ─▶ planeja ─▶ corrige ─▶ mede ─▶ registra ─▶ nova auditoria
   │                                       │
   │   2 críticos, 7 altos, 14 médios,     │   docs/ + memória:
   │   19 baixos, cada um com file:line,   │   o que quebrou,
   │   severidade e ordem de ataque        │   por quê, e a regra
   └───────────────────────────────────────┘

Um agente auditou a base de código inteira e produziu uma lista ranqueada: dois críticos, sete altos, catorze médios, dezenove baixos, cada um com arquivo e linha, uma reprodução e uma ordem de ataque sugerida. Os críticos eram um deploy que sobrescrevia a configuração de câmera em produção e loops em background que morriam em silêncio. Os dois foram corrigidos na mesma semana, os dois têm testes, e o documento da auditoria ficou como mapa para a próxima passada.

Toda investigação termina num arquivo. O incidente do overlay. A análise de performance que achou resolução vazando pelo pipeline inteiro. O mapa de como três serviços downstream consomem a API, que define o contrato que nada pode quebrar. O plano do catálogo de câmeras, em cinco fases, cada uma entregue e marcada como feita.

E uma memória que sobrevive ao chat. Quando o restream de câmeras do YouTube a partir de um IP de datacenter continuava falhando, o agente achou a combinação que funciona, e depois achou a parede que não: o YouTube invalida a sessão depois de uma hora naquele IP, não importa o que se faça, enquanto um IP residencial segura indefinidamente. Isso está registrado como um fato com data, para que nenhuma sessão futura gaste um dia redescobrindo.

Por que isso não é sobre carros

 câmera de trânsito           qualquer sinal ao vivo
 ─────────────────────        ──────────────────────────────
 track, não detecção          conta a entidade, não o evento
 lado A, depois lado B        duas confirmações antes de um fato
 overlay medido em 3 res      performance é teste, não achismo
 stream público               o número que dá para verificar
 ciclo parcial                leitura ruim pausa, nunca mente
 catálogo de failover         a próxima fonte, já configurada
 auditoria, ordem de ataque   agentes que ranqueiam o que fazem
 memória com datas            o que era verdade, e quando

A parte difícil nunca foi o modelo. Detecção foi resolvida em uma semana. Produzir um número em que milhares de pessoas podem apostar, a cada cinco minutos, a partir de uma câmera pública numa máquina sem GPU, por meses, é um problema de sistemas. Os agentes fizeram o trabalho de sistemas, porque o loop deu a eles um jeito de auditar, corrigir, medir e lembrar.

O que eu fiz

Eu escolhi o sinal, desenhei a primeira linha, defini a regra de que o número é público e a regra de que um ciclo quebrado nunca liquida. Construí o primeiro pipeline, depois passei as auditorias, as correções, o catálogo e o failover para os agentes, e li todo documento que eles deixaram. Quando um desses documentos disse "não faça deploy sem medir em três resoluções", isso era o sistema melhorando a si mesmo.