Come ho costruito RFP Search, un aggregatore di RFP potenziato dall'IA
Se avete mai cercato bandi di gara (RFP) governativi per servizi digitali, conoscete bene la frustrazione. Le opportunità sono sparse su SAM.gov, portali di appalti statali, Grants.gov, l'ONU, l'UE e una dozzina di altri posti. Ogni sito ha la sua interfaccia di ricerca, il suo formato e le sue stranezze. Controllarli tutti manualmente è tedioso, così ho costruito RFP Search per farlo al posto mio.
Cosa fa
RFP Search esegue lo scraping di 11 fonti di appalti governative e no-profit ogni notte, elabora ogni opportunità tramite un modello di AI per estrarre dati strutturati e presenta tutto attraverso un'unica interfaccia ricercabile con filtri e una mappa interattiva.
Le fonti includono SAM.gov, Grants.gov, il Texas ESBD, il Cal eProcure della California, il Florida MFMP, il portale TED dell'UE, l'UN Global Marketplace, NYC Open Data, il Federal Register, USAspending e Brave Search per una copertura più ampia. Ogni mattina, i risultati aggiornati sono pronti.
L'architettura
L'intero sistema funziona su Cloudflare. Tre Worker, un database D1 e zero server tradizionali. L'ho strutturato come un monorepo con Turborepo e npm workspaces.
I tre Worker hanno ciascuno un compito ben definito.
- rfp-web è il frontend, costruito con React Router v7 (il successore di Remix). Gestisce l'interfaccia utente di ricerca, i filtri e una mappa interattiva Leaflet con clustering dei marker.
- rfp-api è un'API REST Hono che gestisce le query di ricerca, la paginazione, le statistiche e i dati di geolocalizzazione per la mappa.
- rfp-scraper è un Worker pianificato che esegue processi cron notturni per recuperare ed elaborare gli RFP da tutte le fonti.
Il Worker web comunica con il Worker API tramite Cloudflare Service Bindings. Niente HTTP, niente CORS, nessun endpoint API pubblico. Il binding chiama l'API direttamente all'interno della rete di Cloudflare, il che è più veloce e più sicuro.
Lo scraper
Questa è la parte più interessante. Ogni fonte è un plugin che implementa una semplice interfaccia. Volete aggiungere una nuova fonte? Create un file, implementate l'interfaccia, registratela e aggiungete una riga al database. Tutto qui.
Lo scraper viene eseguito in tre batch sfalsati (9:00, 9:15 e 9:30 UTC) per rimanere entro i limiti di tempo della CPU dei Worker. Ogni batch elabora un sottoinsieme di fonti.
Batch 1 (9:00 UTC) SAM.gov, TED EU, USAspending
Batch 2 (9:15 UTC) Texas ESBD, Cal eProcure, Brave Search, Grants.gov, Federal Register
Batch 3 (9:30 UTC) Florida MFMP, UNGM, NYC Open DataOgni plugin di fonte sa come recuperare e analizzare il proprio formato dati. Alcuni accedono ad API REST, altri fanno scraping di HTML, altri ancora usano feed RSS. L'architettura a plugin mantiene questa complessità contenuta. Se una fonte cambia il suo formato, devo aggiornare solo un file.
Lo scraper tiene traccia anche dei fallimenti. Se una fonte fallisce tre volte di seguito, viene automaticamente disattivata in modo da non sprecare cicli o inquinare i log.
Estrazione tramite AI
Gli elenchi grezzi degli RFP sono disordinati. Alcuni hanno descrizioni dettagliate, altri hanno solo un titolo e un link. Utilizzo Cloudflare Workers AI con Llama 3.1 70B Instruct per normalizzare tutto in una struttura coerente.
Ogni RFP riceve una singola chiamata AI che estrae campi strutturati (scadenza, budget, località, informazioni di contatto), metadati decisionali (certificazioni richieste, tipo di contratto, possibilità di lavoro da remoto, dimensione stimata del team, stack tecnologico), categorie (sviluppo web, CMS, cloud, AI, migrazione e altro) e un riassunto di 2-3 frasi.
Una chiamata per RFP, un prompt che fa tutto. Questo mantiene bassi i costi di Workers AI pur ottenendo una buona qualità di estrazione. Il modello è sorprendentemente capace nell'estrarre dati strutturati dal testo non strutturato degli appalti governativi.
Il database
Cloudflare D1 (SQLite gestito all'edge) memorizza tutto. La tabella principale rfps ha oltre 30 campi che coprono i dati principali, i campi estratti dall'AI e i metadati decisionali.
Per la ricerca, sto utilizzando FTS5 (Full-Text Search 5) di SQLite con trigger che sincronizzano automaticamente l'indice di ricerca ogni volta che una riga viene inserita, aggiornata o eliminata. Nessun reindicizzazione manuale, nessun servizio di ricerca separato. Semplicemente funziona.
CREATE VIRTUAL TABLE rfps_fts USING fts5(
title, description, ai_summary, categories,
content=rfps, content_rowid=id
);La deduplicazione utilizza un vincolo univoco su (source_name, external_id), quindi se lo stesso RFP appare in più esecuzioni di scraping, viene aggiornato anziché duplicato.
Lo stato dell'RFP (aperto, in scadenza, chiuso) viene calcolato al momento della query a partire dal campo della scadenza anziché essere memorizzato. Ciò significa che lo stato è sempre accurato senza la necessità di un processo in background per aggiornare i record obsoleti.
Il frontend
L'interfaccia utente è costruita con React Router v7 su Cloudflare Workers, stilizzata con Tailwind CSS v4. Dispone di una barra di ricerca con ricerca full-text su titoli, descrizioni, riepiloghi e categorie. I filtri a discesa consentono di restringere la ricerca per tipo di organizzazione, categoria, stato della scadenza e valore stimato.
La mappa interattiva utilizza Leaflet con clustering dei marker. Ogni RFP con dati di geolocalizzazione viene visualizzato come un segnaposto. La geolocalizzazione vera e propria avviene nello scraper utilizzando Nominatim (il servizio di geolocalizzazione di OpenStreetMap) per convertire le stringhe di località in coppie latitudine/longitudine.
Una barra delle statistiche in alto mostra il conteggio degli RFP attualmente aperti e delle fonti attive, in modo da poter capire a colpo d'occhio quanto sono aggiornati i dati.
Cosa farei diversamente
Se dovessi ricominciare da capo, probabilmente aggiungerei gli avvisi via email fin dall'inizio. Al momento è necessario visitare il sito per controllare le nuove opportunità. Un semplice riepilogo giornaliero per le ricerche salvate lo renderebbe molto più utile.
Investirei anche di più nei test di affidabilità delle fonti. I siti web governativi cambiano senza preavviso e lo scraping è intrinsecamente fragile. Un monitoraggio migliore e avvisi automatici quando una fonte inizia a restituire dati inattesi farebbero risparmiare tempo di debug.
Provalo
La piattaforma è attiva su rfp.davidloor.com. È focalizzata su opportunità relative a servizi digitali, sviluppo web, CMS, cloud e AI, ma l'architettura potrebbe supportare qualsiasi categoria di RFP.