Airbnb

search-area

airbnb.com
InstallationInstalls just this skill for your agent
Side effectread
Authnone
Needsnavigate · evaluate
Summary

Search Airbnb stays by MAP BOUNDING BOX (reliable geocoding: free-text location in the URL is unreliable).

Params
{"bbox":"querystring fragment ne_lat=..&ne_lng=..&sw_lat=..&sw_lng=..","label":"place name for the slug (cosmetic)","adults":"number of adults","checkin":"YYYY-MM-DD","checkout":"YYYY-MM-DD","price_max":"max nightly USD ('' to skip)","price_min":"min nightly USD ('' to skip)"}
Returns
{"count":"number","listings":"[{id,url,name,title,price,originalPrice,priceQualifier,rating,reviews,beds,badges,lat,lng,photo}]"}
SKILL.md17 lines

Intent

Search Airbnb stays by MAP BOUNDING BOX (reliable geocoding: free-text location in the URL is unreliable). Navigates /s/{label}/homes?ne_lat&ne_lng&sw_lat&sw_lng&search_by_map=true and reads the embedded #data-deferred-state-0 SSR JSON in the real browser (StaysSearch GraphQL is blocked by rotating hashes+fingerprinting). Params: ne_lat,ne_lng,sw_lat,sw_lng (the box corners), label (cosmetic place name for the slug), checkin & checkout (YYYY-MM-DD), adults, price_min, price_max (USD, '' to skip). Returns {count, listings:[{id,url,name,title,price,originalPrice,priceQualifier,rating,reviews,beds,badges,lat,lng,photo}]}. To get a box from a place name, geocode it first (e.g. Nominatim boundingbox).

Execute

  1. navigate → https://www.airbnb.com/s/{{label|encode}}/homes?checkin={{checkin}}&checkout={{checkout}}&adults={{adults}}&price_min={{price_min}}&price_max={{price_max}}&{{bbox}}&zoom=11&search_by_map=true&currency=USD
  2. evaluate the Extractor below with params. It does the in-page work and returns the JSON in returns.

Extractor

( async (root, params) => { const el = document.querySelector('#data-deferred-state-0'); if (!el) return { count: 0, listings: [], error: 'no SSR blob (anti-bot shell)' }; let data; try { data = JSON.parse(el.textContent); } catch (e) { return { count: 0, listings: [], error: 'json parse fail' }; } let arr; try { arr = data.niobeClientData[0][1].data.presentation.staysSearch.results.searchResults || []; } catch (e) { return { count: 0, listings: [], error: 'path fail' }; } const decodeId = (b64) => { try { const s = atob(b64); const i = s.indexOf(':'); return i >= 0 ? s.slice(i + 1) : s; } catch (e) { return null; } }; const listings = arr.filter(r => r && r.demandStayListing).map(r => { const id = decodeId(r.demandStayListing.id); const pl = (r.structuredDisplayPrice && r.structuredDisplayPrice.primaryLine) || {}; const price = pl.discountedPrice || pl.price || null; const original = pl.originalPrice || null; const qualifier = pl.qualifier || null; let rating = null, reviews = null; const m = (r.avgRatingLocalized || '').match(/([\d.]+)\s*((\d+))/); if (m) { rating = parseFloat(m[1]); reviews = parseInt(m[2], 10); } const loc = (r.demandStayListing.location && r.demandStayListing.location.coordinate) || {}; const beds = ((r.structuredContent && r.structuredContent.primaryLine) || []).map(x => x.body).filter(Boolean).join(', '); const badges = (r.badges || []).map(b => b.loggingContext && b.loggingContext.badgeType).filter(Boolean); const photo = (r.contextualPictures && r.contextualPictures[0] && r.contextualPictures[0].picture) || null; return { id, url: id ? 'https://www.airbnb.com/rooms/' + id : null, name: r.subtitle || (r.nameLocalized && r.nameLocalized.localizedStringWithTranslationPreference) || null, title: r.title || null, price, originalPrice: original, priceQualifier: qualifier, rating, reviews, beds, badges, lat: loc.latitude, lng: loc.longitude, photo }; }); return { count: listings.length, listings }; } )

Success assertion

{
  "expr": "#data-deferred-state-0",
  "type": "dom"
}