{"id":"6d219851-3f42-4e73-8414-e3cef1f00b32","entityType":"agent","slug":"clawhub-chrischall-onehome-fpx","name":"onehome-fpx","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-onehome-fpx","canonicalPath":"/agent/clawhub-chrischall-onehome-fpx","generatedAt":"2026-10-10T14:53:02.439Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:15:08.616Z","emptyReason":null},"description":"Query OneHome (CoreLogic) — the agent magic-link real-estate portal at portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli) instead of running the onehome-mcp server. Resolve the consumer's group / saved-search scope, search shared listings, and read listing detail via one-shot GraphQL calls routed through the signed-in browser tab. Use when you want OneHome 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.4K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:onehome-fpx","sourceUrl":"https://clawhub.ai/chrischall/onehome-fpx","homepage":"https://clawhub.ai/chrischall/skills/onehome-fpx","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/onehome-fpx","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/onehome-fpx","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"onehome-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-10T12:15:08.616Z","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-10T12:15:08.616Z","emptyReason":null},"stars":null,"forks":null,"downloads":1439,"packageName":null,"latestVersion":"1.1.8","tractionLabel":"1.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:15:08.615Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T12:15:08.616Z","lastCrawledAt":"2026-10-10T12:15:08.615Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T12:15:08.615Z","lastVerifiedAt":null,"highlights":[{"version":"1.1.8","createdAt":"2026-10-09T23:28:13.465Z","changelog":"- Removed the sample skill card file (skill-card.md). - No changes to user-facing functionality or documentation content. - Version bump to 1.1.8 reflects cleanup of repository files.","fileCount":4,"zipByteSize":8361},{"version":"1.1.7","createdAt":"2026-10-07T13:39:17.667Z","changelog":"- Removed the file: skill-card.md - No changes to functionality or documentation beyond file removal.","fileCount":4,"zipByteSize":8282},{"version":"1.1.6","createdAt":"2026-10-05T02:50:08.167Z","changelog":"- Removed redundant documentation file: skill-card.md. - No changes made to core functionality or usage. - Documentation remains in SKILL.md; all instructions and details are unchanged.","fileCount":4,"zipByteSize":8312},{"version":"1.1.5","createdAt":"2026-10-03T01:46:59.130Z","changelog":"- Removed the sample file skill-card.md. - No changes were made to the main skill functionality or documentation.","fileCount":4,"zipByteSize":8288},{"version":"1.1.4","createdAt":"2026-09-28T13:55:45.037Z","changelog":"- Updated all instructions and references to use the ContextMint Bridge browser extension (replacing the legacy Transporter/fetchproxy extension). - Added details for installing the extension from the official ContextMint Bridge GitHub releases and provided verification steps. - Noted that Safari is not yet supported, and users should use Chrome for now. - Removed skill-card.md from the project.","fileCount":4,"zipByteSize":8226},{"version":"1.1.3","createdAt":"2026-09-25T15:50:38.576Z","changelog":"- Updated references in documentation: improved or revised graphql-operations.md. - Removed obsolete or redundant documentation file: skill-card.md. - No changes to feature set or CLI usage; documentation only.","fileCount":4,"zipByteSize":8065},{"version":"1.1.2","createdAt":"2026-09-23T21:41:00.116Z","changelog":"- Removed the file skill-card.md. - No changes to functionality or documentation other than file cleanup. - Version bump to 1.1.2 reflects this maintenance update.","fileCount":4,"zipByteSize":8162},{"version":"1.1.1","createdAt":"2026-09-23T15:40:54.112Z","changelog":"onehome-fpx 1.1.1 - Removed the file: skill-card.md - No functional or user-facing changes; this is a cleanup release removing unused documentation.","fileCount":4,"zipByteSize":8354}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:onehome-fpx","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:onehome-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/onehome-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-onehome-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-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-10T14:53:02.436Z"}},"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-onehome-fpx/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-onehome-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-10T12:15:08.616Z","emptyReason":null},"readme":"Skill: onehome-fpx\n\nOwner: chrischall\n\nSummary: Query OneHome (CoreLogic) — the agent magic-link real-estate portal at portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli) instead of running the onehome-mcp server. Resolve the consumer's group / saved-search scope, search shared listings, and read listing detail via one-shot GraphQL calls routed through the signed-in browser tab. Use when you want OneHome data without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:1.1.8\n\nVersion history:\n\nv1.1.8 | 2026-10-09T23:28:13.465Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No changes to user-facing functionality or documentation content.\n- Version bump to 1.1.8 reflects cleanup of repository files.\n\nv1.1.7 | 2026-10-07T13:39:17.667Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation beyond file removal.\n\nv1.1.6 | 2026-10-05T02:50:08.167Z | auto\n\n- Removed redundant documentation file: skill-card.md.\n- No changes made to core functionality or usage.\n- Documentation remains in SKILL.md; all instructions and details are unchanged.\n\nv1.1.5 | 2026-10-03T01:46:59.130Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes were made to the main skill functionality or documentation.\n\nv1.1.4 | 2026-09-28T13:55:45.037Z | auto\n\n- Updated all instructions and references to use the ContextMint Bridge browser extension (replacing the legacy Transporter/fetchproxy extension).\n- Added details for installing the extension from the official ContextMint Bridge GitHub releases and provided verification steps.\n- Noted that Safari is not yet supported, and users should use Chrome for now.\n- Removed skill-card.md from the project.\n\nv1.1.3 | 2026-09-25T15:50:38.576Z | auto\n\n- Updated references in documentation: improved or revised graphql-operations.md.\n- Removed obsolete or redundant documentation file: skill-card.md.\n- No changes to feature set or CLI usage; documentation only.\n\nv1.1.2 | 2026-09-23T21:41:00.116Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation other than file cleanup.\n- Version bump to 1.1.2 reflects this maintenance update.\n\nv1.1.1 | 2026-09-23T15:40:54.112Z | auto\n\nonehome-fpx 1.1.1\n\n- Removed the file: skill-card.md\n- No functional or user-facing changes; this is a cleanup release removing unused documentation.\n\nv1.1.0 | 2026-09-20T02:49:47.509Z | auto\n\n- Removed sample file skill-card.md for a leaner repository.\n- No user-facing features or functional changes in this version.\n- Documentation remains unchanged.\n\nv1.0.0 | 2026-09-19T11:20:49.145Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing feature or functionality changes in this version.\n\nv0.15.7 | 2026-09-15T18:29:41.723Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or documentation in SKILL.md.\n\nv0.15.6 | 2026-09-15T17:25:33.931Z | auto\n\n- Removed the file: skill-card.md\n- No user-facing feature or documentation changes; minor cleanup only.\n\nv0.15.5 | 2026-09-14T20:00:36.659Z | auto\n\n- Removed the file skill-card.md.\n- No changes to user or core functionality.\n- Documentation and usage remain unchanged.\n\nv0.15.4 | 2026-09-11T02:44:44.811Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or interface; this is a documentation/file cleanup release.\n\nv0.15.3 | 2026-09-10T17:50:59.292Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing functionality changes; documentation and CLI usage remain unchanged.\n\nv0.15.2 | 2026-09-09T21:16:38.552Z | auto\n\n- Removed the file: skill-card.md.\n- No user-facing functional changes; documentation and usage remain unchanged.\n\nv0.15.1 | 2026-09-05T00:51:12.655Z | auto\n\n- Removed the file: skill-card.md\n- No other functional or documentation changes in this version.\n\nv0.15.0 | 2026-09-04T22:21:38.390Z | auto\n\n- Dropped the redundant skill-card.md file for a leaner repo.\n- No user-facing or behavioral changes. The CLI usage and documentation remain the same.\n- All OneHome functionality and instructions are unchanged.\n\nv0.14.0 | 2026-08-29T13:54:16.870Z | auto\n\n- Removed the sample file skill-card.md from the repository.\n- No changes to SKILL.md content or core functionality.\n\nv0.13.4 | 2026-08-28T11:34:50.353Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation in SKILL.md.\n\nv0.13.3 | 2026-08-06T00:43:18.645Z | auto\n\n- Removed the sample file skill-card.md.\n- No other user-facing changes.\n\nv0.13.2 | 2026-07-30T12:54:17.052Z | auto\n\n- Added a detailed SKILL.md documenting setup, usage, and authentication flows for querying OneHome (CoreLogic) via the fpx CLI, removing the need for the MCP server.\n- Describes how to obtain session tokens using either a magic-link email URL or a live browser tab.\n- Provides ready-to-run command examples for searching, querying, and reading listing details using GraphQL.\n- Explains agent vs. consumer permission differences and handling session scope.\n- Includes troubleshooting notes, exit codes, and JWT decoding tips.\n\nArchive index:\n\nArchive v1.1.8: 4 files, 8361 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (2194b), SKILL.md (6481b), _meta.json (130b)\n\nFile v1.1.8:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.8\",\n  \"publishedAt\": 1791588493465\n}\n\nFile v1.1.8:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.8:skill-card.md\n\n## Description:\n\nGuides agents in querying an authorized OneHome real-estate portal session from the shell using fpx to find saved searches, listings, and property details without 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\nReal-estate agents, consumers, and developers with authorized OneHome access use this skill to retrieve saved-search listings, property details, photos, and related data through a signed-in browser session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Copied or captured OneHome session tokens can grant access to private portal data if exposed in files, shell history, or outputs.\n\nMitigation: Treat bearer tokens as account credentials; avoid shared machines and token files in /tmp, and clear shell history and temporary outputs.\n\nRisk: Pairing browser tooling with a signed-in session may expose private portal access to tools that have not been reviewed.\n\nMitigation: Use only portals you are authorized to access, and review the browser extension and fpx tooling before pairing.\n\n## Reference(s):\n\n- [OneHome fpx skill release](https://clawhub.ai/chrischall/skills/onehome-fpx)\n- [OneHome GraphQL and REST operations](references/graphql-operations.md)\n- [ContextMint Bridge source and installation](https://github.com/nullnet-app/contextmint-bridge)\n- [fetchproxy extension documentation](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with shell, GraphQL, and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires an authorized OneHome session and a paired browser extension.]\n\n## Skill Version(s):\n\n1.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 v1.1.7: 4 files, 8282 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (2001b), SKILL.md (6481b), _meta.json (130b)\n\nFile v1.1.7:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.7\",\n  \"publishedAt\": 1791380357667\n}\n\nFile v1.1.7:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.7:skill-card.md\n\n## Description:\n\nHelps agents query a signed-in OneHome session from a shell to find shared listings, saved searches, and listing details 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\nAgents and developers with the account owner's consent use this skill to access their OneHome saved searches and shared property listings through a signed-in browser session without an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill extracts and reuses live OneHome session credentials, which can expose account access if shared or logged.\n\nMitigation: Use only with the account owner's consent on a trusted, single-user machine; do not log authorization headers and unset bearer-token variables after use.\n\nRisk: The provided temporary-file examples can leave token-bearing data accessible or retained.\n\nMitigation: Use private temporary files instead of the examples as written, and delete token-bearing data immediately after use.\n\n## Reference(s):\n\n- [OneHome GraphQL and REST operations](artifact/references/graphql-operations.md)\n- [ContextMint Bridge source](https://github.com/nullnet-app/contextmint-bridge)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell and GraphQL examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands retrieve account-scoped OneHome data as JSON when run.]\n\n## Skill Version(s):\n\n1.1.7 (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 v1.1.6: 4 files, 8312 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (1992b), SKILL.md (6481b), _meta.json (130b)\n\nFile v1.1.6:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.6\",\n  \"publishedAt\": 1791168608167\n}\n\nFile v1.1.6:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.6:skill-card.md\n\n## Description:\n\nHelps agents query authorized OneHome saved searches and property listings from the shell using fpx instead of 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 real-estate professionals use this skill to retrieve shared OneHome searches and listing details from sessions they are authorized to access without running an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Captured session bearer values and magic-link tokens can expose private OneHome access if logged, saved, or shared.\n\nMitigation: Treat these credentials like passwords; avoid logging or storing them in shell history or /tmp.\n\nRisk: Reusing a live browser session or magic link can access OneHome data outside the user's authorization.\n\nMitigation: Use only sessions and links you are explicitly authorized to access, and prefer an official or scoped integration when available.\n\n## Reference(s):\n\n- [OneHome GraphQL and REST operations](references/graphql-operations.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n- [Fetchproxy extension documentation](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration instructions, Guidance, JSON]\n\n**Output Format:** [Markdown with shell and GraphQL examples; queries return JSON]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Results depend on the authorized OneHome session and its saved-search scope.]\n\n## Skill Version(s):\n\n1.1.6 (source: ClawHub release)\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 v1.1.5: 4 files, 8288 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (2027b), SKILL.md (6481b), _meta.json (130b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1790992019130\n}\n\nFile v1.1.5:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.5:skill-card.md\n\n## Description:\n\nHelps agents query shared OneHome real-estate listings and listing details from a shell using the fpx CLI and an authorized OneHome 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\nDevelopers and real-estate professionals use this skill to look up saved searches, shared listings, and property details through their own authorized OneHome access without running an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Capturing or exchanging a private OneHome session token exposes account-linked data to the CLI and browser extension.\n\nMitigation: Use only your own authorized session or magic-link share, and remove the fpx profile or extension access when finished.\n\nRisk: Bearer tokens and real-estate account data may be exposed through command output, local files, or logs.\n\nMitigation: Treat tokens and query results as private; avoid shared machines and logs, and remove saved token files after use.\n\n## Reference(s):\n\n- [OneHome GraphQL and REST operations](artifact/references/graphql-operations.md)\n- [ClawHub OneHome FPX release](https://clawhub.ai/chrischall/skills/onehome-fpx)\n- [ContextMint Bridge extension](https://github.com/nullnet-app/contextmint-bridge)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance, JSON]\n\n**Output Format:** [Markdown instructions with shell and GraphQL examples; OneHome query results in JSON]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires authorized OneHome access and a paired browser extension.]\n\n## Skill Version(s):\n\n1.1.5 (source: server-resolved ClawHub release)\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 v1.1.4: 4 files, 8226 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (1799b), SKILL.md (6481b), _meta.json (130b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1790603745037\n}\n\nFile v1.1.4:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.4:skill-card.md\n\n## Description:\n\nGuides agents to search private OneHome saved searches and property listings through an authenticated browser session using command-line requests.\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 authorized OneHome users use this skill to retrieve shared saved searches, property details, photos, and neighborhood information without running an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Magic-link tokens and bearer credentials can grant access to private account and listing data.\n\nMitigation: Use only authorized sessions; keep tokens out of shared terminal output, screenshots, and shell history.\n\nRisk: Temporary request and response files may retain sensitive session information.\n\nMitigation: Prefer ephemeral variables, restrict access to temporary files, and remove them after use.\n\n## Reference(s):\n\n- [OneHome GraphQL and REST operations](references/graphql-operations.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/onehome-fpx)\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 GraphQL examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [None]\n\n## Skill Version(s):\n\n1.1.4 (source: server-resolved release)\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 v1.1.3: 4 files, 8065 bytes\n\nFiles: references/graphql-operations.md (10595b), skill-card.md (1961b), SKILL.md (6007b), _meta.json (130b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** browser extension installed and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1790351438576\n}\n\nFile v1.1.3:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nGuides agents in querying shared OneHome real-estate listings through the fpx CLI and an authorized browser session 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\nAgents and developers with authorized access to a OneHome account or share can retrieve saved-search scope, shared listings, and listing details from a shell instead of using the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Session credentials or magic links may be exposed through shell commands, logs, or temporary files.\n\nMitigation: Use only authorized sessions; keep links and tokens out of shared machines, shell logs, and predictable temporary files, and do not reuse them beyond the intended session.\n\nRisk: Queries may disclose private OneHome listing or contact data beyond the intended share.\n\nMitigation: Confirm authorization for the specific account and share, limit queries to its intended scope, and handle returned contact data as sensitive.\n\n## Reference(s):\n\n- [GraphQL operations reference](references/graphql-operations.md)\n- [OneHome fpx ClawHub release](https://clawhub.ai/chrischall/skills/onehome-fpx)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with shell and GraphQL examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands can return private listing and contact data when run with an authorized session.]\n\n## Skill Version(s):\n\n1.1.3 (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 v1.1.2: 4 files, 8162 bytes\n\nFiles: references/graphql-operations.md (10612b), skill-card.md (2064b), SKILL.md (6007b), _meta.json (130b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** browser extension installed and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790199660116\n}\n\nFile v1.1.2:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      shareToken\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id — `GetSavedSearchBySearchId` (consumer-readable)\n\nThe by-groupId variant is agent-only; this one works for a consumer share.\n`savedSearch.listingIds` feeds §4.\n\n```graphql\nquery GetSavedSearchBySearchId($searchId: String!) {\n  savedSearch(id: $searchId) {\n    id\n    name\n    createdAt\n    updatedAt\n    setType\n    listingIds\n    isActive\n    resourceID\n    userQuery {\n      fieldName\n      type\n      values\n    }\n    polygon {\n      latitude\n      longitude\n    }\n  }\n}\n```\n\n```json\n{ \"searchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\" }\n```\n\n```sh\njq -r '.data.savedSearch | \"\\(.name): \\(.listingIds | length) listings\"'\n```\n\n## 3. Group listings — `GetListings` (`listings(groupId, browseParameter)`)\n\nFree-form filter via `BrowseParameter` (`searchQuery` / `sort` /\n`pageInput`). Often returns 0 for a consumer session — the consumer view\nis usually scoped to a saved search (§4), not the group as a whole.\n\n```graphql\nquery GetListings($groupId: String!, $browseParameter: BrowseParameter, $includeDislikes: Boolean) {\n  listings(groupId: $groupId, browseParameter: $browseParameter, includeDislikes: $includeDislikes) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      hideWhenUnauth\n      property {\n        OriginatingSystemKey\n        StreetNumber\n        StreetName\n        StreetSuffix\n        UnitNumber\n        City\n        StateOrProvince\n        PostalCode\n        ListPrice\n        LivingArea\n        PropertyType\n        BedroomsTotal\n        BathroomsTotalInteger\n        Latitude\n        Longitude\n        StandardStatus\n        MajorChangeType\n        MajorChangeTimestamp\n        PreviousListPrice\n      }\n      UnparsedAddress\n      customProperty {\n        ListingKey\n        FIPSCode\n      }\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"browseParameter\": { \"pageInput\": { \"pageNum\": 0, \"size\": 50 } },\n  \"includeDislikes\": false\n}\n```\n\n```sh\njq -r '.data.listings | \"total=\\(.pageInfo.totalElements)\", (.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\")'\n```\n\n## 4. Saved-search listings — `GetSavedListings` (`listingsBySavedSearchId`) — the canonical consumer view\n\nWhat the portal renders for \"Homes at `<name>`\". Supply `listingIds` from\n§2's `savedSearch.listingIds`.\n\n```graphql\nquery GetSavedListings($groupId: String!, $listingIds: [String!]!, $sort: SortCriteria, $pageInput: PageInput, $savedSearchId: String!, $includeDislikes: Boolean!, $suppressEvent: Boolean!) {\n  listingsBySavedSearchId(groupId: $groupId, osks: $listingIds, sort: $sort, pageInput: $pageInput, savedSearchId: $savedSearchId, includeDislikes: $includeDislikes, suppressEvent: $suppressEvent) {\n    pageInfo {\n      totalElements\n      totalPages\n      pageNumber\n      pageSize\n    }\n    listings {\n      id\n      property {\n        StreetNumber\n        StreetName\n        City\n        StateOrProvince\n        ListPrice\n        LivingArea\n        BedroomsTotal\n        BathroomsTotalInteger\n        StandardStatus\n      }\n      UnparsedAddress\n    }\n  }\n}\n```\n\n```json\n{\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": \"REPLACE_WITH_SAVED_SEARCH_ID\",\n  \"listingIds\": [\"REPLACE\", \"WITH\", \"IDS\", \"FROM\", \"SECTION\", \"2\"],\n  \"sort\": { \"name\": \"property.MajorChangeTimestamp\", \"order\": \"DESC\" },\n  \"pageInput\": { \"pageNum\": 0, \"size\": 50 },\n  \"includeDislikes\": false,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq -r '.data.listingsBySavedSearchId.listings[] | \"\\(.id)\\t\\(.property.ListPrice)\\t\\(.UnparsedAddress)\"'\n```\n\n## 5. Free-text search — `ListingSuggestionsSearch` (works without a group scope)\n\nUseful for an MLS-number / address lookup when you don't know the group id\nyet. `groupId` is optional.\n\n```graphql\nquery ListingSuggestionsSearch($browseParameter: String!, $groupId: String) {\n  listingSuggestionsSearch(browseParameter: $browseParameter, groupId: $groupId) {\n    id\n    listingId\n    postalCode\n    city\n    stateOrProvince\n    streetName\n    streetNumber\n    unitNumber\n    bedroomsTotal\n    bathroomsTotalInteger\n    listPrice\n    media {\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"browseParameter\": \"123 Main St\", \"groupId\": null }\n```\n\n```sh\njq -r '.data.listingSuggestionsSearch[] | \"\\(.id)\\t\\(.listPrice)\\t\\(.streetNumber) \\(.streetName), \\(.city)\"'\n```\n\n## 6. Listing detail by id — `ListingById` (`listingDetail`)\n\n`listingId` is an OSK (from any of the searches above, or from a portal\nURL — the trailing path segment). `groupId` is required; `savedSearchId`\nis optional context.\n\n```graphql\nquery ListingById($listingId: String!, $groupId: String!, $savedSearchId: String, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, savedSearchId: $savedSearchId, suppressEvent: $suppressEvent) {\n    id\n    createdAt\n    property {\n      StreetNumber\n      StreetName\n      City\n      StateOrProvince\n      PostalCode\n      ListPrice\n      LivingArea\n      BedroomsTotal\n      BathroomsTotalInteger\n      YearBuilt\n      AssociationFee\n      AssociationFeeFrequency\n      TaxAnnualAmount\n      TaxYear\n      PublicRemarks\n      Latitude\n      Longitude\n      VirtualTourURLUnbranded\n    }\n    UnparsedAddress\n    rooms {\n      RoomType\n      RoomLevel\n      RoomDimensions\n      RoomFeatures\n    }\n    openHouse {\n      OpenHouseDate\n      OpenHouseStartTime\n      OpenHouseEndTime\n      OpenHouseType\n    }\n  }\n}\n```\n\n```json\n{\n  \"listingId\": \"REPLACE_WITH_LISTING_OSK\",\n  \"groupId\": \"REPLACE_WITH_GROUP_ID\",\n  \"savedSearchId\": null,\n  \"suppressEvent\": true\n}\n```\n\n```sh\njq '.data.listingDetail | {addr: .UnparsedAddress, price: .property.ListPrice, beds: .property.BedroomsTotal, baths: .property.BathroomsTotalInteger}'\n```\n\n## 7. Listing photos — `MediaListingById`\n\n```graphql\nquery MediaListingById($listingId: String!, $groupId: String!, $suppressEvent: Boolean = true) {\n  listingDetail(listingId: $listingId, groupId: $groupId, suppressEvent: $suppressEvent) {\n    id\n    media {\n      LongDescription\n      ShortDescription\n      MediaType\n      Order\n      Image {\n        Thumbnail {\n          mediaUrl\n          width\n          height\n        }\n        Medium {\n          mediaUrl\n          width\n          height\n        }\n        Large {\n          mediaUrl\n          width\n          height\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n{ \"listingId\": \"REPLACE_WITH_LISTING_OSK\", \"groupId\": \"REPLACE_WITH_GROUP_ID\", \"suppressEvent\": true }\n```\n\n```sh\njq -r '.data.listingDetail.media[] | select(.Image.Large) | .Image.Large.mediaUrl'\n```\n\n---\n\n## 8. LocalLogic — schools / walk score (REST, not GraphQL)\n\nThe portal bundle wraps these in an Apollo `@rest` directive, which is a\nclient-side construct — POSTing the \"query\" to `/graphql` fails with\n`UnknownDirective 'rest'`. Hit the real REST URLs directly with `fpx get`,\nsame bearer:\n\n```sh\nfpx get 'https://services.onehome.com/api/locallogic/scores?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n\nfpx get 'https://services.onehome.com/api/locallogic/schools?lat=35.4382&lng=-82.1968&locale=en-US' -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' | jq '.'\n```\n\n`schools` frequently 403s for a consumer-share session (agent-only\ndataset) — `scores` (walk score + school-proximity summaries) is the\nconsumer-safe equivalent. `lat`/`lng` come from `ListingById`'s\n`property.Latitude`/`property.Longitude` (§6).\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nGuides an agent through querying authorized OneHome/CoreLogic real-estate portal data with the fpx CLI using documented REST and GraphQL calls.\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 with authorized OneHome access use this skill to obtain the relevant session scope, search shared listings, and retrieve listing details from a shell without running a separate MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill is designed to exchange or capture live OneHome session credentials and reuse them from a shell.\n\nMitigation: Install only when authorized to access the target OneHome data; treat bearer tokens, magic-link tokens, temporary files, and command transcripts as sensitive account material and clean them after use.\n\nRisk: Some operations may expose data that is only available to agent sessions or specific shared-search scopes.\n\nMitigation: Use the consumer-readable saved-search path first, check GraphQL errors before using results, and avoid attempting agent-only queries unless the session is authorized for them.\n\n## Reference(s):\n\n- [GraphQL and REST operation examples](references/graphql-operations.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/onehome-fpx)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline shell and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Includes command patterns for fpx, jq, REST requests, and GraphQL request bodies.]\n\n## Skill Version(s):\n\n1.1.2 (source: server 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 v1.1.1: 4 files, 8354 bytes\n\nFiles: references/graphql-operations.md (10612b), skill-card.md (2571b), SKILL.md (6007b), _meta.json (130b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** browser extension installed and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\"\n```\n\n**Path B — you only have a live signed-in tab** (lost the email, or want\nto ride the browser's existing session). This waits for the tab to fire\nits next GraphQL request and snapshots the `Authorization` header off it —\nscroll the map or click a listing in the tab if it hangs:\n\n```sh\nTOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')\n```\n\nPath B gives you a bearer only — no session scope, so pass `group_id` /\n`saved_search_id` explicitly on every call (or use\n`ListingSuggestionsSearch`, which needs neither).\n\n`TOKEN` from either path is already the full `Bearer <jwt>` value.\n\n## Core call\n\nEvery operation is a POST of `{\"query\": \"...\", \"variables\": {...}}` to the\nGraphQL endpoint, carrying the bearer plus the portal's `Origin`/`Referer`\n(upstream checks these). Keep the query text in its own file so you don't\nfight shell-quoting on the embedded GraphQL, then assemble the JSON body\nwith `jq -n --rawfile`:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'\n```\n\nReady-to-run operation bodies (user/groups, saved search, listing search,\nlisting detail, photos, and the LocalLogic REST calls) are in\n`references/graphql-operations.md`.\n\n## The consumer/agent split\n\nMany fields are **agent-only** and 403 for a consumer-share session:\n`user { ... }` (and its nested `groups`), and `savedSearchByGroupId`. The\nconsumer-readable equivalents are `GetSavedSearchBySearchId(searchId)` →\n`listingIds` → `GetSavedListings` (`listingsBySavedSearchId`). Always try\nthe by-searchId path first; only fall back to the `user`/groups query if\nyou know the session is an agent's.\n\n## Exit codes (fetch verbs)\n\n- `0` — success (a GraphQL `errors` array can still ride in a `0` body — always check `jq '.errors // empty'`).\n- `2` — bridge unavailable: extension not connected or pairing pending → `fpx pair -p onehome`.\n- `3` — bot wall (shouldn't happen on `services.onehome.com` — if it does, something upstream changed).\n- `4` — upstream non-2xx, most often a stale/expired bearer (401) → get a fresh `TOKEN` via Path A or B above.\n\n## Notes\n\n- The captured/exchanged bearer is a JWT — decode its `exp` if you need to\n  know when to refresh. JWTs are base64url, not base64 (they use `-`/`_`\n  instead of `+`/`/`), so translate the alphabet before `@base64d`:\n  `jq -R 'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson | .exp'`\n  on the token, ignoring padding errors.\n- `fpx health -p onehome` shows bridge connection state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.1\",\n  \"publishedAt\": 1790178054112\n}\n\nFile v1.1.1:references/graphql-operations.md\n\n# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://por\n\nArchive v1.1.0: 4 files, 8313 bytes\n\nFiles: references/graphql-operations.md (10612b), skill-card.md (2484b), SKILL.md (6007b), _meta.json (130b)\n\nArchive v1.0.0: 4 files, 8071 bytes\n\nFiles: references/graphql-operations.md (10612b), skill-card.md (1880b), SKILL.md (6007b), _meta.json (130b)","readmeExcerpt":"Skill: onehome-fpx Owner: chrischall Summary: Query OneHome (CoreLogic) — the agent magic-link real-estate portal at portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli) instead of running the onehome-mcp server. Resolve the consumer's group / saved-search scope, search shared listings, and read listing detail via one-shot GraphQL calls routed through the signed-in browser tab. Use when you want OneHo","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"npm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge"},{"language":"sh","snippet":"LINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]*\\).*/\\1/p')\nprintf '{\"emailToken\":\"%s\"}' \"$EMAIL_TOKEN\" > /tmp/checktoken-body.json\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' \\\n  @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | tee /tmp/checktoken.json | jq '{groupID,savedSearchID,agentID,contactID,mlsID,email}'\nTOKEN=\"Bearer $(jq -r '.sessionToken' /tmp/checktoken.json)\""},{"language":"sh","snippet":"TOKEN=$(fpx session -p onehome | jq -r '.capturedHeaders.Authorization')"},{"language":"sh","snippet":"cat > /tmp/q.graphql <<'GQL'\nquery GetSavedSearchBySearchId($searchId: String!) { savedSearch(id: $searchId) { id name listingIds } }\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '{\"searchId\":\"REPLACE\"}' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' \\\n  | jq '.data'"},{"language":"sh","snippet":"cat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'"},{"language":"json","snippet":"{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: onehome-fpx\ndescription: >-\n  Query OneHome (CoreLogic) — the agent magic-link real-estate portal at\n  portal.onehome.com — from a shell with the fpx CLI (@fetchproxy/cli)\n  instead of running the onehome-mcp server. Resolve the consumer's group /\n  saved-search scope, search shared listings, and read listing detail via\n  one-shot GraphQL calls routed through the signed-in browser tab. Use when\n  you want OneHome data without the MCP, in a script, or on a machine where\n  the MCP isn't installed.\n---\n\n# OneHome via fpx (no MCP)\n\nOneHome is a CoreLogic product: buyers reach it through a private\n\"magic-link\" URL their real-estate agent emailed\n(`https://portal.onehome.com/en-US/properties/map?token=eyJ...`). It's an\nAngular SPA backed by one GraphQL API at `services.onehome.com/graphql`\n(plus a couple of REST endpoints), authenticated with\n`Authorization: Bearer <sessionToken>`. There's no anti-bot wall on\n`services.onehome.com` — the gate is purely the per-user bearer — so `fpx`\nis used here to reach the token itself (either by exchanging the magic\nlink, or by capturing it from a live tab), then to fire the actual GraphQL\ncalls through the same signed-in tab.\n\nThis is the same data the `onehome_*` MCP tools return, reached with CLI\ncalls instead of a running server. See `src/queries.ts` and `src/auth.ts`\nin the repo for the live-verified source of every shape below.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                 # provides `fpx`\nfpx profile add onehome --domain onehome.com    # covers portal.* and services.*\nfpx profile declare onehome \\\n  --capture-header Authorization@services.onehome.com/graphql  # needed for Path B below\nfpx pair -p onehome                             # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(from https://github.com/nullnet-app/contextmint-bridge/releases — Chrome:\nload the chrome zip unpacked; Safari is not available yet, so use Chrome for now) and paired,\nits Chrome **Site access** allowing `onehome.com`, and (for Path B) an open\n`portal.onehome.com` tab signed in to the share you want to read.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify the release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Getting a bearer token (two paths)\n\n**Path A — you have the magic-link URL** (from the agent's email). The\n`?token=` value is an *email-token*, not a bearer — exchange it once via\nthe same `checkToken` bootstrap the Angular app runs on load. This also\nhands back the consumer's scope (`groupID`, `savedSearchID`, `agentID`,\n`contactID`, `mlsID`), so you don't have to guess `group_id` later:\n\n```sh\nLINK='https://portal.onehome.com/en-US/properties/map?token=eyJ...'\nEMAIL_TOKEN=$(echo \"$LINK\" | sed -n 's/.*[?&]token=\\([^&]"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"onehome-fpx\",\n  \"version\": \"1.1.8\",\n  \"publishedAt\": 1791588493465\n}"},{"path":"references/graphql-operations.md","content":"# OneHome GraphQL + REST operations for fpx\n\nReady-to-run bodies for `fpx post-json 'https://services.onehome.com/graphql' @body.json -p onehome -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/'`.\nEvery query string and variable shape below is drawn from `src/queries.ts`\nin this repo (the same documents `onehome-mcp`'s tools send) — some are\ncopied verbatim, others are trimmed field subsets for readability here,\nbut none are guessed. Get `$TOKEN` first (see `../SKILL.md`).\n\nPattern: write the GraphQL text to a file (a heredoc avoids shell-quoting\nthe doc), assemble the JSON body with `jq -n --rawfile`, then POST:\n\n```sh\ncat > /tmp/q.graphql <<'GQL'\n<query text>\nGQL\njq -n --rawfile query /tmp/q.graphql --argjson variables '<vars>' \\\n  '{query:$query, variables:$variables}' > /tmp/body.json\nfpx post-json 'https://services.onehome.com/graphql' @/tmp/body.json -p onehome \\\n  -H \"Authorization: $TOKEN\" -H 'Origin: https://portal.onehome.com' \\\n  -H 'Referer: https://portal.onehome.com/' | jq '.data'\n```\n\nAlways check for GraphQL errors first: `jq '.errors // empty'` (an errors\narray can ride in an HTTP-200 body — e.g. `Access Denied` on agent-only\nfields for a consumer session).\n\n---\n\n## 0. Auth bootstrap — email-token → sessionToken (REST, not GraphQL)\n\n`POST /api/authentication/checkToken` — the Angular app's own load-time\nexchange. Not auth-gated itself; needs no `Authorization` header. See\n`../SKILL.md`'s \"Path A\" for the full flow (extracting `?token=` from the\nmagic link, running this, and deriving `$TOKEN`).\n\n```json\n{ \"emailToken\": \"REPLACE_WITH_URL_TOKEN_PARAM\" }\n```\n\nResponse fields used downstream: `sessionToken` (the bearer JWT),\n`groupID`, `savedSearchID`, `agentID`, `contactID`, `mlsID`, `email`,\n`signedIn`, `registered`.\n\n```sh\nfpx post-json 'https://services.onehome.com/api/authentication/checkToken' @/tmp/checktoken-body.json -p onehome \\\n  -H 'Origin: https://portal.onehome.com' -H 'Referer: https://portal.onehome.com/' \\\n  | jq '{sessionToken, groupID, savedSearchID, agentID, contactID, mlsID, email}'\n```\n\n---\n\n## 1. User + groups — `GetOneHomeUser` (agent sessions only)\n\nReturns `Access Denied` for a consumer-share session (the `user` field is\nagent-only) — try `GetSavedSearchBySearchId` (§2) first unless you know\nthis is an agent session.\n\n```graphql\nquery GetOneHomeUser {\n  user {\n    id\n    firstName\n    lastName\n    email\n    phone\n    registered\n    lastAccessedGroupId\n    lastAccessedSavedSearchId\n    userWelcomed\n    groups {\n      id\n      firstName\n      lastName\n      contactId\n      emails\n      contactStatus\n      createdAt\n      agent {\n        id\n        firstName\n        lastName\n        fullName\n        email\n        phone\n        officeName\n        officePhone\n        teamName\n        mls {\n          mlsid\n        }\n      }\n    }\n  }\n}\n```\n\nNo variables.\n\n```sh\njq -r '.data.user.groups[] | \"\\(.id)\\t\\(.firstName) \\(.lastName)\"'\n```\n\n## 2. Saved search by id "},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in querying an authorized OneHome real-estate portal session from the shell using fpx to find saved searches, listings, and property details without 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\nReal-estate agents, consumers, and developers with authorized OneHome access use this skill to retrieve saved-search listings, property details, photos, and related data through a signed-in browser session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Copied or captured OneHome session tokens can grant access to private portal data if exposed in files, shell history, or outputs.\n\nMitigation: Treat bearer tokens as account credentials; avoid shared machines and token files in /tmp, and clear shell history and temporary outputs.\n\nRisk: Pairing browser tooling with a signed-in session may expose private portal access to tools that have not been reviewed.\n\nMitigation: Use only portals you are authorized to access, and review the browser extension and fpx tooling before pairing.\n\n## Reference(s):\n\n- [OneHome fpx skill release](https://clawhub.ai/chrischall/skills/onehome-fpx)\n- [OneHome GraphQL and REST operations](references/graphql-operations.md)\n- [ContextMint Bridge source and installation](https://github.com/nullnet-app/contextmint-bridge)\n- [fetchproxy extension documentation](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with shell, GraphQL, and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires an authorized OneHome session and a paired browser extension.]\n\n## Skill Version(s):\n\n1.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."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1451,"uniquenessScore":42,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T12:15:08.616Z","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-10T12:15:08.616Z","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-10T14:53:02.439Z","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"}]}}}