Hoe ik RFP Search bouwde, een AI-aangedreven RFP-aggregator
Als je ooit hebt geprobeerd overheids-RFPs (Request for Proposals) voor digitale diensten te vinden, ken je de pijn. Mogelijkheden zijn verspreid over SAM.gov, inkoopportals van staten, Grants.gov, de VN, de EU en een dozijn andere plaatsen. Elke site heeft zijn eigen zoekinterface, zijn eigen formaat en zijn eigen eigenaardigheden. Ze allemaal handmatig controleren is vervelend, dus bouwde ik RFP Search om het voor mij te doen.
Wat het doet
RFP Search schraapt elke nacht 11 inkoopbronnen van overheden en non-profitorganisaties, voert elke mogelijkheid door een AI-model om gestructureerde gegevens te extraheren, en presenteert alles via één doorzoekbare interface met filters en een interactieve kaart.
De bronnen omvatten SAM.gov, Grants.gov, de Texas ESBD, Californië's Cal eProcure, Florida MFMP, de TED-portal van de EU, de UN Global Marketplace, NYC Open Data, de Federal Register, USAspending, en Brave Search voor bredere dekking. Elke ochtend staan verse resultaten klaar.
De architectuur
Het geheel draait op Cloudflare. Drie Workers, één D1-database en nul traditionele servers. Ik structureerde het als een monorepo met Turborepo en npm workspaces.
De drie Workers hebben elk een duidelijke taak.
- rfp-web is de frontend, gebouwd met React Router v7 (de opvolger van Remix). Het beheert de zoek-UI, filters en een interactieve Leaflet-kaart met marker clustering.
- rfp-api is een Hono REST API die zoekopdrachten, paginering, statistieken en geolocatiegegevens voor de kaart afhandelt.
- rfp-scraper is een geplande Worker die nachtelijke cronjobs uitvoert om RFP's van alle bronnen op te halen en te verwerken.
De web Worker communiceert met de API Worker via Cloudflare Service Bindings. Geen HTTP, geen CORS, geen openbaar API-eindpunt. De binding roept de API rechtstreeks aan binnen het netwerk van Cloudflare, wat zowel sneller als veiliger is.
De scraper
Dit is het meest interessante deel. Elke bron is een plugin die een eenvoudige interface implementeert. Wil je een nieuwe bron toevoegen? Maak een bestand aan, implementeer de interface, registreer deze en voeg een rij toe aan de database. Dat is alles.
De scraper draait in drie gespreide batches (9:00, 9:15 en 9:30 uur UTC) om binnen de CPU-tijdslimieten van de Worker te blijven. Elke batch verwerkt een subset van bronnen.
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 DataElke bronplugin weet hoe hij zijn eigen gegevensformaat moet ophalen en parseren. Sommige maken verbinding met REST API's, sommige schrapen HTML, sommige gebruiken RSS-feeds. De plugin-architectuur houdt deze complexiteit ingeperkt. Als een bron zijn formaat wijzigt, hoef ik slechts één bestand bij te werken.
De scraper houdt ook fouten bij. Als een bron drie keer achter elkaar faalt, wordt deze automatisch gedeactiveerd, zodat deze geen cycli verspilt of de logboeken vervuilt.
AI-extractie
Ruwe RFP-vermeldingen zijn rommelig. Sommige hebben gedetailleerde beschrijvingen, andere hebben alleen een titel en een link. Ik gebruik Cloudflare Workers AI met Llama 3.1 70B Instruct om alles te normaliseren naar een consistente structuur.
Elke RFP krijgt één AI-oproep die gestructureerde velden extraheert (deadline, budget, locatie, contactgegevens), besluitvormingsmetadata (vereiste certificeringen, contracttype, flexibiliteit voor werken op afstand, geschatte teamgrootte, techstack), categorieën (webontwikkeling, CMS, cloud, AI, migratie en meer), en een samenvatting van 2-3 zinnen.
Eén oproep per RFP, één prompt die alles doet. Dit houdt de kosten van Workers AI laag, terwijl toch een goede extractiekwaliteit wordt bereikt. Het model is verrassend bekwaam in het halen van gestructureerde gegevens uit ongestructureerde overheidsaanbestedingsteksten.
De database
Cloudflare D1 (beheerde SQLite aan de rand) slaat alles op. De hoofd rfps tabel heeft meer dan 30 velden die de kerngegevens, AI-geëxtraheerde velden en besluitvormingsmetadata omvatten.
Voor zoeken gebruik ik SQLite's FTS5 (Full-Text Search 5) met triggers die de zoekindex automatisch synchroniseren telkens wanneer een rij wordt ingevoegd, bijgewerkt of verwijderd. Geen handmatige herindexering, geen aparte zoekservice. Het werkt gewoon.
CREATE VIRTUAL TABLE rfps_fts USING fts5(
title, description, ai_summary, categories,
content=rfps, content_rowid=id
);Deduplicatie maakt gebruik van een unieke beperking op (source_name, external_id), zodat als dezelfde RFP in meerdere schraaprondes verschijnt, deze wordt bijgewerkt in plaats van gedupliceerd.
De RFP-status (open, bijna verlopen, gesloten) wordt berekend op het moment van de query op basis van het deadlineveld in plaats van opgeslagen. Dit betekent dat de status altijd nauwkeurig is zonder dat een achtergrondtaak nodig is om verouderde records bij te werken.
De frontend
De UI is gebouwd met React Router v7 op Cloudflare Workers, gestyled met Tailwind CSS v4. Het heeft een zoekbalk met full-text zoeken in titels, beschrijvingen, samenvattingen en categorieën. Via dropdownfilters kun je filteren op organisatietype, categorie, deadline-status en geschatte waarde.
De interactieve kaart gebruikt Leaflet met marker clustering. Elke RFP met geocoderinggegevens verschijnt als een pin. De geocodering zelf gebeurt in de scraper met behulp van Nominatim (de geocoderingsservice van OpenStreetMap) om locatie-strings om te zetten in breedtegraad/lengtegraad-paren.
Een statistiekbalk bovenaan toont het aantal momenteel openstaande RFP's en actieve bronnen, zodat je in één oogopslag kunt zien hoe recent de gegevens zijn.
Wat ik anders zou doen
Als ik opnieuw zou beginnen, zou ik waarschijnlijk vanaf dag één e-mailwaarschuwingen toevoegen. Momenteel moet je de site bezoeken om te controleren op nieuwe mogelijkheden. Een eenvoudige dagelijkse samenvatting voor opgeslagen zoekopdrachten zou het veel nuttiger maken.
Ik zou ook meer investeren in het testen van de betrouwbaarheid van bronnen. Overheidswebsites veranderen zonder waarschuwing, en het scrapen ervan is inherent fragiel. Betere monitoring en automatische waarschuwingen wanneer een bron onverwachte gegevens begint terug te geven, zouden debugtijd besparen.
Probeer het uit
Het platform is live op rfp.davidloor.com. Het is gericht op digitale diensten, webontwikkeling, CMS, cloud en AI-mogelijkheden, maar de architectuur zou elke categorie van RFP's kunnen ondersteunen.