Project overzicht: Zoekertjes-tool

Een zelfgebouwde tool op mijn lokale netwerk die zowel de koop- als de verkoopkant van tweedehands marktplaatsen automatiseert. Hij beheert mijn eigen zoekertjes en speurt naar goede deals met een lokaal gehoste AI-stack.

Ik begon met het idee om enkel de verkoopkant te automatiseren. Nog voor dat deel af was, werd duidelijk dat de koopkant er evenveel of meer bij te winnen had, zeker met een LLM die het leeswerk doet. Uiteindelijk bleek elke kant een eigen aanpak nodig te hebben.

Verkopen: een zoekertje op één plek beheren

Manueel zoekertjes plaatsen is omslachtig: de juiste categorie zoeken, door meerdere schermen klikken, en dat voor elke marktplaats opnieuw.

Hier wordt een zoekertje één keer ingevoerd in één formulier: titel, beschrijving, categorie, foto's, prijs en de marktplaatsen waar het terecht moet komen. Vanaf daar neemt de automatisering het over: Symfony's Messenger- en Scheduler-componenten vuren de juiste jobs af na het aanmaken, na het aanpassen of volgens een schema.

Het plaatsen zelf zou idealiter via een API gebeuren maar de eerste marktplaats die ik implementeerde, heeft geen schrijf-API dus loopt het via browser-automatisering met Playwright. Dat is het fragiele deel van het project, en twee dingen houden het beheersbaar en veilig:

  • Het formulier om een zoekertje aan te maken ligt vast in een gecommitte HTML-fixture. Verandert de site zijn markup en breekt dat het plaatsen, dan is er één bestand met selectors om aan te passen in plaats van een mysterie om in productie te debuggen.
  • De browsersessie wordt met de hand opgezet, nooit automatisch aangemaakt. Geen onbewaakte login, geen CAPTCHA's om langs te geraken.

Kopen: breed zoeken en filteren met hulp van AI

Zoekertjes zijn vaak slecht geschreven: een generieke titel, een vage beschrijving, ontbrekende modelnummers, de verkeerde categorie.
Zoeken op specifieke trefwoorden zorgt er dan voor dat je goede deals mist, en breed zoeken is een naald in een hooiberg zoeken.
Een naald in een hooiberg zoeken kan je echter automatiseren met AI.

Eerst wordt er opzettelijk breed gezocht, waarna het grote aantal resultaten wordt opgeslagen als 'observaties'. Gelukkig heeft de marktplaats die ik als eerste implementeerde een publiek beschikbare JSON-API om te zoeken.
Elke observatie wordt vervolgens naar een lokaal LLM-model op mijn LocalAI-server gestuurd die de tekst analyseert en een score van 0 tot 100 teruggeeft, op basis van een omschrijving in gewone taal van wat ik zoek.
Advertenties die in de twijfelzone belanden, dus boven de afkeuring- en onder de acceptatiedrempel, krijgen een tweede ronde met 'vision' op de foto's. Daar is geen aparte configuratie voor nodig: 'twijfel' is automatisch wat tussen de twee drempels valt.

Het model geeft enkel de score en een zin uitleg terug. Het oordeel zelf is een gewone functie in code op de opgeslagen zoekopdracht, gedreven door de drempels die erop ingesteld staan. Daardoor is het mijn beslissing hoe hoog de lat ligt, en niet die van het model. Omdat elke score bewaard blijft, verandert het verschuiven van een drempel elke beoordeling zoekertje die al gezien is, zonder ook maar één nieuwe LLM-call.

Scheduler en Messenger zijn ook aan deze kant de drijvende kracht: de zoekopdracht loopt op een schema en elk nieuw of gewijzigd resultaat gaat via de queue naar de LocalAI-server, één call per zoekertje. Die calls duren elk een paar seconden, dus houdt de queue ze uit de weg van al de rest, met automatische retries als het model op een call ontspoort en in een time-out loopt.

Uitgelicht

  • SQLite draait in WAL-modus, omdat de scheduler, de workers en de web-UI allemaal hetzelfde bestand aanspreken en de standaard rollback journal daar "database is locked" van maakt.
  • Foreign keys worden tijdens runtime afgedwongen, maar uitgeschakeld bij schemawijzigingen. SQLite kan een kolom niet ter plaatse aanpassen, waardoor DBAL's copy-drop-recreate anders elke cascade zou afvuren en stilletjes de gerelateerde tabellen mee zou nemen.
  • Zoekopdrachten worden met de hand geschreven, niet gegenereerd op basis van de omschrijving van wat ik zoek. Zo blijveb de zoekresultaten consistent tussen runs: de LLM hoort een stabiele set te analyseren, geen set die varieert naargelang de gegenereerde query.
  • Aandachtspunten tellen niet mee in de score. Schade, ontbrekende onderdelen en zware slijtage worden apart gerapporteerd, zodat een niet functionele, maar anderzijds perfecte match toch bovenkomt en ik zelf beslis of het probleem het waard is.
  • Het blijft op persoonlijke schaal, want het bevraagt een publiek endpoint: een maximum aantal pagina's per scan, een vertraging tussen requests en een identificeerbare User-Agent.
  • Er is maar één marktplaats geïmplementeerd, hoewel de tool ontworpen en opgezet is om er meerdere te ondersteunen.

Details

status

Actief in ontwikkeling

stack

PHP · Symfony · Doctrine · SQLite · Playwright · Node.js · Docker · LocalAI · llama.cpp

Photo of Jeroen

Interesse in iets gelijkaardigs?

[email protected] +32 478 47 61 96
beschikbaar vanaf januari 2027