Applied AI / Research Automation
Unattended LLM market research across 11,500 cities, every night
AnnaTech reference build - self-funded, running nightly in our own production environment
- cities in scope - every city above 50,000 inhabitants
- 11,543
- cities priced unattended (69.7%), 5,849 with rent data
- 8,044
- cities researched per night on one self-hosted model
- 80-140
- independent sources required before a >40% price move is accepted
- 2
Architecture at a glance
Context
Every business that depends on market intelligence faces the same trade-off: buy a data feed you cannot audit, or pay analysts to research by hand. We built this to test a third option end to end - a language model doing the research, inside engineering that makes its output trustworthy enough to act on.
The subject, residential prices for every city above 50,000 inhabitants with four price points each and a buy-or-rent view, was chosen because it is large, multilingual, inconsistent and public. Nothing about it is easier than a real client problem.
Constraint
Three rules were set on day one. The model runs on our own hardware; frontier cloud models are used only in bounded, separately logged sessions for the markets the local model cannot crack, so the cost tier is a choice per city rather than a default. The model is never trusted: it does not browse on its own, never converts currency, never answers from memory - it reads the pages the pipeline hands it and returns structured JSON that must pass a schema. And nobody is on call: the pipeline runs every night and has to recover from its own failures.
Architecture
A scheduler works one continent per night from a staleness-ordered queue - never-priced cities first, then the oldest data. For each city the pipeline searches through a self-hosted metasearch instance, triages the results, fetches pages through a three-stage fallback (plain fetch, anti-bot solver, headed browser), and asks the local model to extract prices into a fixed schema. Currency conversion is deterministic, from central-bank rates with a seven-day staleness cap.
A validation layer then applies per-continent sanity bounds, requires a gross rental yield between 1.5 and 15 percent, refuses a move of more than 40 percent unless confidence is at least 0.8 and two sources agree, and never lets a failure or an empty field overwrite stored data. When every stronger method has failed, a last-resort step mines dated statements from local forums, restricted to a three-month window, requiring two concordant figures, and capped at low confidence.
Operations are self-healing. Search outages pause the run and rotate the network exit automatically; a hard 600-second deadline and a watchdog catch workers wedged on a half-dead response; a Sunday reconciliation pass re-checks failures and outliers. The public site is fully static and makes no external requests at all - even the map tiles are self-hosted.
Outcome
8,044 of 11,543 cities priced, 5,849 with rent data, and a globe that answers in one click with the date and confidence of every figure shown. More useful to a client than the map is the incident log: a 4.5-hour hang caused by a response that never finished, a network rotation that created the outage it was meant to fix, a circuit breaker that could never trip under parallel workers - each turned into a guard that now runs every night.
That is the discipline we bring when a language model has to produce numbers your business will act on: the model does the reading, the rules decide what gets published.
From the workbench
00:30:04 INFO scheduler continent=EU · 138 cities · never-priced first, then stalest 00:31:12 TRACE city 2950159 search 14 results · triage 5 pages · fetched (plain) 00:31:58 TRACE city 2950159 llm extract ok · 4 metrics · confidence 0.82 · 2 sources 00:31:58 INFO validate yield 4.1% in 1.5-15% · move +6% · stored 00:33:40 WARN city 3094802 move +47% · conf 0.61 · 1 source -> rejected, old value kept 00:41:07 WARN search 5 failures in 10 min -> pause_for_search · rotate exit 00:52:19 INFO search probe 12 results -> resume 01:14:33 TRACE step=yield_assistant public chat active -> hold model calls 180 s -------- no failure overwrites a price · no null erases a value · Sunday re-checks failures and outliers
Illustrative trace. Step names, guardrail thresholds and the self-heal chain are exactly as delivered.
More work
Related case studies
Talk to the person who will actually build it
One architect, end to end: scoping, architecture, delivery, operations. Write a paragraph about your problem and you will get an engineering answer, not a sales call.