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.
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
Time estimate: 15-45 minutes depending on use case complexity
Prerequisites
Submit your Claude Code skill and start earning
browse install redfin.com/get-comparable-sales-lzph4dCompatible with
Supported agents
| 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.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:
browse install redfin.com/get-comparable-sales-lzph4dFetches 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:
.cursor/skills/get-comparable-sales
Restart Cursor to activate get-comparable-sales. Access via /get-comparable-salesin your agent's command palette.
Learn more:View source on GitHubSkills CLI docsAbout CursorWhat are agent skills?
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.
find-a-hotel
hotpads.com/find-a-hotel-7v8ssy · real-estate · tag: real-estate
extract-listings
realtor.com/extract-listings-9v0y4r · real-estate · tag: real-estate
add-to-cart
kmart.com.au/kmart-irwsr8 · shopping · tag: read-only
get-smb-funding
nav.com/get-smb-funding-2s1rpm · smb-finance · tag: read-only
search-flights
google.com/search-flights-ts4g1f · travel · tag: read-only
explore-use-cases
pepperpong.com/explore-use-cases-fejudo · research · tag: read-only
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