Tasador
A property valuation report in one or two minutes, with every price justified.
For a real estate agency in Buenos AiresTested with real data, code published

- per valuation report, against an hour or more of manual searching
- 1 to 2 min
- agent steps in LangGraph, from the address to the PDF
- 11
- nDCG@25 for lexical search, against 0.747 for the baseline, on 113 queries
- 0.821
- of made-up figures get past the text check (down from 50.5%)
- 4.3%
- backend tests and Playwright end-to-end tests, in CI
- 492 + 55




The problem
To decide what price to list a property at, the real estate agent compares it by hand against similar listings: they search the portals, note them in a spreadsheet and adjust by eye. It is skilled, repetitive work, and two valuations of the same property can end up at different prices with nothing written down about why.
The owner hears a number verbally, with no document to back it. For a real estate agency in Buenos Aires, that number decides whether it wins the listing.
What I built
A system that puts together the valuation report in one or two minutes, with the comparable listings that justify the price. The agent enters the address and whatever details they have, the screen shows the progress step by step, and at the end there is a report on the web and as a PDF with the price, the range, the confidence level and the table of comparables: the ones used and the ones discarded, each with its reason.
The rule that orders everything: the AI handles language and judgment; math handles the numbers.
- Search. Over a corpus of 93,544 listings, a lexical search in Spanish brings up the active candidates in the neighborhood that most resemble the property.
- Reading. An 11-step agent in LangGraph reads each listing, extracts what moves the price (condition, orientation, floor, elevator, age), detects the same property listed several times and discards whatever does not compare, such as off-plan units or swaps.
- Verification. Every fact the AI takes from a listing comes with its exact quote, and a function with no AI checks that the quote is in the original listing.
- Price. It is calculated by an explicit formula, never by the model: a robust median of USD/m² with written adjustment coefficients. With fewer than 5 valid comparables, the system says it does not know.
- Writing. A model explains the result in plain language, and a check reviews the text before it is delivered.
On top of the finished report there is a second RAG, “Ask the report”: the owner asks why a listing was discarded and gets an answer with verified citations, or a refusal if there is no evidence.
Around it there is a full application: a Next.js 15 frontend with React 19, sessions and roles, user management, a corpus explorer with each listing next to what the system extracted, and the health of each data source. Each report costs between 2 and 5 US cents, with the cost traced per step.
The hardest part
Measuring the RAG before switching it on, and having the simplest option win. I built the full RAG: listing chunking, local embeddings in pgvector, lexical search, fusion with Reciprocal Rank Fusion and a reranker. Before measuring, I wrote down the criteria for switching each piece on, and then I built the yardstick: 113 queries, with nDCG@25, recall, MRR and bpref, and paired bootstrap intervals against the baseline.
Lexical search on its own won: nDCG@25 of 0.821 against 0.747 for the baseline, ahead of the hybrids. It is the one switched on. The dense embeddings and the reranker are built, measured and switched off, one configuration setting away.
Making sure no figure in the text strays from the data. The model writes, and before delivery a two-phase check reviews what it wrote. The first phase uses no AI: it extracts every number from the prose and looks for it in the report’s data, and a single figure that does not appear rejects the text. The second is an adversarial model that looks for unsupported claims. If the text is rejected three times, the report goes out with the table and the range, which come from the formula, and without prose.
I audited it by injecting made-up prices into real reports: at first 50.5% got through; after three measured fixes, 4.3%.
How it is tested
- 492 backend tests with pytest, including architecture tests that prevent assigning a model to the step that calculates the price or loosening the minimum number of comparables.
- 55 end-to-end tests with Playwright against the real stack, with no mocks, on desktop and on mobile. They check product decisions: that a report without enough data does not read as an error, that every field has its label, that contrast meets AA in both themes.
- CI on GitHub Actions on every change: lint, types, migrations from scratch, isolation between organizations, secret scanning and scanning of the Docker images.
- Real market data: the system runs on the real corpus of 93,544 Buenos Aires listings. The repository also includes a synthetic corpus to bring up the whole system with Docker Compose from an empty database.
How it works
- 01Valuation requestaddress and details
- 02Listings corpus93,544 listings
- 02Lexical searchsimilar listings
- 03The agent readspicks comparables
- 04Checks each citationchecked without AI
- 05Price formulamedian USD/m²
- 06Writingthe model explains
- 07Figure checkevery figure in data
- 08Report and PDFwith comparables
- 09Ask the reportverified citations
Results
A formula calculates the price
A robust median of USD/m² with written adjustment coefficients. An architecture test prevents assigning a model to that step.
Every fact, with its citation
Whatever the AI extracts from a listing comes with the exact passage that backs it, and a function with no AI checks that it is in the original.
It knows when to say it does not know
With fewer than 5 valid comparables it gives no price and shows why each candidate was dropped.
Ask the report
A second RAG answers with verified citations, or declines in milliseconds if there is no evidence.
2 to 5 US cents per report
The cost is traced per step and per model call.
93,544 listings in the corpus
Idempotent ingestion by hash, cascading deduplication and the same property counted only once.
Gallery
Stack
- Application
- Next.js 15React 19TypeScriptPython 3.12FastAPIPydantic v2
- AI and search
- LangGraphLiteLLMLexical search in PostgreSQLpgvectorfastembedReciprocal Rank Fusion
- Data
- PostgreSQL 16RedisSQLAlchemy 2Alembicpg_trgm
- Quality and deployment
- pytestPlaywrightmypyGitHub ActionsDocker Compose



