Jesús Martínez
EN
← Trabajo · Caso de estudio

USD 77 millones en publicidad política, ahora consultables.

Cada estación de TV registra sus contratos de publicidad política en PDF. Sumarlos significaba leerlos uno por uno. Construí el sistema que los lee, descubre quién está pagando de verdad y conecta cada dólar con su fuente.

Cliente
Un asesor político en EE. UU.
Rol
Ingeniero líder y arquitecto, en solitario
Período
Oct 2025 a ago 2026
Construido con
Python, FastAPI, Supabase, Claude, Gemini, React

El contexto

El dinero es público. Las cifras no.

El gasto en publicidad política en TV es de registro público. Está en los archivos públicos de la FCC como miles de contratos en PDF, cada estación con su propio formato y cada comprador con su propio nombre. Un PAC, un comité y una campaña pueden gastar por el mismo candidato sin que ningún documento lo diga.

El problema

Una pregunta sencilla. Miles de documentos.

El cliente necesitaba una respuesta cada semana: ¿cuánto gastó cada lado en cada mercado? Obtenerla significaba abrir los documentos uno a uno y hacer las cuentas a mano, mientras seguían llegando más.

El enfoque

No hay que parsear los documentos. Hay que leerlos.

Las plantillas se rompen en cuanto una estación cambia su formulario, y lo cambian sin aviso. Así que el sistema lee cada contrato como lo haría una persona: los modelos de lenguaje leen y el código verifica.

01

Recolectar

Consulta la FCC para 44 estaciones en los mercados de Nueva York y Filadelfia, descarga cada documento nuevo y omite los que ya vio.

02

Leer

Decide si cada documento es político y luego extrae cada línea: anunciante, monto, fechas, cuñas. Lo que genera duda va a una persona, no a los totales.

03

Resolver

Descubre quién está pagando de verdad. Los nombres de los anunciantes se vinculan a entidades políticas y se rastrean a través de los PAC hasta el candidato al que apoyan.

04

Responder

Gasto semanal por coalición, tendencias y desgloses. Cada cifra abre el documento original del que salió.

Decisiones y trade-offs

Tres decisiones que lo hicieron funcionar.

01

Leer, no parsear.

Los PDF cambian más rápido de lo que se pueden arreglar las plantillas, así que los modelos hacen la extracción. Claude clasifica y resuelve, Gemini extrae, todo detrás de un único gateway, así que cambiar de modelo es un cambio de configuración.

Elegí extracción con LLM en vez de plantillas
02

Recordar cada respuesta.

Antes de preguntarle a un modelo quién es un anunciante, el sistema revisa lo que ya sabe, y cada corrección humana queda guardada para siempre. Las búsquedas bajaron de unos 3 segundos a menos de 100 ms, y los anunciantes repetidos no cuestan nada.

Elegí una caché que aprende en vez de llamar al modelo cada vez
03

Mandar la duda a una persona.

Los resultados con baja confianza van a una cola de revisión en vez de darse por buenos o descartarse. Los revisores pueden corregir registros ya contabilizados, no solo llenar vacíos.

Elegí revisión humana en vez de suposiciones silenciosas

El giro

Entonces las cifras dejaron de cuadrar.

Meses después, una auditoría encontró algo peor que una cifra faltante. Casi la mitad de los registros de gasto venían de extracciones que ya habían sido reemplazadas: reprocesar un documento agregaba cifras nuevas sin retirar las viejas. Rediseñé la publicación para que las cifras de un documento cambien en un solo paso, y escribí una reparación que corrigió el historial sin destruir nada.

47,5%de los registros contados dos veces
1×cada dólar, después de la corrección

El resultado

Dónde terminó.

~USD 77 Men publicidad política en TV, rastreados y consultables
52.768registros de gasto, cada uno vinculado a su documento fuente
98,8%de 1.664 documentos leídos sin fallas
≥80%de precisión de extracción durante dos semanas, frente a una meta de 75%

En producción contra la API real de la FCC, y construido para que la próxima elección o mercado sea configuración, no una reconstrucción.

Para el lector técnico

Notas de ingeniería

Consistencia sin transacciones

La API de Supabase no ofrece transacciones del lado del cliente, así que cada escritura de varios pasos vive en funciones de Postgres que bloquean la fila y revalidan sus condiciones dentro del bloqueo. Cuando dos revisores compiten, el que pierde no escribe nada. El borrado de entidades usa FOR UPDATE NOWAIT para ceder ante una inserción concurrente de alias en vez de competir con ella.

Las pruebas como pipeline

1.492 pruebas en Python con 92% de cobertura y 1.025 pruebas de frontend, aplicadas como compuerta antes de cada push. El nivel de integración pasó de 55–66 minutos a unos 22 segundos sin borrar una sola prueba, reemplazando TRUNCATE CASCADE por prueba con un borrado de hijos primero.

Seguridad desde cero

Autenticación por usuario con hashing argon2id, tokens de sesión hasheados, cookies __Host- y protección CSRF. En el camino encontré y cerré el acceso abierto a las tablas (la seguridad a nivel de fila estaba apagada) y un secreto que se estaba metiendo en cada imagen de Docker.

La consola de administración como prueba de contrato

La consola interna en React solo habla con la API pública, nunca con la base de datos, con tipos generados desde el esquema OpenAPI en vivo. Si el frontend y el backend se desalinean, el build falla.

Polling en vez de push

La consola lanza una ejecución del pipeline y consulta una tabla de ejecuciones con un intervalo acotado. El backend no tiene nada que transmitir, así que un canal push solo habría sido polling con más formas de fallar.

  • Python
  • FastAPI
  • Supabase
  • PostgreSQL
  • Pydantic
  • OpenRouter
  • Claude
  • Gemini
  • React
  • TypeScript
  • TanStack Query
  • Recharts
  • Docker
  • Render

Siguiente proyecto · IA de voz y evals · 2026

Un coach de voz que se califica a sí mismo

¿Tienes una pila de documentos que nadie logra sumar?