Rightmove Listings + Investor Deal Scoring (UK)
Pricing
$0.70 / 1,000 listing scoreds
Rightmove Listings + Investor Deal Scoring (UK)
Pull live Rightmove for-sale listings by city/town or search URL (UK-wide: England, Scotland, Wales, Northern Ireland), and score every listing 0-100 for investor deal quality: price/sqft (or price/bedroom when size isn't listed) vs the batch median, days-on-market percentile, and gross rent yield.
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
5 days ago
Last modified
Categories
Share
Pull live Rightmove for-sale listings by city/town or search URL -- UK-wide coverage: England, Scotland, Wales, and Northern Ireland -- 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: a city/town name (
"Leeds","Newcastle upon Tyne"), or a fullrightmove.co.uk/property-for-sale/*.htmlsearch URL. - It requests Rightmove's own server-rendered search-results page directly
-- the same HTML the site itself renders -- and reads the listing data
out of its embedded
__NEXT_DATA__JSON. No browser, no separate resolver/typeahead call, no residential proxy spend. - Each listing comes back normalized (price, bedrooms, bathrooms, size in sqft where listed, address, lat/lng, property type, tenure, days on market, listing-update reason/date, the agent's summary, listing URL, image count, and the branch name where Rightmove exposes it) plus 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 Rightmove scrapers hand you the same raw listing cards the site's own search page 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, and does it cash-flow at your assumed rent. 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 50-listing Leeds batch (no rent assumption supplied, so the
rent-yield component is null; the summary string is shown in full here):
{"id": 154815185,"price": 2750000,"bedrooms": 6,"bathrooms": 4,"sizeSqFt": 8103,"displayAddress": "Wigton Lane, Alwoodley, Leeds","latitude": 53.858769,"longitude": -1.518732,"propertySubType": "Detached","tenure": "FREEHOLD","firstVisibleDate": "2024-11-11T09:21:36Z","daysOnMarket": 646.8434082986112,"listingUpdateReason": "price_reduced","listingUpdateDate": "2025-11-01T11:56:58Z","summary": "A substantial family residence, occupying an established prime position in one of the most sought after residential addresses in Alwoodley","propertyUrl": "https://www.rightmove.co.uk/properties/154815185#/?channel=RES_BUY","imageCount": 32,"agent": "Butler Ridge, Wetherby","ppsf": 339.3804763667777,"ppsfDeltaPct": -17.576099294463283,"pricePerBed": null,"ppBedDeltaPct": null,"valueAxis": "ppsf","valueDeltaPct": -17.576099294463283,"valueAxisReason": null,"domPercentile": 97,"grossRentYieldPct": null,"dealScore": 85.6,"batch": {"search": "Leeds","ppsfMedian": 411.7500791175008,"ppsfMedianReason": null,"ppBedMedian": 406785.7142857143,"ppBedMedianReason": null,"batchSize": 50,"validPpsfCount": 36,"validPpBedCount": 50,"resultCount": 4821,"truncated": true,"pagesFailed": 0}}
Where 85.6 comes from -- every number above is reproducible from the weights table below:
- Value axis (£/sqft, since this listing has a size): per sqft vs. the batch median of2750000 / 8103 = £339.38
£411.75->(17.6% cheaper per sqft than the batch). Mapped overvalueDeltaPct = -17.58%-30%..+30% -> 100..0, that's a sub-score of79.29. - Days on market: this listing has been up
646.8days -- longer than every other listing in the batch bar one, so its midrank percentile is97.0. Percentiles are used directly as the sub-score. - Rent yield:
null(nomonthlyRentPerBedroomGBPgiven), so its weight drops out and the remaining two renormalize over.0.45 + 0.25 = 0.70 - Blend: ->79.29 x (0.45/0.70) + 97.0 x (0.25/0.70) = 50.98 + 34.64 = 85.62
dealScore85.6.
The £/bed fallback axis, on the same batch -- an 8-bed estate on
Colliers Lane (id 91889727, £3,500,000) had no displaySize at all
(sizeSqFt: null), so its value axis fell back to price-per-bedroom
instead: £3,500,000 / 8 beds = £437,500/bed vs. the batch's own £/bed
median of £406,785.71 -> valueDeltaPct = +7.55% (7.55% more expensive
per bedroom than the batch). Mapped over that axis's own wider
-60%..+60% band, that's a sub-score of 43.71; combined with a very fresh
listing (domPercentile = 9.0, near the bottom -- newly listed, least
negotiable) it blends to dealScore = 31.3. Same formula, same weights,
just a different value metric feeding it -- valueAxis: "ppbed" on the row
tells you which one was used.
And when a listing isn't comparable at all -- the same batch contained
"Development Opportunity - Moor Road, Headingley" (id 168402920,
£2,250,000, 58 "bedrooms", 25,198 sqft): a development site priced like
land, 78.3% below the batch's £/sqft median. Clamping that to a perfect
100 would have crowned a non-comparable listing the batch's best deal, so
the value axis is dropped instead -- valueAxis: "none",
valueDeltaPct: null, and a valueAxisReason string saying exactly why.
Its score then comes from the remaining axes alone (here DOM only, 89.0).
The raw ppsfDeltaPct stays on the row either way, so you can still see and
filter on the number.
Input examples
1. City/town search (spaces slugify to hyphens, matching Rightmove's own URL form):
{ "searches": ["Leeds", "Newcastle upon Tyne"], "maxListingsPerSearch": 300 }
2. Full Rightmove search URL:
{ "searches": ["https://www.rightmove.co.uk/property-for-sale/Manchester.html"], "maxListingsPerSearch": 500 }
3. Investor filters + rent assumption (enables the rent-yield metric):
{"searches": ["Cardiff"],"monthlyRentPerBedroomGBP": 700,"minPrice": 100000,"maxPrice": 300000,"minBeds": 2}
Scoring methodology
dealScore (0-100) is a weighted blend of up to three 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 three.
| Component | Weight | What it measures | Mapping |
|---|---|---|---|
| Value axis: £/sqft (or £/bedroom fallback) | 45% | Listing's price/sqft vs. the median price/sqft of your search batch -- or, when the listing (or the whole batch) has no size data, price/bedroom vs. the batch's own £/bed median instead | £/sqft: -30%..+30% delta -> 100..0. £/bed: -60%..+60% -> 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 | 30% | 12 * monthlyRentPerBedroomGBP * max(bedrooms, 1) / price * 100, only if you supply monthlyRentPerBedroomGBP | 0%..12% -> 0..100 |
Rent is per BEDROOM, not per property. monthlyRentPerBedroomGBP is
multiplied by each listing's own bedroom count (a missing or 0-bed listing
counts as 1, so studios still get a yield). A single flat whole-property
rent applied to every listing degenerates into an inverse-of-price proxy --
it just favours whatever is cheapest regardless of how many bedrooms it has
-- which is why this input is stated per bedroom.
The two value axes use different clamp bands, and are not strictly
comparable listing-to-listing. £/bed spreads far wider than £/sqft in the
same batch (bedroom counts are coarse buckets; floor area is continuous), so
the ±30% band that fits £/sqft saturates most £/bed listings at 0 or 100 and
throws away the ranking. £/bed therefore gets its own ±60% band. The
practical consequence: a "ppsf" row and a "ppbed" row with the same
dealScore are not making the same claim about relative value. Compare
within an axis (filter on valueAxis) when ranking matters.
Non-comparable listings are dropped from the value axis, not clamped.
A selected-axis delta beyond ±60% (a land/development plot's £/sqft, a
shared-ownership part-price's £/bed) is treated as a listing that isn't
comparable to the batch at all: valueAxis becomes "none",
valueDeltaPct becomes null, valueAxisReason explains it, and the
remaining weights renormalize. Clamping those to a perfect 100 instead would
put a distortion at the top of every ranking. The raw ppsfDeltaPct /
ppBedDeltaPct are still reported on the row.
Why no last-sale axis. Rightmove's for-sale search payload carries no sold-price history for a live listing (that data lives behind a separate sold-comps product this actor doesn't call -- see Limitations). Rather than ship a fourth weight slot that always drops out and silently distorts every listing's renormalization, the 10% the Redfin sibling actor gives to last-sale delta is folded directly into rent yield here (20% -> 30%), so the three real axes always sum to 100% of the score when all are available.
Value axis (ppsfMedian / ppBedMedian, the batch's £/sqft and £/bed
baselines) each require at least 8 listings in the batch with the
relevant fields (price+sizeSqFt for ppsf, price+bedrooms for ppbed); below
that, the relevant median and every listing's delta come back null with a
*MedianReason string explaining why. UK listings frequently omit
displaySize (confirmed live: 14 of 50 in the sample Leeds batch above
had no size at all) -- when a listing has no size, or the batch is too small
for a ppsf median, its value axis falls back to price-per-bedroom
automatically; valueAxis on each row ("ppsf", "ppbed", "none" if the
delta tripped the outlier guard above, or null if
neither could be computed) tells you which metric actually fed the score.
How the DOM percentile handles ties. daysOnMarket is computed as a
continuous value (now - firstVisibleDate, in fractional days) rather than
a whole-day count, so exact ties are rare -- but when they happen, 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, both medians (and their reasons if null), the batch
size, the site's own resultCount for that search, and a
truncated/pagesFailed pair -- see "Coverage and dedupe" below.
Coverage and dedupe
- UK-wide, spike-verified. This actor's server-rendered-HTML approach
was live-tested against 12 cities and towns spanning all four home
nations -- Manchester, Birmingham, Leeds, Glasgow, Edinburgh, Cardiff,
Swansea, Belfast, Bristol, Newcastle-upon-Tyne, plus two small-market
probes, Inverness and Truro -- with a 100% success rate (17/17 live page
fetches returned a real, parseable listing payload). Market size varies
hugely (Manchester:
resultCount6,899; Belfast: 4), and both extremes worked identically. - Deduped across the whole run, not just within one search. A listing
is keyed by its Rightmove
idand pushed at most once per run, so overlapping searches never bill you twice for the same home or double it in the dataset. Rightmove's own pagination also overlaps by one listing between consecutive pages (confirmed live: a 24-stepindexbut 25 listings returned per page) -- the same dedupe absorbs that for free. - Failed searches never charge you. A search that can't be resolved
(an empty string, or a non-
rightmove.co.uk/ wrong-shape URL), and a search whose slug resolves but returns no usable results page at all (a misspelled town, usually), is pushed to the dataset as an uncharged{ searchInput, error }row -- the latter with a "check spelling; you can paste a rightmove.co.uk search URL" hint -- so you can see exactly what went wrong. If every search fails, or every page fetch fails, or nothing survives to be pushed, the run itself fails rather than reporting success on an empty dataset. - A blocked or challenged proxy pool stops the run, loudly. A 403, or a
CAPTCHA/anti-bot marker in the response body, aborts the entire run
immediately with a clear error rather than quietly returning empty pages
-- that's a proxy-pool problem, not a "this page happened to fail"
problem, and retrying it just burns your budget for the same answer.
Ordinary page hiccups (a soft-404 shape, an unparseable body) get one
retry, then that search's pagination stops and the batch carries whatever
it collected with
truncated: true.
Pricing
Pay-per-event: $0.70 per 1,000 listings scored ($0.0007 per
listing-scored event). Apify bills in USD regardless of the market
this actor covers, so that is the actual charge -- roughly £0.55 per
1,000 at typical USD/GBP rates, but the GBP figure is an approximation
that moves with the exchange rate, not a fixed price. Failed searches and
error rows are never charged.
Integration
curl (Apify API):
curl "https://api.apify.com/v2/acts/steadycrawl~rightmove-deal-finder/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-X POST -H "Content-Type: application/json" \-d '{"searches": ["Leeds"], "maxListingsPerSearch": 100}'
Python (apify-client):
from apify_client import ApifyClientclient = ApifyClient(APIFY_TOKEN)run = client.actor("steadycrawl/rightmove-deal-finder").call(run_input={"searches": ["Leeds"], "monthlyRentPerBedroomGBP": 600})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["dealScore"], item["displayAddress"])
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/rightmove-deal-finder like any other tool.
Limitations (transparent, by design)
- For-sale search results only, v1. No sold-comps search, no
price-drop history beyond the single
listingUpdateReason/listingUpdateDatepair the search payload itself carries. sizeSqFtis genuinely sparse. In live sampling, 28% of listings (14/50 in a Leeds batch) had no size at all -- Rightmove's owndisplaySizefield ships as an empty string on those. That's why the price-per-bedroom fallback exists as a first-class value axis, not a patch: every listing gets a value-axis score whenever it has either a size or a bedroom count, andvalueAxistells you which one fed it.- Rent yield needs your own assumption. There's no automatic market
rent estimate; supply
monthlyRentPerBedroomGBP(a per-bedroom figure, not a whole-property total) to enable that metric. propertySubTypeis Rightmove's own free-text category ("Terraced", "Semi-Detached", "Detached", "Flat", "Penthouse", ...), passed through as-is.- Architecture constraint: this actor calls exactly one Rightmove
route --
GET /property-for-sale/<slug>.html(+?index=pagination) -- and reads its embedded__NEXT_DATA__JSON. No typeahead/resolver call, no separate search API: the city/town name is the URL slug, so region resolution needs no offline lookup table or extra request. This was a deliberate architecture correction -- an earlier round of discovery targeted two JSON-API endpoints that both turned out to be dead routes (0% success); the server-rendered HTML page is the only route confirmed live.
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.0 -- initial release: server-rendered-HTML crawl with
__NEXT_DATA__extraction, UK-wide coverage (England/Scotland/Wales/NI), automatic £/sqft-or-£/bed value-axis fallback, 3-component deal score, pay-per-event pricing at $0.70/1,000 listings scored (USD -- see Pricing).