Rental Listing Normalizer
Pricing
Pay per usage
Rental Listing Normalizer
Turns public rental listing pages (Craigslist and Zillow search results) into clean, comparable facts like price, beds, baths, size, location and amenities, with source links and no contact details.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Show Before
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Rental Listing Normalizer + Scam Signals
Turn public rental listing pages into clean, comparable facts, plus rental-scam red flags and perceptual photo hashes for duplicate detection. Built at Cal Hacks 13.0 for Showbefore, a housing tool covering the cities defined in src/markets.js.
The output retains up to five remote photo URLs for listing cards. It never contains image bytes, listing descriptions, street addresses, names, phone numbers or emails. You get facts you can compare, plus a link back to the original.
What it does
| Input URL | Output |
|---|---|
Craigslist search page (…craigslist.org/search/apa, /roo, /sub) | One item per result: monthly price, beds, baths, approx. location, kind (apartment / room / sublet), price basis |
Craigslist real-estate search (…/search/rea) | Explicit intent: "sale" and askingPrice; never a monthly rent |
| Craigslist posting page | One detailed item: the facts above, plus laundry / parking / pets, red flags found in the ad text, and dHash of each photo |
Zillow homedetails page | Calls the Store Actor maxcopell/zillow-detail-scraper (Actor-to-Actor), then normalizes its row the same way |
| Any page with schema.org JSON-LD / OpenGraph | A best-effort item from the structured data |
| Facebook / Instagram | Refused (login_walled_site_not_supported). Login-walled pages are out of scope by design |
Normalization you would otherwise write yourself
- Price basis. Student complexes often advertise rent per bedroom ("4 bd $549"). Rows like that get
priceBasis: "per_bed"so they don't drag down whole-unit comps. Room shares getroom; weekly or nightly prices getunknown. - Kinds. Craigslist
apa,rooandsubmap toapartment,roomandsublet. Zillow home types map tohouseorapartment. - Market. Market ids come from the shared
src/markets.jsregistry (eleven cities). ZIP, then city/state, then nearest bounded center for coordinate-only rows; unmatched rows remainother. - Location privacy. Coordinates are rounded to 3 decimals (about 100 m). Zillow
homedetailsURLs are rewritten to the address-free…/homedetails/<zpid>_zpid/form. - Address key.
addressKeyis a SHA-256 of the normalized street, unit and ZIP, so you can join the same unit across sites without storing the address. - Photos.
photoUrlskeeps up to five HTTPS URLs from known listing image hosts. Search rows with no images fetch their posting page by default (capturePhotos: falsedisables this). Failed photo pages retain the original search facts with no photos; sale intent/prices are preserved. Craigslist sizes prefer 600×450; Zillow supports both snake_case and camelCase responsive-photo fields. These page requests use the existing Actor compute/proxy budget.
Scam signals
redFlags: patterns from FTC rental-scam guidance, matched in the ad text. The text itself is not output; each match comes with a short excerpt.payment_wire_or_giftcard: wire transfer, gift cards, crypto, or a cashier's checkowner_abroad: the owner says they are away or out of the countrydeposit_before_viewing: money before a viewing, or keys sent by mailoff_platform_contact: pushes you to text, email or a messaging appurgency_pressure: pressure to act fast
photoHashes: a 64-bit dHash per photo. Each photo is downloaded into memory, hashed, and discarded. Compare hashes by Hamming distance (≤ 6 is a near-certain copy) to catch photos copied from another listing. Photos are hashed 4 at a time under one 40 s budget per listing; if it runs out, the listing is still returned with the hashes that finished andhashing: "incomplete".
Input
{"startUrls": [{ "url": "https://gainesville.craigslist.org/search/apa" },{ "url": "https://sfbay.craigslist.org/search/sfc/roo" },{ "url": "https://www.craigslist.org/view/d/<slug>/<id>" }],"hashPhotos": true,"capturePhotos": true,"maxPhotosPerListing": 12,"maxItemsPerUrl": 500,"zillowViaActor": true,"proxyConfiguration": { "useApifyProxy": true }}
Output (one dataset item)
{"mode": "listing","sourceSite": "craigslist.org","sourceId": "craigslist:<id>","sourceUrl": "https://www.craigslist.org/view/d/<slug>/<id>","market": "gainesville", "city": "Gainesville", "state": "FL", "zip": "32608","lat": 29.613, "lng": -82.387,"kind": "apartment", "beds": 1, "baths": 1, "sqft": null,"priceMonthly": 1285, "priceBasis": "unit","amenities": { "laundry": "in_unit", "parking": "assigned", "pets": "allowed", "furnished": null },"redFlags": [],"photoHashes": ["c8d4b2a29292b2f0", "…"],"photoUrls": [],"addressKey": null,"flags": [],"retrievedAt": "2026-10-24T23:02:12.987Z"}
Rows that fail come back as { "sourceUrl", "error", "detail" }, so you can see which URLs failed and why.
How Showbefore uses it
- Live comps. The Worker starts this Actor on Craigslist
apa,rooandsubsearches for Gainesville and SF. It upserts the results into itsmarket_compstable, with this run's ID as the provenance of every row. - "Is this listing legit?" A renter pastes a listing URL. The Worker calls
run-sync-get-dataset-itemswith a $0.10 spend cap. It compares the price with a distance- and recency-weighted band of real comps, and the photo hashes with hashes of other real listings. It also checks for the same address on another site at a different price, and for the red flags above.
Pricing
Pay per event: listing-normalized is charged once per item output. Zillow calls are billed by maxcopell/zillow-detail-scraper.
Responsible use
- Public, logged-out pages only.
- Use the output as facts for comparison, with attribution and a link to the original. Do not republish other people's photos or descriptions.
- Respect each site's terms. Craigslist and Zillow restrict automated collection and reuse. You are responsible for how you use this Actor.
Local development
npm installnode --test test/*.test.mjsAPIFY_LOCAL_STORAGE_DIR=./storage CRAWLEE_STORAGE_DIR=./storage node src/main.js # reads storage/key_value_stores/default/INPUT.jsonapify push