Redfin Listings + Investor Deal Scoring
Pricing
$0.70 / 1,000 listing scoreds
Redfin Listings + Investor Deal Scoring
Pull live Redfin for-sale listings by city, bounding box, or search URL, and score every listing 0-100 for investor deal quality: $/sqft vs the batch median, days-on-market percentile, gross rent yield, and last-sale delta.
Pricing
$0.70 / 1,000 listing scoreds
Rating
0.0
(0)
Developer
Ian Rose
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Redfin Deal Finder
Pull live Redfin for-sale listings by city, bounding box, or search URL, and get every listing back with a transparent 0-100 investor deal score -- not just a field dump.
What it does
- You give it one or more searches:
"Austin, TX", a raw bounding box, or a redfin.com search URL. - It queries Redfin's public
/stingray/api/gisJSON endpoint directly (the same endpoint the redfin.com map view uses) -- no browser, no HTML scraping, no residential proxy spend. - Each listing comes back normalized (price, beds, baths, sqft, address, lat/lng, days on market, continuous listing age, year built, lot size, monthly HOA, the agent's listing remarks, and last-sale date where Redfin exposes it) plus four investor-relevant metrics and a single blended deal score, computed transparently from the listings in your own search batch.
Why deal scores (and why this is different from the field-dump actors)
Most Redfin scrapers hand you the same raw JSON Redfin's map already shows you. This actor adds the layer an investor actually wants: is this listing cheap for the area, has it been sitting long enough to negotiate, does it cash-flow at your assumed rent, and is it priced below what it last sold for. Every input to the score is documented below -- nothing is a black box.
Sample output
One real listing, exactly as this actor pushed it -- normalized and scored
against a live 25-listing Boise batch (no rent assumption supplied, so the
rent-yield component is null; the 699-character listingRemarks string is
the only thing abbreviated here, for readability):
{"propertyId": 181145809,"mlsId": "98997726","price": 403990,"beds": 3,"baths": 2.5,"sqft": 1679,"streetLine": "12535 W Victory Rd","city": "Boise","state": "ID","zip": "83709","latitude": 43.575434,"longitude": -116.338079,"daysOnMarket": 1,"listingAgeDays": 1.2205244675925926,"propertyType": 13,"yearBuilt": 2022,"lotSize": 2657,"hoaMonthly": 230,"listingRemarks": "Welcome to 12535 W Victory Rd! Built in 2022, this spacious Boise townhome offers 3 bedrooms...","url": "https://www.redfin.com/ID/Boise/12535-W-Victory-Rd-83709/home/181145809","lastSaleDate": null,"lastSalePrice": null,"sashesRaw": [7],"ppsf": 240.61346039309112,"ppsfDeltaPct": -29.11062571765032,"domPercentile": 70,"grossRentYieldPct": null,"lastSaleDeltaPct": null,"dealScore": 88.3,"batch": {"search": "Boise, ID","ppsfMedian": 339.42105263157896,"ppsfMedianReason": null,"batchSize": 25,"validPpsfCount": 25,"truncated": false,"truncatedTiles": 0,"capReachedEarly": false}}
Where 88.3 comes from -- every number above is reproducible from the weights table below:
- $/sqft:
403990 / 1679 = 240.61per sqft vs. the batch median of339.42->ppsfDeltaPct = -29.11%(29% cheaper per sqft than the batch). Mapped over-30%..+30% -> 100..0, that's a sub-score of98.52. - Days on market: this listing has been up
1.22days; 17 of the 25 listings in the batch are newer, so its midrank percentile is(17 + 0.5) / 25 = 70.0. Percentiles are used directly as the sub-score. - Rent yield / last sale: both
nullhere (nomonthlyRentAssumptiongiven, and Redfin published no last-sale price for this home), so their weights drop out and the remaining two renormalize over.0.45 + 0.25 = 0.70 - Blend: ->98.52 x (0.45/0.70) + 70.0 x (0.25/0.70) = 63.33 + 25.00 = 88.33
dealScore88.3.
For contrast, the same batch's one genuinely stale listing (75.3 days up)
lands at domPercentile 98.0 -- the highest in the batch, above every
same-day listing -- but it's priced ~4% above the batch's $/sqft median,
so its blended score comes out at 63.2. High DOM alone doesn't buy a high
score; the score is the blend.
Input examples
1. City search (looked up in a bundled ~660-city table):
{ "searches": ["Austin, TX", "Boise, ID"], "radiusMiles": 8, "maxListingsPerSearch": 300 }
2. Raw bounding box (lat1,lng1,lat2,lng2):
{ "searches": ["30.20,-97.80,30.32,-97.68"], "maxListingsPerSearch": 500 }
3. Investor filters + rent assumption (enables the rent-yield metric):
{"searches": ["Tampa, FL"],"monthlyRentAssumption": 2200,"minPrice": 150000,"maxPrice": 400000,"minBeds": 2}
Scoring methodology
dealScore (0-100) is a weighted blend of up to four sub-scores. If a
component can't be computed for a given batch or listing (e.g. no rent
assumption supplied, or fewer than 8 listings in the batch), its weight is
dropped and the rest are renormalized -- a listing missing one metric is
still comparable to one with all four.
| Component | Weight | What it measures | Mapping |
|---|---|---|---|
| $/sqft advantage | 45% | Listing's price/sqft vs. the median price/sqft of your search batch | -30%..+30% delta -> 100..0 (cheaper = higher score) |
| Days-on-market percentile | 25% | How long this listing has sat vs. the rest of the batch | 0..100 midrank percentile, used directly (older = more negotiable = higher score) |
| Gross rent yield | 20% | 12 * monthlyRentAssumption / price * 100, only if you supply monthlyRentAssumption | 0%..12% -> 0..100 |
| Last-sale delta | 10% | Current price vs. the listing's last recorded sale price, where Redfin exposes it | -20%..+20% delta -> 100..0 (priced below last sale = higher score) |
ppsfMedian (the batch's $/sqft baseline) requires at least 8 listings
with both a price and a sqft in the batch; below that, ppsfMedian and
every listing's ppsfDeltaPct come back null with a ppsfMedianReason
string explaining why, and the deal score falls back to whatever other
components are available.
How the DOM percentile handles ties and fresh listings. Redfin's own
dom field is whole days and pins at 1 for nearly every fresh listing
(22 of 25 in the live sample above), which makes it useless for ranking
within a normal batch. So the score is driven by listingAgeDays -- a
continuous listing age derived from Redfin's millisecond timeOnRedfin
field -- and falls back to daysOnMarket only when that's missing. Ties
are scored by midrank (listings strictly below, plus half the listings
tied with you): a group all tied at the freshest end lands mid-pack rather
than each member claiming the whole tied group as "older than me".
What's in the batch. Every scored row carries a batch object with the
search it came from, the median it was compared against, the batch size,
and a truncated/truncatedTiles pair -- see "Coverage and dedupe" below.
Coverage and dedupe
- Deduped across the whole run, not just within one search. A listing
is keyed by its Redfin
propertyIdand pushed at most once per run, so overlapping searches ("Boise, ID"plus a bounding box over the same neighborhood) never bill you twice for the same home or double it in the dataset. The first search to see a listing keeps it, and it is scored against that search's batch. - Dense areas are tiled. Redfin's map endpoint truncates around 350
homes per request, so a search box that hits that ceiling is split into
quadrants and re-queried, up to 3 levels deep. If a tile is still
saturated at the deepest level, the run logs a warning and every row from
that search carries
batch.truncated: trueand abatch.truncatedTilescount. In practice the cap below usually stops the crawl before the depth limit is ever reached, so for dense searches the coverage signal to read isbatch.capReachedEarly, nottruncated. Narrow the search (smallerradiusMilesor a tighter bounding box) or raise the cap to get full coverage of an area that flags either one. - The cap stops the crawl, not just the output. Tiles are fetched only
until
maxListingsPerSearchfilter-passing listings are in hand; the rest of the quadtree is skipped. A dense search that stops this way carriesbatch.capReachedEarly: true-- the batch is the first N listings of a larger area, not the whole area. RaisemaxListingsPerSearch(up to 1000) for fuller coverage. This is also what keeps the default input fast: the prefilled "Austin, TX" + "Boise, ID" run completes in a handful of requests instead of walking ~90 tiles. - Failed searches never charge you. A search that can't be resolved
(unparseable city, out-of-range bounding box, a Redfin URL with no
viewport in it) is pushed to the dataset as an uncharged
{ searchInput, error }row so you can see exactly what went wrong. If every search in a run fails, the run itself fails rather than reporting success on an empty dataset.
Integration
curl (Apify API):
curl "https://api.apify.com/v2/acts/steadycrawl~redfin-deal-finder/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-X POST -H "Content-Type: application/json" \-d '{"searches": ["Austin, TX"], "maxListingsPerSearch": 100}'
Python (apify-client):
from apify_client import ApifyClientclient = ApifyClient(APIFY_TOKEN)run = client.actor("steadycrawl/redfin-deal-finder").call(run_input={"searches": ["Austin, TX"], "monthlyRentAssumption": 2000})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["dealScore"], item["streetLine"])
Make / n8n: use the built-in Apify app/node -- run this actor, then feed
defaultDatasetId into a "Get dataset items" step and sort/filter on
dealScore.
MCP: this actor is callable from any MCP-compatible client via Apify's
Actors MCP Server -- point your MCP client at it
and call steadycrawl/redfin-deal-finder like any other tool.
Limitations (transparent, by design)
-
Extreme $/sqft outliers are excluded from scoring: when a listing's $/sqft deviates more than ±60% from the batch median (land parcels, part-shares, data errors), its value axis is dropped and
ppsfAxisReasonexplains why — the raw delta is still reported, but it can't fake a top deal score. -
For-sale search results only, v1. No sold-comps search, no price-drop history -- Redfin exposes those through endpoints that are edge-blocked for plain HTTP (see the "architecture" note below).
-
lastSaleDateis common;lastSalePriceis rare. The date comes from Redfin's own top-level sold-date field and was populated for 16 of the 25 listings in the live sample above. The price is only picked up when Redfin ships it on a listing'ssashesbadge, and in that same sample every sash carried a blank placeholder (lastSalePrice: "") rather than a real number -- solastSalePrice, and with it the last-sale component of the deal score (10% weight), is genuinely best-effort. When it's absent its weight is redistributed across the other components, so scores stay comparable. -
Rent yield needs your own assumption. There's no automatic market rent estimate; supply
monthlyRentAssumptionto enable that metric. -
propertyTypeis Redfin's internal numeric enum, passed through as-is rather than decoded to a label (the enum table isn't publicly documented and guessing at it risked shipping wrong labels). -
Architecture constraint: this actor calls exactly one Redfin endpoint,
/stingray/api/gis, and nothing else. Redfin's location-autocomplete,/zipcode/pages, and per-listing detail endpoint are all edge-WAF-blocked for plain HTTP and are never called. Monthly HOA (hoaMonthly) and the agent's listing remarks (listingRemarks) are included -- they ship in the map endpoint's own payload (HOA on 15 of 25 and remarks on 25 of 25 listings in the live sample). What genuinely needs a blocked endpoint, and is therefore out of scope until that changes, is deeper per-listing detail: full price-drop history, tax history, and school/walkability data.
Support
Issues get a same-day response. Open one on the actor's Issues tab with a sample input and what you expected to see.
Changelog
- 1.0.6 -- cap-aware crawl: stop fetching tiles once
maxListingsPerSearchfilter-passing listings are collected (newbatch.capReachedEarlyflag); the prefilled input now finishes in seconds instead of walking the whole quadtree (86 tiles / ~4 min for "Austin, TX"), which is what tripped Apify's 5-minute Store QA run. Local test harness now also runs the exact prefilled input as a timing gate. - 1.0.5 -- run-start pricing gate (refuse to crawl when the
listing-scoredevent carries no price), $/sqft outlier guard. - 1.0.0 -- initial release: gis-only crawl, quadtree tile splitting, offline city lookup table (~660 US cities), 4-component deal score, pay-per-event pricing.