{"id":"fd59759c-7dae-43b5-84f6-dc4875defc96","entityType":"agent","slug":"clawhub-chrischall-homes-fpx","name":"homes-fpx","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-homes-fpx","canonicalPath":"/agent/clawhub-chrischall-homes-fpx","generatedAt":"2026-10-10T11:53:36.075Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":null},"description":"Query homes.com (US real-estate portal) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the homes-mcp server — search listings, resolve street addresses, fetch property detail/photos/ history, and read the signed-in user's saved homes, all through a one-shot call over their own signed-in browser tab. Use when you want homes.com data without the MCP, in a script, or on a machine where the MCP isn't installed.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.5K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:homes-fpx","sourceUrl":"https://clawhub.ai/chrischall/homes-fpx","homepage":"https://clawhub.ai/chrischall/skills/homes-fpx","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/homes-fpx","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/homes-fpx","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":64,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"homes-fpx technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":null},"stars":null,"forks":null,"downloads":1540,"packageName":null,"latestVersion":"2.1.10","tractionLabel":"1.5K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T09:01:48.163Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T09:01:48.217Z","lastCrawledAt":"2026-10-10T09:01:48.163Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T09:01:48.163Z","lastVerifiedAt":null,"highlights":[{"version":"2.1.10","createdAt":"2026-10-09T23:24:30.735Z","changelog":"- Removed the sample skill card file (skill-card.md) from the repository. - No changes to functionality or documentation apart from the file removal.","fileCount":4,"zipByteSize":9762},{"version":"2.1.9","createdAt":"2026-10-07T13:40:46.971Z","changelog":"- Removed the file: skill-card.md. - No changes to core functionality or user-facing documentation in this update.","fileCount":4,"zipByteSize":9761},{"version":"2.1.8","createdAt":"2026-10-05T02:49:28.802Z","changelog":"- Removed the skill-card.md file. - No functional or documented user-facing changes.","fileCount":4,"zipByteSize":9846},{"version":"2.1.7","createdAt":"2026-10-03T01:46:06.618Z","changelog":"- Removed the file skill-card.md. - No changes to user-facing documentation or functionality.","fileCount":4,"zipByteSize":9779},{"version":"2.1.6","createdAt":"2026-09-28T13:55:53.119Z","changelog":"- Replace all references to \"Transporter\" browser extension with \"ContextMint Bridge\". - Update setup instructions to reflect \"ContextMint Bridge\" installation, including link to releases, installation notes, and verification steps. - Note that Safari is not yet supported (extension will ship later); Chrome should be used for now. - Remove the file skill-card.md.","fileCount":4,"zipByteSize":9780},{"version":"2.1.5","createdAt":"2026-09-25T15:51:23.699Z","changelog":"- Removed the file skill-card.md. - No changes to functionality or usage. This is a housekeeping update.","fileCount":4,"zipByteSize":9497},{"version":"2.1.4","createdAt":"2026-09-24T15:05:34.905Z","changelog":"- Removed the obsolete skill-card.md file for cleanup. - No changes to core functionality or documentation.","fileCount":4,"zipByteSize":9730},{"version":"2.1.3","createdAt":"2026-09-23T21:41:24.535Z","changelog":"- Removed the skill-card.md file. - No changes to functionality or documentation content.","fileCount":4,"zipByteSize":9734}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:homes-fpx","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:homes-fpx` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/chrischall/homes-fpx before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T11:53:36.072Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-homes-fpx/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":null},"readme":"Skill: homes-fpx\n\nOwner: chrischall\n\nSummary: Query homes.com (US real-estate portal) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the homes-mcp server — search listings, resolve street addresses, fetch property detail/photos/ history, and read the signed-in user's saved homes, all through a one-shot call over their own signed-in browser tab. Use when you want homes.com data without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:2.1.10\n\nVersion history:\n\nv2.1.10 | 2026-10-09T23:24:30.735Z | auto\n\n- Removed the sample skill card file (skill-card.md) from the repository.\n- No changes to functionality or documentation apart from the file removal.\n\nv2.1.9 | 2026-10-07T13:40:46.971Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to core functionality or user-facing documentation in this update.\n\nv2.1.8 | 2026-10-05T02:49:28.802Z | auto\n\n- Removed the skill-card.md file.\n- No functional or documented user-facing changes.\n\nv2.1.7 | 2026-10-03T01:46:06.618Z | auto\n\n- Removed the file skill-card.md.\n- No changes to user-facing documentation or functionality.\n\nv2.1.6 | 2026-09-28T13:55:53.119Z | auto\n\n- Replace all references to \"Transporter\" browser extension with \"ContextMint Bridge\".\n- Update setup instructions to reflect \"ContextMint Bridge\" installation, including link to releases, installation notes, and verification steps.\n- Note that Safari is not yet supported (extension will ship later); Chrome should be used for now.\n- Remove the file skill-card.md.\n\nv2.1.5 | 2026-09-25T15:51:23.699Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or usage. This is a housekeeping update.\n\nv2.1.4 | 2026-09-24T15:05:34.905Z | auto\n\n- Removed the obsolete skill-card.md file for cleanup.\n- No changes to core functionality or documentation.\n\nv2.1.3 | 2026-09-23T21:41:24.535Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or documentation content.\n\nv2.1.2 | 2026-09-23T15:41:16.920Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation in SKILL.md.\n- This release is a minor cleanup with no user-facing changes.\n\nv2.1.1 | 2026-09-21T16:35:48.575Z | auto\n\n- Removed the file: skill-card.md (no longer included in the project)\n- No functional or documentation changes within SKILL.md\n- No changes to user-facing features or usage\n\nv2.1.0 | 2026-09-20T02:50:21.297Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing features or functionality changed in this release.\n\nv2.0.0 | 2026-09-17T17:51:12.461Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No user-facing or functional changes; documentation structure remains consistent.\n\nv1.4.5 | 2026-09-15T18:26:15.148Z | auto\n\n- Removed the sample skill card file (skill-card.md) from the project.\n- No changes to functionality or core documentation.\n\nv1.4.4 | 2026-09-14T19:58:39.886Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No functional or user-facing changes.\n\nv1.4.3 | 2026-09-10T17:49:44.020Z | auto\n\n- Removed the skill card documentation file (skill-card.md).\n- No user-facing changes or functional updates.\n\nv1.4.2 | 2026-09-09T21:16:02.026Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to user-facing features or functionality.\n\nv1.4.1 | 2026-09-05T00:51:13.688Z | auto\n\n- Removed the unused file: skill-card.md.\n- No functional or user-facing changes in this release.\n\nv1.4.0 | 2026-09-04T22:20:35.501Z | auto\n\nhomes-fpx 1.4.0\n\n- Removed the file skill-card.md.\n- No changes made to user-facing documentation or functionality.\n\nv1.3.0 | 2026-09-02T23:26:55.053Z | auto\n\n- Removed the redundant file skill-card.md.\n- No changes to CLI usage, features, or API.\n- Documentation and user experience remain the same.\n\nv1.2.0 | 2026-08-29T13:54:07.417Z | auto\n\n- Removed the sample skill card file (skill-card.md) for a leaner repository.\n- No changes to core functionality or user-facing documentation.\n\nv1.1.5 | 2026-08-28T21:07:18.549Z | auto\n\n- Removed the file skill-card.md.\n- No functional changes; documentation and all features remain unchanged.\n\nv1.1.4 | 2026-08-28T11:34:44.505Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation in SKILL.md.\n- Internal cleanup; no user-facing impact.\n\nv1.1.3 | 2026-08-06T00:42:57.937Z | auto\n\n- Removed the redundant skill-card.md file.\n- No changes to functionality or end-user documentation.\n\nv1.1.2 | 2026-07-30T12:53:45.444Z | auto\n\n- Now supports querying homes.com using the fpx CLI, eliminating the need for the homes-mcp server.\n- Enables searching listings, resolving street addresses, fetching property details/photos/history, and accessing the signed-in user's saved homes directly through your own browser session.\n- Uses the Transporter browser extension to route all requests through an active, signed-in homes.com tab, bypassing AWS WAF challenges.\n- Provides extraction recipes for search, property detail, typeahead, market reports, saved homes/searches, and other endpoints.\n- Detailed setup instructions and ready-to-use shell command examples included in the documentation.\n\nArchive index:\n\nArchive v2.1.10: 4 files, 9762 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2003b), SKILL.md (5569b), _meta.json (129b)\n\nFile v2.1.10:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.10:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.10\",\n  \"publishedAt\": 1791588270735\n}\n\nFile v2.1.10:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.10:skill-card.md\n\n## Description:\n\nGuides agents in retrieving homes.com listings, property details, and signed-in saved homes or searches through a paired browser session using fpx.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to search US real-estate listings, resolve addresses, inspect property details and history, and access a signed-in user's saved homes or searches without the homes-mcp server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Accessing saved homes or searches can expose private account-specific real-estate interests.\n\nMitigation: Confirm the user wants this access before running saved-data recipes, and avoid retaining or sharing fetched HTML or JSON unnecessarily.\n\nRisk: Address resolution can return a nearby property rather than an exact match.\n\nMitigation: Check the resolved property's street address against the requested address before using its details.\n\n## Reference(s):\n\n- [homes-fpx release on ClawHub](https://clawhub.ai/chrischall/skills/homes-fpx)\n- [homes.com request recipes](artifact/references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell, JavaScript, and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Offers recipes for public listing data and signed-in saved homes or searches; results depend on the paired browser session.]\n\n## Skill Version(s):\n\n2.1.10 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.9: 4 files, 9761 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (1997b), SKILL.md (5569b), _meta.json (128b)\n\nFile v2.1.9:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.9:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.9\",\n  \"publishedAt\": 1791380446971\n}\n\nFile v2.1.9:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.9:skill-card.md\n\n## Description:\n\nGuides agents in querying US homes.com listings, property details, and saved account data through the user's paired browser session without a dedicated MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents, developers, and home shoppers use this skill to search US listings, inspect property details and history, resolve addresses, and retrieve their own saved homes or searches using a paired homes.com browser tab.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Pairing the browser bridge with a signed-in homes.com tab allows access to private saved homes and searches.\n\nMitigation: Pair only when comfortable with this access; request saved account data only when needed and do not store or share it without explicit consent.\n\nRisk: Address search may suggest a nearby property rather than the requested address.\n\nMitigation: Verify the returned street address before using or reporting property details.\n\n## Reference(s):\n\n- [homes.com request recipes](references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n- [homes-fpx ClawHub release](https://clawhub.ai/chrischall/skills/homes-fpx)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Text]\n\n**Output Format:** [Markdown with shell commands and extracted listing or property data]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Results depend on an available paired browser tab and current homes.com page content.]\n\n## Skill Version(s):\n\n2.1.9 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.8: 4 files, 9846 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2229b), SKILL.md (5569b), _meta.json (128b)\n\nFile v2.1.8:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.8\",\n  \"publishedAt\": 1791168568802\n}\n\nFile v2.1.8:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.8:skill-card.md\n\n## Description:\n\nHelps agents search U.S. homes.com listings, retrieve property details and history, and read a user's saved homes or searches through their signed-in browser tab.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and developers use this skill to find U.S. property listings, look up addresses, and summarize listing details, photos, and history. With explicit permission, they can also retrieve a user's saved homes and searches.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Saved homes and searches expose account-linked information from the signed-in browser session.\n\nMitigation: Fetch saved items only when the user explicitly asks; confirm the browser pairing and avoid sharing or retaining account-linked results unnecessarily.\n\nRisk: Address lookup can return a nearby property rather than the requested address.\n\nMitigation: Check the selected property's street address against the requested address before presenting it as a match.\n\nRisk: Fetching listings through a signed-in browser session may exceed the user's intended site usage.\n\nMitigation: Keep requests within homes.com's terms and use the paired session only with the user's consent.\n\n## Reference(s):\n\n- [homes.com request recipes](references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n- [homes.com](https://www.homes.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell commands and structured property data examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Property results may include listing links, photos, prices, and history; saved items are account-linked.]\n\n## Skill Version(s):\n\n2.1.8 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.7: 4 files, 9779 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2079b), SKILL.md (5569b), _meta.json (128b)\n\nFile v2.1.7:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.7\",\n  \"publishedAt\": 1790991966618\n}\n\nFile v2.1.7:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.7:skill-card.md\n\n## Description:\n\nGuides agents in querying U.S. homes.com listings, property details, and optionally a signed-in user's saved homes through a paired browser tab using fpx.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and developers use this skill to prepare shell commands for searching U.S. homes.com listings, resolving addresses, inspecting property details, and accessing saved homes or searches when explicitly requested.\n\n### Deployment Geography for Use:\n\nGlobal (U.S. real-estate listings)\n\n## Known Risks and Mitigations:\n\nRisk: Saved homes and searches may reveal private account information.\n\nMitigation: Request saved-account recipes only when needed, and avoid logging or sharing their results.\n\nRisk: Pairing fpx with a browser tab grants access through that session.\n\nMitigation: Verify the npm package and browser extension source before setup, and pair only if comfortable with this access.\n\nRisk: Address resolution may return a nearby property rather than an exact match.\n\nMitigation: Confirm the candidate's street address before using property details.\n\n## Reference(s):\n\n- [homes-fpx on ClawHub](https://clawhub.ai/chrischall/skills/homes-fpx)\n- [homes.com request recipes](references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell and JavaScript examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands require a paired homes.com browser tab; saved-account queries require sign-in.]\n\n## Skill Version(s):\n\n2.1.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.6: 4 files, 9780 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2129b), SKILL.md (5569b), _meta.json (128b)\n\nFile v2.1.6:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.6\",\n  \"publishedAt\": 1790603753119\n}\n\nFile v2.1.6:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.6:skill-card.md\n\n## Description:\n\nHelps agents search homes.com listings, inspect property details, and access saved homes and searches through the user's browser session.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and developers use this skill to find US real-estate listings, look up property details and history, and retrieve a signed-in user's saved homes or searches without running the homes-mcp server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Access to saved homes and searches uses the user's signed-in homes.com session and may expose account-specific data.\n\nMitigation: Run saved-homes or saved-searches commands only when the user intends to access that data; handle results carefully.\n\nRisk: The skill relies on an external CLI and browser extension paired with the user's browser session.\n\nMitigation: Review the fpx CLI and ContextMint Bridge extension before installing and pairing them.\n\nRisk: Address lookup may return a nearby property rather than the requested one.\n\nMitigation: Verify each resolved address against the requested address before using property details.\n\n## Reference(s):\n\n- [homes-fpx ClawHub release](https://clawhub.ai/chrischall/skills/homes-fpx)\n- [homes.com request recipes](references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell commands and data-extraction examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include listing data or account-specific saved homes and searches.]\n\n## Skill Version(s):\n\n2.1.6 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.5: 4 files, 9497 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (1866b), SKILL.md (5195b), _meta.json (128b)\n\nFile v2.1.5:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the Transporter extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** browser extension installed, with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.5\",\n  \"publishedAt\": 1790351483699\n}\n\nFile v2.1.5:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.5:skill-card.md\n\n## Description:\n\nHelps agents query homes.com listings, property details, and signed-in saved homes or searches through the user's paired browser tab without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to search US real-estate listings, inspect property information, and retrieve saved homes or searches when the user authorizes access to a signed-in homes.com tab.\n\n### Deployment Geography for Use:\n\nUnited States\n\n## Known Risks and Mitigations:\n\nRisk: A paired signed-in browser tab can expose private saved homes and saved searches.\n\nMitigation: Pair only if comfortable granting access to the signed-in tab, and retrieve saved account data only when specifically requested.\n\nRisk: Address lookup can return a nearby property rather than the requested one.\n\nMitigation: Confirm the returned street address before presenting or relying on property details.\n\n## Reference(s):\n\n- [Homes FPX release](https://clawhub.ai/chrischall/skills/homes-fpx)\n- [Homes.com request recipes](references/homes-requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Text, JSON]\n\n**Output Format:** [Markdown instructions and shell examples; retrieved results can be extracted as text or JSON]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Saved homes and searches require an authorized signed-in browser session.]\n\n## Skill Version(s):\n\n2.1.5 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.4: 4 files, 9730 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2456b), SKILL.md (5195b), _meta.json (128b)\n\nFile v2.1.4:SKILL.md\n\n---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the Transporter extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** browser extension installed, with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json\n```\n\nThe one non-HTML endpoint (address typeahead) is a real JSON POST — no\nextraction step, pipe straight to `jq`:\n\n```sh\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\nReady-to-run request/extraction recipes for every endpoint — search,\nproperty detail, photos, history/tax, nearby, market report, saved\nhomes/searches, and the typeahead — are in\n`references/homes-requests.md`.\n\n## The one rule: resolve before you fetch detail\n\nIf you only have a free-text address (not a `/property/<slug>/<hash>/`\nURL), resolve it first — same three-rung order `homes_get_by_address`\nuses:\n\n1. **Typeahead** (`POST /routes/res/consumer/smartsearch/autocomplete/`)\n   — the primary rung; returns the real detail URL directly.\n2. **Slug** (`GET /<address-city-state-zip-slug>/`) — homes.com often\n   routes an unambiguous address straight to the detail page.\n3. **Search fallback** (`GET /<city-slug>/`, fuzzy-match the street) —\n   only when 1 and 2 miss.\n\nSee `references/homes-requests.md` for the exact body/path shapes and a\n`jq` street-match recipe. Verify whatever candidate you pick against the\naddress you asked for — homes.com will happily return the \"closest\"\nresult, not a confirmed match.\n\n## Auth-gated pages\n\n`homes_get_saved_homes` / `homes_get_saved_searches` need a signed-in\ntab. A missing session shows up as either:\n\n- a redirect to `/sign-in`, or\n- the AWS WAF challenge interstitial (body contains both `awswaf.com`\n  and `challenge.js`, and is under ~80 KB).\n\nIf you see either, open `www.homes.com` in the browser tab fpx is\npaired to, sign in / clear the challenge, and retry.\n\n## Output & exit codes (fetch verbs)\n\n- `0` — success (still check the body — an empty JSON-LD match or a\n  sign-in redirect can ride a `200`).\n- `2` — bridge unavailable: extension not connected or pairing pending\n  → `fpx pair -p homes`, confirm a `www.homes.com` tab is open.\n- `3` — bot wall: the tab hasn't cleared the AWS WAF challenge → open/\n  refresh a `www.homes.com` tab and retry.\n- `4` — upstream non-2xx from homes.com.\n- `fpx health -p homes` shows bridge connection state when a call fails.\n\n## Notes\n\n- No account data beyond the saved-homes/searches pages — everything\n  else is public listing data. Stay within homes.com's terms.\n- This project is developed and maintained by AI (Claude).\n\nFile v2.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.4\",\n  \"publishedAt\": 1790262334905\n}\n\nFile v2.1.4:references/homes-requests.md\n\n# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragment` first — `@id` carries a `#realestatelisting` fragment,\n`url` doesn't):\n\n```sh\njq -r '.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") |\n  .mainEntity.itemListElement[].url' /tmp/jsonld.json \\\n  | sed -E 's#[?#].*$##; s#/$##' | sed -E 's#.*/##'\n```\n\n**Cap:** homes.com SSRs ~40 listings per page even when\n`mainEntity.numberOfItems` reports more — band by price / sub-area to\nenumerate a busy market.\n\nSold/market-report page is the same shape at `/<slug>/sold/`; derive\nmedian price + avg $/sqft yourself:\n\n```sh\nfetch_jsonld 'https://www.homes.com/brooklyn-ny/sold/' > /tmp/jsonld.json\njq '[.[\"@graph\"][] | select(.[\"@type\"]==\"CollectionPage\") | .mainEntity.itemListElement[].offers.price]\n    | sort | { count: length, median: .[length/2 | floor] }' /tmp/jsonld.json\n```\n\n---\n\n## 2. Property detail\n\n`GET /property/<address-slug>/<propertyId>/`\n\n```sh\nfetch_jsonld 'https://www.homes.com/property/3199-delmar-ln-nw-atlanta-ga/rxrzwg0kjnr32/' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"][0]? == \"RealEstateListing\" or (.[\"@type\"] | index(\"RealEstateListing\")))' /tmp/jsonld.json\n```\n\n```sh\njq '.[\"@graph\"][] | select(.[\"@type\"] | index(\"RealEstateListing\")) | {\n  url,\n  name,\n  address: .mainEntity.address,\n  lat: .mainEntity.geo.latitude,\n  lng: .mainEntity.geo.longitude,\n  beds: .mainEntity.numberOfBedrooms,\n  baths: .mainEntity.numberOfBathroomsTotal,\n  sqft: .mainEntity.floorSize.value,\n  year_built: .mainEntity.yearBuilt,\n  price: .offers.price,\n  status: .offers.availability,\n  date_posted: .datePosted,\n  date_modified: .dateModified,\n  agent: (.offers.offeredBy[0] // .offers.offeredBy)\n}' /tmp/jsonld.json\n```\n\n`geo` (lat/lng) is **only** on the detail page — search-page items lack\nit.\n\n### DOM-only fields (not in JSON-LD)\n\nhomes.com also renders highlights, HOA fee, lot size, parking,\nutilities, MLS id/source, tax, schools, estimated payment, and total\nviews as plain sectioned text on the same page (`src/tools/\nproperties.ts::extractDomFields`). A quick grep over the raw HTML body\ntext gets you most of it without a full DOM parse:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const text = html.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \");\n  const grab = (re) => (re.exec(text) || [])[1];\n  console.log(JSON.stringify({\n    hoa: grab(/HOA Fee:\\s*\\$?([0-9,]+|0)/i),\n    mls_id: grab(/MLS#?:?\\s*([A-Z0-9-]+)/i),\n    tax: grab(/(?:Annual Tax|Property Tax|Tax(?:es)?)(?: Amount)?:?\\s*\\$?([0-9,]+)/i),\n    estimated_payment: grab(/Estimated payment\\s*\\$?([0-9,]+)/i),\n    total_views: grab(/Total Views\\s*([0-9,]+)/i),\n  }, null, 2));\n'\n```\n\nFor anything structural (highlights `<li>` list, schools list,\nmatterport/floorplan `<img>`/`<a>` URLs) you need real DOM traversal —\neither `npm install node-html-parser` and mirror\n`src/tools/properties.ts::extractDomFields`, or just call\n`homes_get_property` on the running MCP for full parity.\n\n---\n\n## 3. Photo gallery\n\nSame detail page — JSON-LD carries only one photo, so scrape `<img>`\ntags and filter to the homes.com CDN, deduping by `src`:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<img\\b[^>]*\\bsrc=\"([^\"]+)\"[^>]*>/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const src = m[1];\n    if (!src.includes(\"homes.com\") || src.startsWith(\"data:\") || seen.has(src)) continue;\n    seen.add(src);\n    out.push(src);\n  }\n  console.log(JSON.stringify(out.map((url, i) => ({ url, position: i + 1 })), null, 2));\n'\n```\n\n---\n\n## 4. Address typeahead (structured — the ONE JSON API)\n\n`POST /routes/res/consumer/smartsearch/autocomplete/`, `Content-Type:\napplication/json`. Neither the XSRF nor AT headers the live search box\nsends are required — a plain JSON POST returns 200.\n\n```sh\ncat > /tmp/body.json <<'EOF'\n{\n  \"term\": \"158 raven blvd lake lure\",\n  \"fullTerm\": \"158 Raven Blvd Lake Lure\",\n  \"transactionType\": 1,\n  \"searchTermStartIndex\": null\n}\nEOF\nfpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'\n```\n\n`term` (lowercased, load-bearing) + `fullTerm` = the joined\n`{address, city, state, zip}`, space-separated. `transactionType: 1` =\nfor-sale (mirrors the live box; not required for a 200).\n\nResponse shape — each place:\n\n```json\n{\n  \"n\": \"158 Raven Blvd, Lake Lure, NC\",\n  \"u\": \"/property/158-raven-blvd-lake-lure-nc/yhepckbpqstf1/\",\n  \"g\": { \"k\": { \"key\": \"yhepckbpqstf1\" },\n         \"a\": { \"state\": \"NC\", \"city\": \"Lake Lure\", \"postalCode\": \"28746\",\n                \"street\": \"158 Raven Blvd\", \"unit\": null } }\n}\n```\n\n`u` is the real detail-page path — resolve straight from it, no further\nlookup needed. `g.k.key` is the same opaque property hash as the URL's\ntrailing segment. A nonexistent address returns `places: []`.\n\n```sh\njq -r '.suggestions.places[] | [.u, .g.a.street, .g.a.city, .g.a.state] | @tsv' /tmp/response.json\n```\n\n**Resolution order** (mirrors `homes_get_by_address`): try this\ntypeahead first; if it misses, `GET /<address-city-state-zip-slug>/`\n(built the same way as a search-location slug — join\n`address, city, state, zip` and lowercase/dashify); if that 404s or\nroutes to the wrong street, fall back to a plain city/zip\n`fetch_jsonld` search (§1) and fuzzy-match `mainEntity.address.\nstreetAddress` against your input street (whole-token match, anchored\non the street number — a same-numbered different street should not\nmatch).\n\n---\n\n## 5. Combined price + tax history\n\nSame detail page — four HTML tables (`Property History`, `Purchase\nHistory`, `Mortgage History`, `Tax History`), each row's leading\ndate/year cell is a `<th scope=\"row\">`, the rest `<td>`. Needs real\ntable-structure parsing (heading → nearest following `<table>`), which\nis impractical as a one-off grep. Two options:\n\n- `npm install node-html-parser` and mirror `src/tools/history.ts`\n  (`parsePropertyTable(root, 'Property History')` etc. — the row\n  columns per table are documented in that file's JSDoc: Property\n  History is `[date, event, price, list_to_sale_pct, price_per_sqft]`,\n  Purchase History is `[date, deed_type, sale_price, title_company]`,\n  Mortgage History is `[date, status, loan_amount, loan_type]`, Tax\n  History is `[year, tax_paid, assessment_total, assessment_land,\n  assessment_improvement]`); or\n- call `homes_get_history` on the running MCP when you need this data\n  structured — it's the same page fetch, already parsed.\n\nDate formats: Property History is `MM/DD/YYYY`; Purchase + Mortgage are\n`MM/DD/YY` (50-year window: `00–49` → `20xx`, `50–99` → `19xx`).\n\n---\n\n## 6. Nearby listings\n\nSame detail page — a tabbed, **headless** (no heading) section:\n\n```\n<section class=\"nearby-links-section-dt-v2\">\n  <ul id=\"nb-Property\">   <!-- For Sale, ~20 entries -->\n  <ul id=\"nb-Neighborhood\">\n  <ul id=\"nb-City\">\n  <ul id=\"nb-property\">   <!-- lowercase p: Rentals, ~20 entries -->\n```\n\nEach `<li>` is `<a href=\"/property/<slug>/<id>/\" title=\"<address>\">`.\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/homes-page.html\", \"utf8\");\n  const ulMatch = /<ul[^>]*\\bid=\"nb-Property\"[^>]*>([\\s\\S]*?)<\\/ul>/.exec(html);\n  if (!ulMatch) process.exit(0);\n  const out = [];\n  const seen = new Set();\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"[^>]*\\btitle=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(ulMatch[1]))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1], address: m[2] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nSwap `nb-Property` for `nb-property` (lowercase) to get the Rentals\ntab. No price/beds/baths/sqft/photo here — call `homes_get_property` /\n§2 on a row's URL to enrich it.\n\n---\n\n## 7. Saved homes / saved searches (auth-gated)\n\nRequires a signed-in `www.homes.com` tab — a missing session redirects\nto `/sign-in` or serves the AWS WAF challenge interstitial instead.\n\n```sh\nfpx get 'https://www.homes.com/customer/dashboard/favorites/' -p homes > /tmp/saved.html\nfpx get 'https://www.homes.com/customer/dashboard/saved-searches/' -p homes > /tmp/searches.html\n```\n\nBoth pages render plain HTML cards (no JSON-LD) — property-linked\n`<a href=\"/property/...\">` inside `article`/`[class*=\"favorite\"]`/\n`[class*=\"saved\"]` containers for favorites, and non-property `<a>`\nlinks inside `article`/`[class*=\"saved-search\"]` for searches. Grabbing\njust the property/search links + ids:\n\n```sh\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/saved.html\", \"utf8\");\n  const seen = new Set();\n  const out = [];\n  const re = /<a\\b[^>]*\\bhref=\"([^\"]*\\/property\\/[^\"]+)\"/g;\n  let m;\n  while ((m = re.exec(html))) {\n    const idMatch = /\\/property\\/[^/]+\\/([^/]+)\\/?$/.exec(m[1]);\n    if (!idMatch || seen.has(idMatch[1])) continue;\n    seen.add(idMatch[1]);\n    out.push({ property_id: idMatch[1], url: m[1] });\n  }\n  console.log(JSON.stringify(out, null, 2));\n'\n```\n\nPrice/beds/baths/sqft/status per card are DOM class-name lookups\n(`.price`, `.beds`, …) — best-effort on the live site; for the full\nper-card fields use `node-html-parser` mirroring\n`src/tools/saved.ts::parseSavedHomes`, or call `homes_get_saved_homes`\non the running MCP.\n\n---\n\n## 8. Bridge health check\n\n`GET /robots.txt` — small, public, no auth. Good smoke test for \"is the\nbridge/tab alive\" before debugging a real query:\n\n```sh\nfpx get 'https://www.homes.com/robots.txt' -p homes\n```\n\nFile v2.1.4:skill-card.md\n\n## Description:\n\nQuery homes.com from a shell with the fpx CLI to search listings, resolve street addresses, fetch property detail, photos, and history, and read the signed-in user's saved homes through the user's own signed-in browser tab.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and real-estate data users use this skill to make one-shot homes.com requests from an agent or shell workflow when they need listing search, address resolution, property details, saved homes, or saved searches without running the homes-mcp server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill uses the user's own authenticated homes.com browser session, and saved homes or saved searches can include private account data.\n\nMitigation: Run only requests the user intends, treat saved-account outputs as private data, and avoid sharing fetched account pages or extracted results.\n\nRisk: The workflow depends on third-party @fetchproxy/cli and the Transporter extension having access to a homes.com tab.\n\nMitigation: Review and trust the fpx CLI and Transporter extension before pairing them with an authenticated browser session.\n\nRisk: homes.com address resolution can return the closest result rat\n\nArchive v2.1.3: 4 files, 9734 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2368b), SKILL.md (5195b), _meta.json (128b)\n\nArchive v2.1.2: 4 files, 9639 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2158b), SKILL.md (5195b), _meta.json (128b)\n\nArchive v2.1.1: 4 files, 9797 bytes\n\nFiles: references/homes-requests.md (12576b), skill-card.md (2596b), SKILL.md (5195b), _meta.json (128b)","readmeExcerpt":"Skill: homes-fpx Owner: chrischall Summary: Query homes.com (US real-estate portal) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the homes-mcp server — search listings, resolve street addresses, fetch property detail/photos/ history, and read the signed-in user's saved homes, all through a one-shot call over their own signed-in browser tab. Use when you want homes.com data without the MCP, in a ","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"npm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge"},{"language":"sh","snippet":"fpx get 'https://www.homes.com/<path>' -p homes"},{"language":"sh","snippet":"fpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process.stdout.write(m[1]);\n' > /tmp/jsonld.json\njq '.[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")' /tmp/jsonld.json"},{"language":"sh","snippet":"fpx post-json 'https://www.homes.com/routes/res/consumer/smartsearch/autocomplete/' \\\n  @/tmp/body.json -p homes | jq '.suggestions.places'"},{"language":"sh","snippet":"cat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF"},{"language":"sh","snippet":"# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: homes-fpx\ndescription: >-\n  Query homes.com (US real-estate portal) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the homes-mcp server — search\n  listings, resolve street addresses, fetch property detail/photos/\n  history, and read the signed-in user's saved homes, all through a\n  one-shot call over their own signed-in browser tab. Use when you want\n  homes.com data without the MCP, in a script, or on a machine where the\n  MCP isn't installed.\n---\n\n# homes.com via fpx (no MCP)\n\nhomes.com is a fully server-rendered site with **no public JSON API**\nand gates traffic through **AWS WAF at the session level** — every\nrequest, not just login, needs to ride a real browser session. `fpx`\nroutes each call through the user's own signed-in `www.homes.com` tab\n(the ContextMint Bridge extension), which has already cleared the WAF\nchallenge, so the same fetch a Node process gets 403'd on succeeds.\n\nThis is \"Pattern A\" (every call rides the bridge) — there's no\nbootstrap-once/direct-fetch shortcut like some sibling portals get.\n\nAlmost every page is HTML with one embedded Schema.org\n`<script type=\"application/ld+json\">` block carrying the structured\ndata (search results, property detail). One endpoint — the address\ntypeahead — is a real JSON API. Everything else this skill covers is\nDOM scraping over specific, verified sections of the same pages the\n`homes_*` MCP tools parse.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli       # provides `fpx`\nfpx profile add homes --domain homes.com\nfpx pair -p homes                    # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from its [releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the zip unpacked;\nSafari: not available yet (will ship inside the ContextMint app) — use Chrome for now;\nit is the renamed fetchproxy extension from the same maintainer — verify a\nrelease zip with `shasum -a 256 -c <zip>.sha256` or build from source), with an\nopen `www.homes.com` tab (signed in — required for the saved-homes/\nsaved-searches tools below, and helps every other page render the way\nthe extractors expect), and its Chrome **Site access** allowing\n`homes.com`. Pairing persists after the first approval.\n\n## Core call pattern\n\nAlways pass the **full URL** (fpx has no base-URL concept of its own):\n\n```sh\nfpx get 'https://www.homes.com/<path>' -p homes\n```\n\nMost responses are HTML — pull the JSON-LD block out with `node`, then\nproject with `jq`. homes.com HTML-entity-encodes the script tag's\n`type` attribute (`application/ld&#x2B;json`), so match loosely:\n\n```sh\nfpx get 'https://www.homes.com/atlanta-ga/' -p homes > /tmp/page.html\nnode -e '\n  const html = require(\"fs\").readFileSync(\"/tmp/page.html\", \"utf8\");\n  const m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\n  if (!m) { console.error(\"no JSON-LD found\"); process.exit(1); }\n  process."},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"homes-fpx\",\n  \"version\": \"2.1.10\",\n  \"publishedAt\": 1791588270735\n}"},{"path":"references/homes-requests.md","content":"# homes.com request recipes\n\nEvery path below is relative to `https://www.homes.com` — `fpx` needs\nthe full URL. All shapes are transcribed from `homes-mcp`'s\n`src/tools/*.ts` (live-verified there); nothing here is guessed.\n\nA shared helper — save once, source before the recipes that need it:\n\n```sh\ncat > /tmp/homes-jsonld.js <<'EOF'\n// Usage: node /tmp/homes-jsonld.js <html-file>\n// Prints the parsed JSON-LD document (the `{ \"@context\", \"@graph\" }`\n// envelope, or a synthetic one-element graph if the page emits a bare\n// root node) as JSON to stdout.\nconst fs = require('fs');\nconst html = fs.readFileSync(process.argv[2], 'utf8');\n// homes.com HTML-entity-encodes the `+` in the script `type` attribute\n// (`application/ld&#x2B;json`) — match both forms.\nconst m = html.match(/<script type=\"application\\/ld(?:\\+|&#x2B;)json\">([\\s\\S]*?)<\\/script>/);\nif (!m) { console.error('no JSON-LD block found'); process.exit(1); }\nconst doc = JSON.parse(m[1].trim());\nif (!doc['@graph'] && doc['@type']) {\n  console.log(JSON.stringify({ '@context': doc['@context'], '@graph': [doc] }));\n} else {\n  console.log(JSON.stringify(doc));\n}\nEOF\n```\n\n```sh\n# fetch + extract in one step\nfetch_jsonld() { # $1 = full URL\n  fpx get \"$1\" -p homes > /tmp/homes-page.html\n  node /tmp/homes-jsonld.js /tmp/homes-page.html\n}\n```\n\n---\n\n## 1. Search listings\n\n`GET /<location-slug>/[<facet-segment>/[newest/]]?price-min=<n>&price-max=<n>`\n\nPath facets (verified live; everything except the price band is\npath-based — query strings for facets other than price are stripped at\nthe edge):\n\n| Filter | Path segment |\n| --- | --- |\n| `for_sale` + `single_family` | `/<slug>/houses-for-sale/` |\n| `condo` | `/<slug>/condos-for-sale/` |\n| `townhouse` | `/<slug>/townhouses-for-sale/` |\n| `land` | `/<slug>/land-for-sale/` |\n| `mobile` | `/<slug>/mobile-homes-for-sale/` |\n| `multi_family` | `/<slug>/multi-family-for-sale/` |\n| `sold` | `/<slug>/sold/` |\n| `for_rent` (untyped) | `/<slug>/homes-for-rent/` |\n| `for_rent` + house/condo/townhouse | `/<slug>/<type>-for-rent/` |\n| `open_houses` | `/<slug>/open-houses/` |\n| `new_construction` | `/new-homes/for-sale/<slug>/` (own URL root) |\n| sort `newest` | append `newest/` after the facet segment |\n| price band (the ONE query-string facet) | append `?price-min=<n>&price-max=<n>` (either optional, integer USD) |\n\n`<slug>` is the free-text location lowercased/slugified (e.g.\n`\"Atlanta, GA\"` → `atlanta-ga`, a ZIP passes through as-is).\n\n```sh\nfetch_jsonld 'https://www.homes.com/atlanta-ga/houses-for-sale/?price-min=300000&price-max=500000' > /tmp/jsonld.json\njq -r '\n  .[\"@graph\"][] | select(.[\"@type\"] == \"CollectionPage\")\n  | .mainEntity.itemListElement[]\n  | [ (.url // .[\"@id\"]),\n      .mainEntity.address.streetAddress,\n      .offers.price,\n      .mainEntity.numberOfBedrooms,\n      .mainEntity.numberOfBathroomsTotal,\n      .mainEntity.floorSize.value\n    ] | @tsv\n' /tmp/jsonld.json\n```\n\nProperty id = last non-empty path segment of `url` (strip `?query` /\n`#fragmen"},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in retrieving homes.com listings, property details, and signed-in saved homes or searches through a paired browser session using fpx.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to search US real-estate listings, resolve addresses, inspect property details and history, and access a signed-in user's saved homes or searches without the homes-mcp server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Accessing saved homes or searches can expose private account-specific real-estate interests.\n\nMitigation: Confirm the user wants this access before running saved-data recipes, and avoid retaining or sharing fetched HTML or JSON unnecessarily.\n\nRisk: Address resolution can return a nearby property rather than an exact match.\n\nMitigation: Check the resolved property's street address against the requested address before using its details.\n\n## Reference(s):\n\n- [homes-fpx release on ClawHub](https://clawhub.ai/chrischall/skills/homes-fpx)\n- [homes.com request recipes](artifact/references/homes-requests.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell, JavaScript, and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Offers recipes for public listing data and signed-in saved homes or searches; results depend on the paired browser session.]\n\n## Skill Version(s):\n\n2.1.10 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1433,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T09:01:48.217Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:53:36.075Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}