DETERMINISTIC SCRAPER · LANGGRAPH · $0 API

Knowing when NOT to use an LLM.

Independent hotels live and die by their price against the competition, but Booking doesn't give you an easy way to monitor a rival's live rate for a given date and group. Agente-Book automates that audit: it returns the best price for the requested number of guests (not the misleading "from $X"), every option, the status, and a timestamped screenshot. It started as an LLM browser-agent and got re-engineered into a deterministic scraper.

~10 s/query
vs ~10 min for the original LLM-agent version
~60× faster
deterministic, 100% reproducible
0 VRAM
Playwright + direct CSS, no API keys or GPU
$0 cost
no cost per query, runs entirely local
Key features

The correct price, not a misleading headline.

CAPABILITY

Price by group size

Returns the correct price for the requested group — not the global minimum that sleeps fewer people. Counts occupancy icons = room capacity and keeps the rows where capacity ≥ guests.

STATUS

Deterministic detection

AVAILABLE, OCCUPIED, CAPTCHA, ERROR with explicit logic, not a model guessing.

EVIDENCE

Visual proof + data

Full-page screenshot with timestamp per run, and structured Pydantic output with all_options and matched_options.

Detection logic

A decision tree, not a hunch.

nodes/booking_scraper.py
navigate(url, dates, guests)
if captcha_detected:     
    return "CAPTCHA"
if no_availability:     
    return "OCCUPIED"

rows    = parse("#hprt-table")
capable = [f for f in rows if f.capacity >= guests]
price   = min(capable, key=lambda f: f.value)
return "AVAILABLE", price, screenshot  

The orchestration is a LangGraph StateGraph. Today it's a single node, but the graph is the architecture: alerts, multi-hotel comparison, and price history come in as new nodes without touching the existing one.

Playwright 1.48+LangGraph 0.4+Pydantic 2.xCSS + regexPython 3.11+ async
Engineering log

The decision to drop the LLM.

The most useful part of the project: an LLM-agent was the wrong tool for a structured, repeatable extraction.

VersionChangeResult
v1.0browser-use + ChatOllama (gemma3:27b)OOM on a single 24 GB GPU (27b + vision > 24 GB VRAM)
v1.1Switched to gemma3:12bWorked, but ~10 min/query and prone to looping
v2.0Direct Playwright, no LLM~10s/query, deterministic, 0 VRAM
v2.1Capacity filter + full-page screenshotCorrect price per group, complete evidence

The trade-off, stated plainly

Dropping the LLM bought speed, determinism and $0 a query. It also bought coupling. The scraper reads Booking's DOM through fixed selectors — #hprt-table, occupancy cells, price text. The day Booking renames a class, the old LLM agent would have squinted at the page and adapted. This one returns ERROR. That is the honest shape of the trade: this is a scraper for a site you actually watch, not one you leave alone for a year.

What the design does buy is the property that matters when it breaks: it fails loudly. An unexpected DOM produces status="ERROR" carrying the exception detail, and the browser is closed in a finally no matter what failed — never a plausible wrong number reported quietly. For a price you are going to act on, no answer beats a confident bad one.