Skip to content
Kavin MohanadasStart a projectTalk to Kavin AIKavin AI
All workAI · 04 of 10

Built · code on GitHub

Nestly

AI home search that turns a plain-English prompt into live Canadian listings.

Role
Designer & developer

In short

  1. 01

    LLM parses a natural-language prompt into filters run against live Realtor.ca listings

  2. 02

    Firecrawl scraping with a ~30 min cache; empty results returned rather than sample data

  3. 03

    Leaflet map with clustered pins and Nominatim geocoding; no pin without confirmed coords

Built with TypeScript · Node.js · pnpm · React · Vite · Express · PostgreSQL · Drizzle ORM · Zod · Orval · OpenAI · Firecrawl · Leaflet · Clerk · Replit

Problem

Traditional real-estate search makes people translate their wants into filter forms. I wanted a search where someone can describe their ideal home conversationally, such as "4-bed house in Scarborough under $1M", and get back real listings that match.

Two constraints shaped the build: the market is Canada (Realtor.ca as the primary source), and the product must show only real, live listings, never generated or sample houses.

Approach

The search flow is prompt → AI extracts filters → live listings are retrieved, scored, and summarized. The LLM (OpenAI through Replit AI Integrations) handles prompt parsing and search summaries, and the API also exposes conversational AI endpoints; it never produces listings. Listings come from Firecrawl scraping Realtor.ca, with Zolo.ca as fallback.

Search is map-first: a full-bleed dark Leaflet map with listing pins, a map/list toggle, live counters, an AI search bar, and a property drawer. Detail pages show a photo carousel with lightbox, source fields (year built, MLS#, days on market), a derived price per square foot, and a "View on {source}" link; fields the source did not provide read "Not available". Browsing is public; signing in through Replit-managed Clerk unlocks favorites, saved searches, recently-viewed history, and a notification bell.

Architecture

The repo is a pnpm workspace on Node.js 24 and TypeScript 5.9: a React + Vite frontend (artifacts/home-search), an Express 5 API (artifacts/api-server), and PostgreSQL with Drizzle ORM. lib/api-spec/openapi.yaml is the API contract; Orval generates frontend hooks and Zod schemas from it.

User prompt
  │
  ▼
React + Vite frontend ──► Express 5 API
                             │
                             ├─► propertySearch.ts   parsePromptToFilters (LLM)
                             │
                             ├─► firecrawlSearch.ts  Firecrawl → Realtor.ca (Zolo.ca fallback)
                             │                        ~30 min cache by URL · in-memory property store
                             │
                             ├─► propertySearch.ts   scoreProperties · generateSearchSummary
                             │
                             └─► geocode.ts          Nominatim → lat/lng → geocode_cache (ok | failed)
                                                     misses queued; client polls /properties/geocode-status
  │
  ▼
Leaflet map (supercluster pins) · list view · property drawer

Geocoding never blocks listing retrieval: geocode.ts persists terminal ok/failed results to a geocode_cache table and queues misses; the frontend polls geocode-status about every 2.5 s with a hard cap and draws pins only for listings marked ok. The detail route runs a cached per-listing Firecrawl scrape and merges only observed fields, so missing data stays null.

Identity comes from the Clerk session (cookie auth) in a requireAuth middleware, never from the request body or query; a users table is provisioned just-in-time, per-user routes check ownership by user id, and CORS uses a same-origin allowlist.

Technical decisions

  • No fake data, ever → a hard product requirement; when scraping returns nothing the API returns an empty set with an honest summary.
  • Backend calls Firecrawl's API directly with FIRECRAWL_API_KEY → the managed sandbox callback double-encodes JSON bodies and is unavailable in the Node backend.
  • Aggressive caching (~30 min for scrapes, ~6 h for featured listings) → conserves Firecrawl credits and cuts latency.
  • Favorites and visited history persist the client's property snapshot → survives restarts of the in-memory property store.
  • Same-origin CORS allowlist, never reflect-all → auth is cookie-based.
  • Price per square foot is derived from price/sqft, never extracted from the listing → part of the same data-honesty rule as the "Not available" handling for missing fields.

Challenges

  • Direct server fetches of Realtor.ca and Zillow return 403, so retrieval goes through Firecrawl, which needs location: { country: "CA" }, proxy: "auto", and a city-to-province map for Realtor.ca URLs.
  • A cold search hits Firecrawl and can take 20–60 s; repeat searches are fast because results are cached.
  • Nominatim cannot resolve unit-prefixed addresses such as "1007 - 120 VARNA DRIVE" to street level; those listings show "Location pending" or "Map pin unavailable" badges rather than a guessed pin.
  • The property detail store is in-memory and lost on restart, so production would need database persistence for it.

Results

The app covers natural-language search returning live Realtor.ca listings; the map-first search page with clustered pins, toggle, counters, and drawer; a home page with a 3D intro and live "Featured Residences"; property detail pages; per-user favorites, saved searches, recently-viewed history, and a notification bell; and Favorites and Market pages. It is built, and the code is on GitHub.

Kavin AI — an AI version of Kavin

Ask about Nestly.

Kavin AI, the AI version of me, answers from the same approved write-up you just read.

Next project 05 / 10 · Platform

Queen's AMS Event Finder

Pilot · not officially launched

A 3D campus map for finding events at Queen's University.