# Tasador: A property valuation report in one or two minutes, with every price justified.

> The AI finds and reads comparable listings; the price comes from an explicit formula, never from the model. A full application, tested with real data and with its code published.

Industry: For a real estate agency in Buenos Aires · Status: Tested with real data, code published · Developed by Ricardo Brossard
URL: https://ricardobrossard.com/en/projects/tasador/

- github.com/ricardobing/tasador-rag: https://github.com/ricardobing/tasador-rag

## By the numbers

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

## 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.

## Stack

- Application: Next.js 15, React 19, TypeScript, Python 3.12, FastAPI, Pydantic v2
- AI and search: LangGraph, LiteLLM, Lexical search in PostgreSQL, pgvector, fastembed, Reciprocal Rank Fusion
- Data: PostgreSQL 16, Redis, SQLAlchemy 2, Alembic, pg_trgm
- Quality and deployment: pytest, Playwright, mypy, GitHub Actions, Docker Compose
