Return recent comparable sales (sold comps) on Redfin for a subject property, with every filter dimension the Recently Sold UI exposes (recency, distance, price, beds, baths, sqft, lot, year built, property type, days-on-market, sort). Read-only.
Works with
AI-first code editor with Composer
Before installing skills in Cursor, ensure your development environment meets these requirements:
node --versionget-comparable-salesExecute the skills CLI command in your project's root directory to begin installation:
Fetches get-comparable-sales from redfin.com/get-comparable-sales-lzph4d and configures it for Cursor.
The CLI shows a list of agents. Use arrow keys and space to select Cursor:
Confirm successful installation by checking the skill directory location:
Restart Cursor to activate get-comparable-sales. Access via /get-comparable-sales in your agent's command palette.
We perform automated surface-level scans (Gen AI Scanner, Socket, Snyk) during installation. These checks detect common vulnerabilities but do not guarantee complete security. Always review skill source code and verify the publisher's reputation before production use.
Skills execute code in your environment. Always review source, verify the publisher, and test in isolation before production.
Submit your Claude Code skill and start earning
Automate repetitive workflows and reduce manual effort
Example
Generate reports, summarize documents, draft communications
Save 3-5 hours per week on routine tasks
Learn new skills, understand complex topics, get expert guidance
Example
Explain concepts, provide examples, suggest learning resources
Accelerate learning and skill development by 2x
Enhance output quality through reviews, suggestions, and refinements
Example
Review drafts, suggest improvements, catch errors
Improve work quality by 30-40% with less effort
0
total installs
0
this week
0
upvotes
Run in your terminal
0
installs
0
this week
—
stars
| name | get-comparable-sales |
| title | Redfin Comparable Sales |
| description | >- Return recent comparable sales (sold comps) on Redfin for a subject property, with every filter dimension the Recently Sold UI exposes (recency, distance, price, beds, baths, sqft, lot, year built, property type, days-on-market, sort). Read-only. |
| website | redfin.com |
| category | real-estate |
| tags | - real-estate - comps - sold-listings - appraisal - read-only - stingray-api |
| source | 'browserbase: agent-runtime 2026-05-18' |
| updated | '2026-05-18' |
| recommended_method | api |
| alternative_methods | - method: api rationale: >- Redfin's /stingray/api/gis endpoint returns the full sold-comp record (propertyId, address, lat/lon, beds/baths/sqft/lot/year/photos/MLS#/agent/price/soldDate) as JSON. All filter dimensions Redfin's Recently Sold URL exposes map cleanly to query-string params. Cookieless via Browserbase Fetch + residential proxies. 10-50x faster than browsing the rendered map UI. - method: browser rationale: >- Required only when (a) the caller provides a raw street address and propertyId resolution is needed (autocomplete endpoint is 403 cookieless), or (b) the Fetch path hits sustained rate-limit walls across the proxy pool. Use Verified + residential-proxy Browserbase session and read the gis XHR via browser-trace. - method: hybrid rationale: >- List-price-vs-sale-price delta and Redfin Estimate are NOT in the gis JSON — they require fetching each property's detail page and parsing JSON-LD + rendered HTML. Use this path only when the caller asks for list-vs-sale filtering or the subject's Redfin Estimate. |
| verified | true |
| proxies | true |
Given a subject property (Redfin URL, MLS/property ID, full street address, or lat+lon), return recent comparable sales ("comps") in the surrounding area as structured JSON. Every dimension Redfin's "Recently Sold" filter exposes is mapped to the underlying query-string keys (recency, distance, price, beds, baths, sqft, lot, year built, property type, days-on-market, sort order, pagination). The skill also returns the subject's address, lat/lon, Redfin Estimate, and most-recent sale info so the caller can frame the comp set against the subject. Read-only — never click Save, Tour, Contact Agent, Get Pre-Approved, Sign In, or any mutation control.
Redfin's web UI is a thin client over a public-but-undocumented JSON endpoint at https://www.redfin.com/stingray/api/gis?.... No auth or cookies required, but Browserbase residential proxies are mandatory — Redfin's edge throttles non-residential IPs aggressively (intermittent 403/captcha on 2–3 sequential requests from the same datacenter IP). The Fetch path (browse cloud fetch <url> --proxies) is the optimal surface; the rendered web UI is only used as a fallback when the proxy pool is exhausted, when the subject must be resolved from a raw street address (autocomplete is cookie-walled), or when the Redfin Estimate / list-price-vs-sale delta must be surfaced (those are not in the gis JSON — they only live on the property detail page).
Resolve the subject into {propertyId, regionId, regionType, market, lat, lon, zip}.
/home/. Fetch the URL with browse cloud fetch --proxies --allow-redirects and extract from the JSON-LD <script type="application/ld+json"> blocks: mainEntity.address, mainEntity.geo.{latitude,longitude}, mainEntity.numberOfBedrooms, mainEntity.numberOfBathroomsTotal, mainEntity.floorSize.value. The breadcrumb block (BreadcrumbList) carries the city's region URL /city/{regionId}/{ST}/{City} — pull {regionId} from there. (Region type for city is 6.) The Redfin Estimate is in the rendered HTML as <div class="price smallerFont">$X,XXX,XXX</div> inside id="redfin-estimate".https://www.redfin.com/CA/-/home/{id} (Redfin rewrites the slug; follow redirects), then the same JSON-LD extraction.poly= rectangle of ±delta around the point (see step 3). No region required. Pick market= from the lat/lon's metro (e.g. sanfrancisco, losangeles, seattle, boston, newyork, chicago, dallasfortworth, dc); market is a required argument on the gis endpoint and gates the sold-filter behavior./stingray/do/location-autocomplete?location=... returns 403 cookieless. The Fetch path cannot resolve a raw street address to a propertyId. Browser-fallback: use a --remote Browserbase session, navigate to https://www.redfin.com/, type the address into the search input, press Enter, capture the redirected URL — the URL contains the propertyId. Then go back to the Fetch path with {propertyId}. Many callers can avoid this by accepting a ZIP code instead (region_type=2®ion_id={zip}) and lat/lon for distance-ranking.Pick the recency window. Map the caller's sale_recency to sold_within_days:
| Caller value | sold_within_days |
|---|---|
Last 1 month | 30 |
Last 3 months | 90 |
Last 6 months | 180 |
Last 1 year | 365 |
Last 2 years | 730 |
Last 3 years | 1095 |
explicit sold_after=YYYY-MM-DD | compute days delta from today, round up to nearest preset (Redfin only honors discrete buckets) |
Pick the search area.
region_type=2®ion_id={ZIP} for ZIP-scoped, or region_type=6®ion_id={cityId} for city-scoped. Use ZIP for tight comp sets (typical appraisal radius); use city when the ZIP is sparse.poly={lon1}+{lat1},{lon2}+{lat2},{lon3}+{lat3},{lon4}+{lat4},{lon1}+{lat1} (the rectangle must close — first vertex repeated; + between lon and lat, %2C between vertices). Compute the rectangle as subjectLat ± (miles/69) and subjectLon ± (miles / (69·cos(lat))). poly requires market=<metro> to be supplied or it silently returns the entire metro's homes.poly= in the same lon+lat order.Build the request to https://www.redfin.com/stingray/api/gis?.... Required scaffolding params: al=1&v=8&start=0&page_number=1&num_homes={limit}&include_nearby_homes=true&market={metro}&mpt=13&uipt={uipt-csv}&status=9. Then layer each filter onto the query string:
| Caller filter | Query param(s) | Notes |
|---|---|---|
| Sale recency | sold_within_days={N} | Required to switch the endpoint into sold-comps mode. Without it, the same URL returns ACTIVE listings — status alone is not enough. |
| Distance / radius | poly=... rectangle (see step 3) or region_type=2®ion_id={ZIP} for "same ZIP" | Redfin does not expose a pure-radius param; emulate via bounding box. |
| Beds (min) | num_beds={N} | NOT min_beds. Aliases silently dropped. |
| Beds (max) | max_num_beds={N} | |
| Beds (exact) | num_beds=N&max_num_beds=N | |
| Baths (min, half-bath OK) | num_baths={N.0|N.5} | Half-bath increments work: num_baths=2.5. |
| Baths (max) | max_num_baths={N} | |
| Price (min) | min_price={N} | Raw dollars (no $/k/M suffix). |
| Price (max) | max_price={N} | |
| SqFt (min) | min_sqft={N} (plus min_listing_approx_size={N} — Redfin's filter page sends both) | |
| SqFt (max) | max_sqft={N} (plus max_listing_approx_size={N}) | |
| Lot size (min, sqft) | min_parcel_size={sqft} | 1 acre = 43560 sqft. |
| Lot size (max, sqft) | max_parcel_size={sqft} | |
| Year built (min) | min_year_built={YYYY} | |
| Year built (max) | max_year_built={YYYY} | |
| Property type | uipt= CSV of 1,2,3,4,5,7,8 | 1=Single-Family, 2=Condo, 3=Townhouse, 4=Multi-Family, 5=Land, 7=Mobile/Manufactured, 8=Co-op. Combine freely. |
| Days-on-market max | time_on_market_range={N}- (note trailing dash = "N or fewer") | |
| Stories (min/max) | min_stories={N}&max_stories={N} | Optional — also requires sf=1,2,3,5,6,7 (search-feature mask). |
| HOA max | No URL param. Filter client-side on home.hoa.value after fetch. | Redfin's filter UI redirects when max-hoa-fee=… is passed → confirmed unsupported on the sold-comps endpoint. |
| Sold-above/below/at list | No URL param. Filter client-side: fetch each home's property-page JSON-LD to get list price, then compute delta. | Redfin's UI does not expose this as a query filter for the sold view. |
| Has-photos toggle | No URL param. Filter client-side on home.numPictures > 0. | Redfin's URL filter has-photos redirects away → confirmed unsupported. |
| Sort: most-recent sale | ord=redfin-recommended-asc (default; "Recently Sold" page implicitly sorts by sale recency in display order) | Redfin does NOT expose a sort=newest/sold-date-desc URL key. ord=last-sale-date-desc is parsed but does NOT actually reorder — the response is identical to recommended order. To get most-recent sales first, sort client-side on home.soldDate after fetch. |
| Sort: price ↓ | ord=price-desc&sf=1,2,3,5,6,7 | |
| Sort: price ↑ | ord=price-asc&sf=1,2,3,5,6,7 | |
| Sort: $/sqft ↑/↓ | ord=dollars-per-sq-ft-asc|-desc&sf=1,2,3,5,6,7 | |
| Sort: sqft ↑/↓ | ord=square-footage-asc|-desc&sf=1,2,3,5,6,7 | |
| Sort: closest to subject | ord=distance-asc | Only meaningful when poly= is set; otherwise distance is from region centroid. |
| Limit / page size | num_homes={1..350} | Hard cap 350 per page. |
| Pagination | page_number={N}&start={(N-1)*num_homes} | Both required; otherwise Redfin returns page 1. |
Fetch & decode:
browse cloud fetch "https://www.redfin.com/stingray/api/gis?<query>" --proxies
The response body starts with the XSSI prefix {}&& — strip the first 4 bytes before parsing as JSON. Then read:
payload.originalHomes.homes[] — primary results (homes inside the requested region/poly).payload.nearbyHomes.homes[] — expanded radius (populated when include_nearby_homes=true and the inner result set is sparse).payload.nearbyHomeDistance — miles of the expanded ring (typically 1.0).payload.originalHomes.searchMedian — region medians: {price, sqFt, pricePerSqFt, beds, baths}.payload.originalHomes.gisHomesQueryId — handle for the request (helpful for caching/replay).homes.length when < num_homes was requested. To get an exact total, request num_homes=350 and check if a 2nd page returns more; iterate until empty. (gis-aggregates exists but its payload is empty for sold queries — confirmed.)Decode each home. Every homes[i] follows the same shape:
propertyId (Redfin's stable ID), listingId (per-listing), mlsId.value (MLS#), url (relative path; prepend https://www.redfin.com).streetLine.value, unitNumber.value, city, state, zip, postalCode.value, latLong.value.{latitude,longitude}, countryCode.price.value (USD raw int — this is the SOLD price for sold-status homes), pricePerSqFt.value, soldDate (ms epoch — divide by 1000 for seconds, format as ISO). mlsStatus is "Sold" | "Closed" | "Closed Sale"; searchStatus=4 means sold.beds, baths (decimal — 2.5 = 2 full + 1 half), fullBaths, sqFt.value, lotSize.value (sqft), yearBuilt.value, stories, propertyType (raw MLS code — varies by source), uiPropertyType (Redfin's normalized bucket: 1=SFR, 2=Condo, 3=Townhouse, 4=Multi-Family, 5=Land, 6/7=Mfd, 8=Co-op).hoa.value (monthly $; absent or {level:1} with no value ⇒ no HOA or undisclosed). isHoaFrequencyKnown.photos.value is a compact spec like "0-36:2" meaning photos 0–36, format spec :2. numPictures is the count. photoFormat is "webp" or "jpg". Construct the primary photo URL as: https://ssl.cdn-redfin.com/photo/{dataSourceId}/mbphoto/{last3OfListingId}/genMid.{mlsId}_0.jpg (variants: bigphoto, mbpaddedwide, bcsphoto — bigphoto is highest res). additionalPhotosInfo[] is usually empty in the list view — to get every photo URL, fetch the property page and parse <img> srcs.sellingBroker.name, sellingAgent.name, sellingAgent.redfinAgentId, sellingBroker.isRedfin. (For sold-on-Redfin listings only; absent for off-MLS or stale records.)dom.value (when level≥2 — gated by access level). Empty dom: {level: 1} means Redfin hasn't surfaced it to the caller; fall back to the property page.timeOnRedfin, timeZone, listingTags[] (highlight bullets), listingRemarks (MLS description), sashes[] (UI badges; sashTypeName=="Bought" confirms a sold-with-Redfin transaction with lastSaleDate populated).List-price-vs-sale delta (when caller requests it) — NOT in the gis JSON. For each home you want this for, fetch https://www.redfin.com/{home.url} with --proxies, extract offers.price from the JSON-LD block — that's the closing/list price displayed on the detail page. The MLS public-record sometimes shows originalListPrice separately in the property's "Sale & Tax History" table (further down the HTML — parse a row labelled "Listed"); subtract from price.value for the delta. Each property-page fetch costs roughly the same as one gis request — only do this when truly needed (e.g. when the caller filtered on sold_above_list/sold_below_list/sold_at_list).
Paginate when homes.length === num_homes (likely more results). Bump page_number and recompute start=(page-1)*num_homes. Stop when a page returns fewer than num_homes.
Subject framing. Attach to the response: {address, latLong, redfinEstimate, lastSale: {price, date}, url} extracted from step 1. If the caller passed lat/lon and you used a poly= search (no propertyId), set subject.address = null and report {lat, lon, radius_miles} only.
Read-only enforcement. Do NOT call any /stingray/do/* POST endpoint (Save, Tour, Schedule, Submit Offer). All required data is GET-only. If the caller asks for "comps for [property] + book a tour", refuse the second clause.
Only when the Fetch path fails (sustained 403s across the proxy pool, or you must resolve a raw street address):
sid=$(browse cloud sessions create --keep-alive --proxies --verified | python -c "import sys,json;print(json.load(sys.stdin)['id'])")
export BROWSE_SESSION="$sid"
browse open "https://www.redfin.com/" --remote → type the address into the search input → press Enter → capture the redirected URL with browse get url. The URL contains the propertyId.browse open "https://www.redfin.com/city/{regionId}/{ST}/{City}/filter/property-type=house,min-beds=3,max-beds=4,min-baths=2,min-price=500k,max-price=2M,include=sold-3mo" --remote and read the network panel via the browser-trace skill — the gis XHR is the same endpoint as the Fetch path. If you can extract the XHR URL, switch back to Fetch.browse cloud sessions update "$sid" --status REQUEST_RELEASE when done./stingray/api/* JSON response is prefixed with the literal bytes {}&& — strip the first 4 bytes before JSON.parse. Forgetting this is the #1 cause of "the API returned garbage."status=9 alone does NOT restrict to sold homes — even though Redfin's recently-sold UI URL shows status=9, the operative param is sold_within_days={N}. Without it, the same URL returns ACTIVE listings. status becomes a no-op once sold_within_days is set; you can pass status=1 or omit it and get the same sold results. Verified during iteration: status=1/2/4/8/16 with sold_within_days=180 all returned identical sold-only result sets.market=<metro> is required when using poly=. Without it, poly is silently ignored and you get whole-metro homes. The market enum is the lowercase metro slug: sanfrancisco, losangeles, seattle, chicago, newyork, boston, dc, dallasfortworth, houston, phoenix, atlanta, miami, portland, denver, philadelphia, etc. You can read it from the property page JSON-LD or from the breadcrumb URL (city URL contains the metro). When you only have a region_id, omit poly entirely and the region-based scoping works without market strictness.num_beds/max_num_beds/num_baths/max_num_baths/min_parcel_size/max_parcel_size/min_year_built/max_year_built/min_listing_approx_size/max_listing_approx_size. The "natural" aliases (min_beds, max_beds, min_lot_size, min_baths, min_sqft) are accepted by the server with HTTP 200 but silently ignored — you get unfiltered results. Always use the canonical names enumerated in step 4.min_sqft AND min_listing_approx_size are both sent by the official UI for the same min-sqft filter. Either alone works; the UI passes both for redundancy. Same for max_sqft/max_listing_approx_size.last-sale-date-desc is parsed but a no-op. The response is identical to redfin-recommended-asc. Same for sold-date-desc (rejected as "Invalid arguments") and closest (rejected). To deliver "Most recent sale" sort, sort the returned homes client-side on soldDate desc.num_homes caps at 350 per page. Higher values are accepted but the response still returns ≤350.page_number and start: page_number=2&start=350 (for num_homes=350). Passing only page_number returns page 1.region_type enum: 1=State, 2=ZIP code, 5=County, 6=City, 12=School district. For comps you almost always want 2 (ZIP, tight) or 6 (city, broad). For a ZIP, region_id is the 5-digit ZIP itself (e.g. region_type=2®ion_id=94110); for a city, look up the numeric Redfin city ID from the breadcrumb URL — there is no public city-id resolver beyond the breadcrumb.mpt (map-page-type) matters: mpt=13 = sale-search-map (homepage map), mpt=99 = filter-page-map (used by /city/.../filter/... pages). Either works for the gis endpoint, but mpt must be present (omitting it returns 0 homes in some configurations). Default to mpt=13.uipt (UI Property Type) is the correct property-type filter — NOT propertyType. propertyType is the raw MLS source code (varies wildly by MLS), uiPropertyType is Redfin's normalized 1–8 enum. The URL filter is uipt=.https://www.redfin.com/stingray/do/location-autocomplete?location=... returns 403 from cookieless Fetch even with residential proxy. To resolve a raw street address to a propertyId, the only working path is a full browser session (see Browser fallback). Always prefer accepting {propertyId, ZIP, or lat+lon} from the caller and skipping address resolution.belowTheFold is 403 cookieless: https://www.redfin.com/stingray/api/home/details/belowTheFold?... returns 403. Subject-property enrichment beyond JSON-LD requires the rendered property page HTML (parse id="redfin-estimate" div for the Redfin Estimate, parse the "Sale & Tax History" table for prior list/sold prices).soldDate is in milliseconds-since-epoch (not seconds). Divide by 1000 before passing to datetime.fromtimestamp. The value is the closing date (deed-of-trust filing), not the offer-accepted date.mlsStatus values for sold listings vary by MLS: "Sold" | "Closed" | "Closed Sale". Treat all three as "sold." Use searchStatus === 4 for a clean integer check.baths is decimal, fullBaths is an int, but Redfin does not expose halfBaths directly — compute as Math.round((baths - fullBaths) * 2).dom (days on market) is access-gated. For sold homes it's often surfaced only at level≥2 (Redfin sign-in). The cookieless Fetch path frequently returns {level: 1} (no value). When you need DOM, fetch the property page and parse the "Sale & Tax History" Days on Market field, OR compute soldDate - listDate from the same table.dataSourceId: dataSourceId 8 (MLSListings/Bay Area) → bigphoto and mbpaddedwide directories with ML{mlsId}_{N}.jpg. dataSourceId 10 (BAREIS) → mbphoto/bcsphoto with genMid.{mlsId}_{N}.jpg. Don't hardcode the directory — extract from a sample URL on the property page and use the discovered pattern. numPictures tells you the upper bound on _{N}.listingRemarks access-gated: cookieless Fetch may return a truncated description with remarksAccessLevel: 1. Full remarks need the property page.include_nearby_homes=true expands the result set when the inner region/poly has few hits — nearbyHomes.homes[] is populated with comps from a wider 1-mile ring. Set include_nearby_homes=false if the caller wants a strict in-region cut.--proxies is set — single-shot queries are reliable; sustained scraping is not.poly rectangle format: lon1+lat1,lon2+lat2,lon3+lat3,lon4+lat4,lon1+lat1 with lon and lat space-separated within each vertex (URL-encoded space = +), and %2C between vertices. The polygon must close (first vertex repeated). Order is longitude first, then latitude — reversing them returns 0 homes silently. Without market= the param is silently dropped.payload.numHomesOnServer, totalUnclusteredHomes, and originalHomesCount are all null in the sold-comps response shape. To emit "X homes matching" you must paginate until exhaustion and sum, or accept "≥N" semantics.gis-aggregates returns empty payload: {} for sold-status queries — don't waste a request on it.sold_above_list/sold_below_list/sold_at_list, fetch the raw set (no list/sale filter), then for each home pull its property page JSON-LD offers.price (list/last price) and compute delta = soldPrice - listPrice; partition client-side. Cost-of-each-extra-property-page is ~1 fetch per comp.<div class="price smallerFont">$X,XXX,XXX</div> inside id="redfin-estimate". Regex out the dollar string.Prerequisites
Time Estimate
15-45 minutes depending on use case complexity
Steps
Common Pitfalls
✓ Do
✗ Don't
💡 Pro Tips
✓ Use when
Use when skill capabilities match your task, clear ROI on time saved, and you can validate outputs. Best for repetitive tasks, learning, and quality improvement.
✗ Avoid when
Avoid when task requires deep expertise you can't validate, involves sensitive decisions, or when learning process is more valuable than speed of completion.
booking.com/search-hotels-asq6cc
aliexpress.com/search-product-p0h8a7
nav.com/get-smb-funding-2s1rpm
1688.com/search-wholesale-l33zbe
ruwangi.com/ruwangi-parfum-laki-laki-ko0z5t
google.com/search-flights-ts4g1f
Keeps context tight: get-comparable-sales is the kind of skill you can hand to a new teammate without a long onboarding doc.
Registry listing for get-comparable-sales matched our evaluation — installs cleanly and behaves as described in the markdown.
We added get-comparable-sales from the explainx registry; install was straightforward and the SKILL.md answered most questions upfront.
get-comparable-sales has been reliable in day-to-day use. Documentation quality is above average for community skills.
Useful defaults in get-comparable-sales — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
I recommend get-comparable-sales for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
get-comparable-sales reduced setup friction for our internal harness; good balance of opinion and flexibility.
get-comparable-sales fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
get-comparable-sales has been reliable in day-to-day use. Documentation quality is above average for community skills.
get-comparable-sales reduced setup friction for our internal harness; good balance of opinion and flexibility.
showing 1-10 of 69