Skip to content
NR
All writing
Agentic SystemsApril 20, 2026·8 min read

Underwriting NYC Real Estate in Four Seconds

How I wired NYC's public property registries into a parallel pipeline and a single Claude pass to turn an afternoon of spreadsheet work into a source-cited feasibility report.

Figuring out whether a NYC lot is worth developing usually means a broker opening six tabs: GeoSearch to resolve the address, PLUTO for the lot's zoning and dimensions, ACRIS for what nearby properties actually sold for, the Zoning Resolution to decode what any of it means — then a spreadsheet to tie it together. It's an afternoon of work, and it's the same afternoon every single time.

I built the CRE Intelligence Platform to collapse that into one query. You type an address; about four seconds later you get a source-cited feasibility report with a buildable massing model and a pro forma you can actually adjust.

The latency problem came first

The naive version of this is a chain: resolve the address, then fetch the lot, then pull the comps, then ask the model. Done sequentially against three separate city APIs, you're staring at a spinner for fifteen-plus seconds — and on Vercel's serverless timeouts, sometimes you're staring at an error instead.

So the pipeline fans out instead of queuing up. Once GeoSearch hands back a BBL (the borough-block-lot ID), the PLUTO lookup and the ACRIS deed-sales query fire in parallel with asyncio.gather, and the zoning context comes from a small RAG index rather than a live call. By the time the data is assembled, it's one compiled record — which means one Claude turn, not a multi-step agent loop. That single-turn decision is most of where the four seconds comes from.

# resolve once, then fetch everything that doesn't depend on each other together
bbl = await geosearch(address)
lot, comps = await asyncio.gather(
    pluto_lookup(bbl),
    acris_deed_sales(bbl, radius_m=400),
)
zoning = zoning_rag.retrieve(lot.zoning_district)
report = await claude.evaluate(lot=lot, comps=comps, zoning=zoning)

Skipping WebGL on purpose

The buildable-massing visualizer — the little 3D block that scales with footprint and allowable stories — is pure CSS 3D transforms. No Three.js, no WebGL context. For a shape this simple it's the right call: the bundle stays tiny, it renders instantly on a phone, and there's nothing to crash. Reaching for a 3D engine here would've been resume-driven development.

What I'd tell the next person building this

Two things held up. First, in a serverless world, parallelizing your external calls isn't an optimization — it's the difference between shipping and timing out. Second, handing the model raw, compiled context and asking for one grounded judgment beats an elaborate agent that makes ten round-trips to arrive at the same answer more slowly. The agent framing is seductive; the single faithful turn is usually what the user actually wants.