Skip to content
Annatech_

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

ONE CONTINENT PER NIGHT - FIND, READ, EXTRACT, VALIDATE, PUBLISH Night queue 11,543 citiesnever-priced 1st Find pages own metasearch3-stage fallback Local LLM JSON schema outnever browses Validation yield 1.5-15%2 sources >40% Publish atomic exportno ext. requests city pages JSON ok rejected or failed -> re-queued · Sunday pass re-checks failures and outliers self-heal: search outage -> pause + exit rotation · wedged worker -> 600 s deadline + watchdog a failure never overwrites a stored price · a null never erases a stored value · FX is deterministic (ECB)

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

nightly run — continent EU · worker 2 of 3
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.