I Scraped Every Rental Listing in a City to Pick a Neighborhood
A weekend tool that pulled rental listings into SQLite, tracked price changes over time, and told me which listings were actually a good deal instead of just new.
I was moving, and I had two questions no rental site wanted to answer. Is this a good price for this area, or does it only look good next to the listing I saw yesterday? And has this place been sitting unrented for six weeks with the price quietly sliding down?
Rental sites are built to show you what is new. I wanted to see what was changing. So I spent a weekend building a small tool that watched the market instead of browsing it.
The shape of it
It is deliberately unclever. A scraper pulls listings on a schedule, normalizes them into SQLite, and a small React front end reads from an Express API. Everything runs locally. There is no cloud, no account, no deploy.
The scraping layer is the only part with any real design in it, and only because I knew I would want to add sources later:
export interface ListingSource {
name: string;
enabled: boolean;
fetchListings(): Promise<NormalizedListing[]>;
}
Every source implements that and gets registered with an orchestrator. Adding a site is one file and one line. Sources are individually toggleable by environment variable, which matters more than it sounds, because scrapers break constantly and you want to disable a broken one without redeploying your afternoon.
The part that actually mattered
Price history. Everything else was table stakes.
Each scrape writes a full snapshot and diffs it against the last one. A price change becomes a row with the delta in both dollars and percent. A disappearance becomes a removal event. New listings become arrival events. All of it flows into an activity feed that reads like a changelog for the rental market.
That reframes the whole problem. Instead of "here are 40 listings, good luck," you get "this three bedroom dropped 8 percent after 22 days on the market," which is an actionable sentence. The listings that had been sitting were the ones worth calling about, and you cannot see that from a single snapshot no matter how good the filters are.
I also classified listings into the specific school district I cared about versus the rest of the city, using bounding boxes. Crude, and I noted in the code that it should be polygons. It never needed to be, because the boxes were right often enough for a decision I was going to verify in person anyway.
Two things I got right by accident
Deduplication across sources. The same property shows up on multiple sites with slightly different addresses and wildly different photos. Matching them meant one entry with several sources attached, instead of three entries that all look like separate options. Without that, the feed was noise.
Not marking everything as removed when a scrape fails. There is a flag, SKIP_REMOVAL_IF_NO_SOURCE, that exists because the first time every source failed at once, the tool cheerfully recorded that every rental in the city had been taken off the market. A failed scrape and an empty market look identical if you are not careful, and the difference matters enormously.
Would I build it again
Yes, and I would keep it just as small. This is a tool, not a product. It answered a specific question during a specific month and then stopped being needed. Three commits, one weekend, and I signed a lease on a place I would not have looked twice at otherwise, because the feed showed me it had been quietly getting cheaper for a month.
The best thing about software you write for yourself is that it is allowed to be finished.