## The problem
We have toured 22 houses and still haven't bought one.
The reason isn't that there's nothing available. It's that the things which
actually rule a house out for us aren't filters on any listing site:
- Which direction the front door faces. That's Vastu, and it isn't negotiable in my family.
- Whether the ground-floor bathroom has a tub, or only a toilet and a sink — my parents will be staying with us.
- Whether the backyard is flat and fenced, or drops away into a drainage problem. We have a toddler.
- Whether the lot backs onto a four-lane road.
None of that is a field in any listing feed. All of it is sitting in the floor
plan and the satellite image. So it isn't a search problem. It's a vision
problem.
## What it does
VastuNest sends Gemini two images per property — the floor plan and the aerial —
and asks one question: what is physically in this drawing? It comes back with the
entrance direction, the kitchen quadrant, whether a main-floor bathroom contains
a tub, the yard grade, and whether the lot abuts a major road. Every claim quotes
the visual evidence it came from.
A TypeScript scoring engine, not the model, turns that into a match score against
a preference profile. Tell it "it fronts a four-lane road, that's a dealbreaker
with a toddler" and it converts that into weight changes, saves them to
Firestore, and re-ranks everything.
It also runs without being asked. Cloud Scheduler triggers it every morning; it
reads whatever hit the market overnight and decides on its own whether anything
justifies telling you. Staying quiet is a valid outcome — an agent that pings you
about every new listing is just a worse email alert.
When you've decided what's worth seeing, it plans the day: orders the stops by
geography, allocates realistic time at each one, says what to verify at each door
given what it already found in the plan, and returns a Google Maps route.
## How I built it
The decision everything else follows from: **Gemini does perception, code does
judgement.** The model is never shown the buyer's weights and never asked for a
score.
That split buys three things:
1. **Perception caches.** It depends only on the images, so it's computed once
per property. A re-rank costs 23ms and no model call.
2. **Scores are reproducible.** The same house scores the same number every time,
so comparing houses week over week means something.
3. **The rulebook is swappable.** Vastu and Feng Shui are rule tables, not code.
They genuinely disagree — a south-facing entrance is a flaw in one and the
classical ideal in the other — so switching re-ranks the entire list without
calling Gemini at all.
Stack: Gemini 3.5 Flash-Lite through the Google GenAI SDK, two Cloud Run services
at min-instances 0, Firestore for the preference profile and agent briefs, Cloud
Scheduler for the overnight cycle, Cloud Trace for OpenTelemetry spans, Secret
Manager for the key. Next.js 16 and Tailwind on the front.
## What I learned
**Measuring the model mattered more than choosing it.** The floor plans and
aerials are generated from written specs with Gemini 3.1 Flash Image, which is
the only reason ground truth is knowable. `npm run verify:seed` scores the
agent's readings against that answer key offline: 93% exact, 0% wrong across 42
fields. Nothing read backwards. With scraped listings I'd have had no answer key
and no way to tell a confident output from a correct one.
**gemini-3.5-flash was the wrong default.** Measured on identical two-image
requests, it once took 59 seconds to return a 503. gemini-3.5-flash-lite answered
the same request in 1.5 seconds at equal accuracy. Both satisfy the model
requirement. I now run a fallback chain and record which model produced each
reading.
**503 UNAVAILABLE is not 429 RESOURCE_EXHAUSTED.** Every failure I hit was
capacity on Google's side, not my quota. Enabling billing doesn't fix it. A
fallback chain does.
**Three bugs could only surface on Cloud Run.** The Firestore client throws an
uncaught exception when credentials are missing, from a deferred gRPC stub
outside any try/catch. The local fallback then crashed writing beside the app,
because the filesystem is read-only outside /tmp. And OpenTelemetry's batch
processor never flushes, because CPU is throttled to near zero between requests.
**A cache clear that races a live scan is worse than no clear.** It reports
success and silently refills. `/api/cache/clear` now refuses while a scan is in
flight.
## Challenges
The honest one: the first version was a lie. It looked like it was analysing
floor plans, but the image-fetching code was commented out — every audit was
hallucinated from the street address. Scans took 30 seconds, three of five
aerial images were 404ing, and the feedback endpoint returned 500 on every call.
Rebuilding it around the perception/judgement split is what made the rest
possible.
Log in or sign up for Devpost to join the conversation.