Cómo construí RFP Search, un agregador de RFP impulsado por IA
Si alguna vez has intentado buscar licitaciones públicas (RFP) gubernamentales para servicios digitales, conoces el dolor. Las oportunidades están dispersas en SAM.gov, portales de contratación estatales, Grants.gov, la ONU, la UE y una docena de lugares más. Cada sitio tiene su propia interfaz de búsqueda, su propio formato y sus propias peculiaridades. Revisarlos todos manualmente es tedioso, así que construí RFP Search para que lo hiciera por mí.
Qué hace
RFP Search rastrea 11 fuentes de contratación gubernamentales y sin fines de lucro cada noche, procesa cada oportunidad a través de un modelo de IA para extraer datos estructurados y sirve todo a través de una única interfaz de búsqueda con filtros y un mapa interactivo.
Las fuentes incluyen SAM.gov, Grants.gov, el ESBD de Texas, Cal eProcure de California, MFMP de Florida, el portal TED de la UE, el Mercado Global de la ONU, NYC Open Data, el Registro Federal, USAspending y Brave Search para una cobertura más amplia. Cada mañana, los resultados frescos están esperando.
La arquitectura
Todo funciona con Cloudflare. Tres Workers, una base de datos D1 y cero servidores tradicionales. Lo estructuré como un monorepo con Turborepo y espacios de trabajo de npm.
Los tres Workers tienen una tarea clara.
- rfp-web es el frontend, construido con React Router v7 (el sucesor de Remix). Maneja la interfaz de usuario de búsqueda, los filtros y un mapa interactivo de Leaflet con agrupación de marcadores.
- rfp-api es una API REST Hono que maneja las consultas de búsqueda, la paginación, las estadísticas y los datos de geolocalización para el mapa.
- rfp-scraper es un Worker programado que ejecuta trabajos cron nocturnos para obtener y procesar las RFP de todas las fuentes.
El Worker web habla con el Worker de la API a través de Cloudflare Service Bindings. Sin HTTP, sin CORS, sin punto final de API público. La vinculación llama a la API directamente dentro de la red de Cloudflare, lo que es más rápido y seguro.
El scraper
Esta es la parte más interesante. Cada fuente es un plugin que implementa una interfaz simple. ¿Quieres añadir una nueva fuente? Crea un archivo, implementa la interfaz, regístrala y añade una fila a la base de datos. Eso es todo.
El scraper se ejecuta en tres lotes escalonados (9:00, 9:15 y 9:30 AM UTC) para mantenerse dentro de los límites de tiempo de CPU del Worker. Cada lote procesa un subconjunto de fuentes.
Lote 1 (9:00 UTC) SAM.gov, TED UE, USAspending
Lote 2 (9:15 UTC) Texas ESBD, Cal eProcure, Brave Search, Grants.gov, Registro Federal
Lote 3 (9:30 UTC) Florida MFMP, UNGM, NYC Open DataCada plugin de fuente sabe cómo obtener y analizar su propio formato de datos. Algunos acceden a API REST, otros rastrean HTML, algunos usan feeds RSS. La arquitectura de plugins mantiene esta complejidad contenida. Si una fuente cambia su formato, solo necesito actualizar un archivo.
El scraper también rastrea fallos. Si una fuente falla tres veces seguidas, se desactiva automáticamente para no desperdiciar ciclos ni contaminar los registros.
Extracción con IA
Los listados brutos de RFP son confusos. Algunos tienen descripciones detalladas, otros solo tienen un título y un enlace. Utilizo Cloudflare Workers AI con Llama 3.1 70B Instruct para normalizar todo en una estructura consistente.
A cada RFP se le realiza una única llamada a la IA que extrae campos estructurados (fecha límite, presupuesto, ubicación, información de contacto), metadatos de toma de decisiones (certificaciones requeridas, tipo de contrato, facilidad de trabajo remoto, tamaño estimado del equipo, pila tecnológica), categorías (desarrollo web, CMS, nube, IA, migración y más) y un resumen de 2 a 3 frases.
Una llamada por RFP, una instrucción que lo hace todo. Esto mantiene bajos los costos de Workers AI y al mismo tiempo logra una buena calidad de extracción. El modelo es sorprendentemente capaz de extraer datos estructurados del texto no estructurado de la contratación pública.
La base de datos
Cloudflare D1 (SQLite administrado en el borde) almacena todo. La tabla principal rfps tiene más de 30 campos que cubren los datos principales, los campos extraídos por IA y los metadatos de toma de decisiones.
Para la búsqueda, estoy utilizando FTS5 (Full-Text Search 5) de SQLite con disparadores que sincronizan automáticamente el índice de búsqueda cada vez que se inserta, actualiza o elimina una fila. Sin reindexación manual, sin servicio de búsqueda separado. Simplemente funciona.
CREATE VIRTUAL TABLE rfps_fts USING fts5(
title, description, ai_summary, categories,
content=rfps, content_rowid=id
);La deduplicación utiliza una restricción única en (source_name, external_id), por lo que si la misma RFP aparece en varias ejecuciones de rastreo, se actualiza en lugar de duplicarse.
El estado de la RFP (abierta, próxima a vencer, cerrada) se calcula en el momento de la consulta a partir del campo de fecha límite en lugar de almacenarse. Esto significa que el estado siempre es preciso sin necesidad de un trabajo en segundo plano para actualizar registros obsoletos.
El frontend
La interfaz de usuario está construida con React Router v7 en Cloudflare Workers, estilizada con Tailwind CSS v4. Tiene una barra de búsqueda con búsqueda de texto completo en títulos, descripciones, resúmenes y categorías. Los filtros desplegables le permiten acotar por tipo de organización, categoría, estado de la fecha límite y valor estimado.
El mapa interactivo utiliza Leaflet con agrupación de marcadores. Cada RFP con datos de geocodificación de ubicación aparece como un pin. La geocodificación en sí se realiza en el scraper utilizando Nominatim (el servicio de geocodificación de OpenStreetMap) para convertir cadenas de ubicación en pares de latitud/longitud.
Una barra de estadísticas en la parte superior muestra el recuento de RFP abiertas actualmente y las fuentes activas, para que pueda ver de un vistazo qué tan frescos están los datos.
Lo que haría diferente
Si tuviera que empezar de nuevo, probablemente añadiría alertas por correo electrónico desde el primer día. Ahora tienes que visitar el sitio para comprobar si hay nuevas oportunidades. Un simple resumen diario para las búsquedas guardadas lo haría mucho más útil.
También invertiría más en pruebas de fiabilidad de las fuentes. Los sitios web gubernamentales cambian sin previo aviso, y rastrearlos es inherentemente frágil. Una mejor monitorización y alertas automáticas cuando una fuente comienza a devolver datos inesperados ahorrarían tiempo de depuración.
Pruébalo
La plataforma está activa en rfp.davidloor.com. Se centra en oportunidades de servicios digitales, desarrollo web, CMS, nube e IA, pero la arquitectura podría soportar cualquier categoría de RFP.