Project overview: Classifieds
A self-developed tool living on my local network automating both the buy- and sell-side of second-hand marketplaces. It manages my own listings and watches for good deals using a locally hosted AI-stack.
I set out to automate only the selling side. Before that part was finished it became clear the buying side had just as much or more to gain, especially with an LLM doing the reading. The two ended up needing very different machinery.
Selling: maintaining an ad in one place
Posting by hand is tedious: find the right category, work through several screens, repeat per marketplace.
Here a listing is entered once in a single form: title, description, category, images, pricing, and the marketplaces to target. From there the automation takes over, with Symfony's Messenger and Scheduler components dispatching the jobs after a create, after an edit, or on a schedule.
Posting itself would ideally go through an API, but the first marketplace I implemented has no write API, so it runs on browser automation through Playwright. That is the fragile part of the project, and two things keep it manageable and safe:
- The listing creation form is pinned by a committed HTML fixture, so when the site changes its markup and posting fails, there is one file of selectors to fix rather than a mystery to debug in production.
- The browser session is captured by hand, never created automatically. No unattended login, no CAPTCHA handling.
Buying: searching wide and filtering assisted by AI
Listings are often written poorly: generic title, vague description, absent model numbers, wrong category.
Searching by specific keywords then makes deals easy to miss and searching wide means finding a needle in a haystack.
However, finding a needle in a haystack can be automated using AI.
A wide search is performed first and the large result set is retrieved and saved as 'observations'. Luckily, the
marketplace I implemented first has a publicly available JSON API for reading.
Each observation is then sent to a local LLM that analyses the text and returns a score from 0 to 100 based on a
natural-language description of what is wanted.
Listings that land in the unsure band, between the reject and the match threshold, get a second pass with vision on
the images. That needs no configuration of its own: 'unsure' is simply what falls between the two thresholds.
The model only ever returns a score and a sentence of reasoning. The verdict itself is a plain method on the saved search, driven by the thresholds configured on it. That makes how high the bar sits my decision and not the model's. Because every score is persisted, moving a threshold re-decides every listing already seen without a single new LLM call.
Scheduler and Messenger drive this side too: the search runs on a schedule and each new or updated result goes to the LocalAI server through the queue, one call per listing. Those calls take a few seconds each, so the queue keeps them out of the way of everything else, and it retries automatically if the model spirals out on a call and times out.
Notable
- SQLite runs in WAL mode, because the scheduler, the workers and the web UI all touch the same file and the default rollback journal turns that into "database is locked".
- Foreign keys are enforced at runtime, but disabled for schema changes. SQLite cannot alter a column in place, so DBAL's copy-drop-recreate would otherwise fire every cascade and silently take related tables with it.
- Search queries are authored by hand, not generated from the intent description, which keeps the base result set consistent between runs. The LLM should be analysing a stable result set, not one a generated query reshapes each time.
- Caveats are kept out of the score. Damage, missing parts and heavy wear are reported separately, so a perfect but broken match still surfaces and I get to decide whether the caveat is worth it.
- It stays at personal scale, since it polls a public endpoint: a page cap per scan, a delay between requests, and an identifying User-Agent.
- Only one marketplace is implemented, though the tool was designed and set up to support several.
FACTS
status
Actively developed
stack
PHP · Symfony · Doctrine · SQLite · Playwright · Node.js · Docker · LocalAI · llama.cpp
Interested in something similar?