{"id":"a80cd72c-0db9-4c85-a5cb-eeb429cabd9a","entityType":"agent","slug":"clawhub-chrischall-angi-mcp","name":"angi-mcp","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-angi-mcp","canonicalPath":"/agent/clawhub-chrischall-angi-mcp","generatedAt":"2026-10-11T03:55:54.901Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T01:41:02.124Z","emptyReason":null},"description":"Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: angi-mcp Owner: chrischall Summary: Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed. Tags: latest:1.0.7 Versi","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:angi-mcp","sourceUrl":"https://clawhub.ai/chrischall/angi-mcp","homepage":"https://clawhub.ai/chrischall/skills/angi-mcp","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/angi-mcp","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/angi-mcp","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city,"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T01:41:02.124Z","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-11T01:41:02.124Z","emptyReason":null},"stars":null,"forks":null,"downloads":1202,"packageName":null,"latestVersion":"1.0.7","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T01:41:02.060Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T01:41:02.124Z","lastCrawledAt":"2026-10-11T01:41:02.060Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T01:41:02.060Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.7","createdAt":"2026-10-09T23:22:57.065Z","changelog":"angi-mcp 1.0.7 - Removed outdated or redundant skill-card document (skill-card.md). - Updated logic or documentation in references/rsc.mjs. - No impact to user-facing functionality; documentation and internal references cleanup.","fileCount":5,"zipByteSize":11963},{"version":"1.0.6","createdAt":"2026-10-07T13:35:33.950Z","changelog":"- Removed the skill-card.md file. - No changes to functionality or documentation in this release.","fileCount":5,"zipByteSize":11774},{"version":"1.0.5","createdAt":"2026-10-05T02:49:51.080Z","changelog":"- Removed the file skill-card.md. - No functional or user-facing changes to the skill's documentation or instructions.","fileCount":5,"zipByteSize":11842},{"version":"1.0.4","createdAt":"2026-10-03T01:40:12.433Z","changelog":"- Removed the file: skill-card.md. - No user-facing changes to documentation or interface. - Internal cleanup only.","fileCount":5,"zipByteSize":11823},{"version":"1.0.3","createdAt":"2026-09-28T13:54:58.248Z","changelog":"- Updated documentation to refer to the newly renamed ContextMint Bridge browser extension (formerly fetchproxy). - Added installation instructions and source links for ContextMint Bridge. - Clarified pairing instructions and extension requirements. - Removed outdated references to the old extension name. - Deleted obsolete skill-card.md file.","fileCount":5,"zipByteSize":11778},{"version":"1.0.2","createdAt":"2026-09-23T21:38:06.653Z","changelog":"- Removed the file skill-card.md. - No functional or behavioral changes; documentation only. - Project structure cleanup.","fileCount":5,"zipByteSize":11805},{"version":"1.0.1","createdAt":"2026-09-23T15:45:54.297Z","changelog":"angi-mcp 1.0.1 - Removed the file skill-card.md. - No other functional or documentation changes.","fileCount":5,"zipByteSize":11654},{"version":"1.0.0","createdAt":"2026-09-20T08:22:44.205Z","changelog":"- Removed the skill-card.md file. - No user-facing or functional changes; documentation and core usage remain unchanged.","fileCount":5,"zipByteSize":11897}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:angi-mcp","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"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-angi-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/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-11T03:55:54.898Z"}},"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-angi-mcp/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-angi-mcp/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":"high","updatedAt":"2026-10-11T01:41:02.124Z","emptyReason":null},"readme":"Skill: angi-mcp\n\nOwner: chrischall\n\nSummary: Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:1.0.7\n\nVersion history:\n\nv1.0.7 | 2026-10-09T23:22:57.065Z | auto\n\nangi-mcp 1.0.7\n\n- Removed outdated or redundant skill-card document (skill-card.md).\n- Updated logic or documentation in references/rsc.mjs.\n- No impact to user-facing functionality; documentation and internal references cleanup.\n\nv1.0.6 | 2026-10-07T13:35:33.950Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or documentation in this release.\n\nv1.0.5 | 2026-10-05T02:49:51.080Z | auto\n\n- Removed the file skill-card.md.\n- No functional or user-facing changes to the skill's documentation or instructions.\n\nv1.0.4 | 2026-10-03T01:40:12.433Z | auto\n\n- Removed the file: skill-card.md.\n- No user-facing changes to documentation or interface. \n- Internal cleanup only.\n\nv1.0.3 | 2026-09-28T13:54:58.248Z | auto\n\n- Updated documentation to refer to the newly renamed ContextMint Bridge browser extension (formerly fetchproxy).\n- Added installation instructions and source links for ContextMint Bridge.\n- Clarified pairing instructions and extension requirements.\n- Removed outdated references to the old extension name.\n- Deleted obsolete skill-card.md file.\n\nv1.0.2 | 2026-09-23T21:38:06.653Z | auto\n\n- Removed the file skill-card.md.\n- No functional or behavioral changes; documentation only.\n- Project structure cleanup.\n\nv1.0.1 | 2026-09-23T15:45:54.297Z | auto\n\nangi-mcp 1.0.1\n\n- Removed the file skill-card.md.\n- No other functional or documentation changes.\n\nv1.0.0 | 2026-09-20T08:22:44.205Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing or functional changes; documentation and core usage remain unchanged.\n\nv0.4.0 | 2026-09-17T23:36:23.637Z | auto\n\n- skill-card.md file has been removed.\n- No changes to functionality or documentation in SKILL.md.\n\nv0.3.4 | 2026-09-15T19:24:32.937Z | auto\n\n- Removed the skill-card.md file for this version.\n- No other functionality or documentation changes.\n\nv0.3.3 | 2026-09-14T14:12:25.661Z | auto\n\n- Removed the skill-card.md file.\n- No functionality changes; documentation and usage remain the same.\n\nv0.3.2 | 2026-09-10T17:50:46.320Z | auto\n\nangi-mcp 0.3.2\n\n- Removed the file: skill-card.md\n- No other user-facing changes documented.\n\nv0.3.1 | 2026-09-09T21:17:51.852Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to functionality or documentation.\n\nv0.3.0 | 2026-09-04T22:23:44.570Z | auto\n\n- Removed the skill documentation file skill-card.md.\n- No product or API changes; functionality and setup remain unchanged.\n- Skill documentation is now centralized in SKILL.md only.\n\nv0.2.0 | 2026-08-29T13:53:39.533Z | auto\n\n- Removed the file skill-card.md.\n- No other changes to functionality or documentation.\n\nv0.1.2 | 2026-08-28T21:06:47.355Z | auto\n\n- Removed the file skill-card.md.  \n- No changes to functionality or documentation content.  \n- This update is a cleanup/maintenance release.\n\nv0.1.1 | 2026-08-28T13:55:04.263Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to behavior or documentation.\n\nv0.1.0 | 2026-08-10T23:11:14.486Z | auto\n\nInitial release: Directly query angi.com public data via the fpx CLI, without the MCP bridge.\n\n- Use fpx (with Transporter extension) to fetch Angi pros, reviews, ratings, and city/trade taxonomy.\n- No Angi login required for public data; signed-in account data also supported.\n- Trade/city taxonomy is available via plain curl to XML sitemaps; pro/rating/review data via fpx get.\n- Provides setup instructions and extraction recipes for Next.js (RSC and __NEXT_DATA__) data formats.\n- Highlights limitations (e.g., Twilio messaging unreadable, strict slug/path formats).\n- Outlines error codes and troubleshooting guidance.\n\nArchive index:\n\nArchive v1.0.7: 5 files, 11963 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7581b), skill-card.md (2196b), SKILL.md (7155b), _meta.json (127b)\n\nFile v1.0.7:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.7\",\n  \"publishedAt\": 1791588177065\n}\n\nFile v1.0.7:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.7:skill-card.md\n\n## Description:\n\nGuides agents in reading Angi's US home-service listings, provider profiles, ratings, reviews, and trade and city directories from a shell without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents can find US home-service professionals by trade and city, compare profiles and reviews, and inspect available trade and city directories. An optional workflow reads the user's own Angi account pages when explicitly paired with a signed-in browser tab.\n\n### Deployment Geography for Use:\n\nUnited States\n\n## Known Risks and Mitigations:\n\nRisk: Pairing a signed-in Angi account tab may expose the user's account pages to the agent.\n\nMitigation: Pair my.angi.com only when account data is needed, and review retrieved information before sharing it.\n\nRisk: Reading Angi pages requires the fpx CLI and ContextMint Bridge extension in the user's browser.\n\nMitigation: Install and pair these components only if comfortable fetching pages through the browser; use public sitemaps without the extension for directory lookup.\n\n## Reference(s):\n\n- [angi-mcp release on ClawHub](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](artifact/references/angi-pages.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n- [Angi trade directory sitemap](https://www.angi.com/sitemap/statecat-sitemap.xml)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, JSON data]\n\n**Output Format:** [Markdown guidance with shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Public listings and reviews; optional signed-in account data only when the user pairs an account tab.]\n\n## Skill Version(s):\n\n1.0.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.6: 5 files, 11774 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2032b), SKILL.md (7155b), _meta.json (127b)\n\nFile v1.0.6:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.6\",\n  \"publishedAt\": 1791380133950\n}\n\nFile v1.0.6:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.6:skill-card.md\n\n## Description:\n\nHelps agents read Angi's U.S. home-service listings, provider profiles, ratings, and reviews through a browser-assisted command-line workflow without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to find U.S. home-service professionals by trade and city, examine provider ratings and reviews, and browse the Angi trade and city taxonomy. An optional signed-in workflow can read the user's own Angi account information.\n\n### Deployment Geography for Use:\n\nGlobal (U.S. home-service listings)\n\n## Known Risks and Mitigations:\n\nRisk: Pairing a signed-in Angi tab can expose personal account information to the agent.\n\nMitigation: Pair and use my.angi.com only when you intentionally want the agent to read your account identity, project counts, or reviews.\n\nRisk: Browser-assisted fetching grants access through the user's open Angi tab.\n\nMitigation: Keep the extension's site access and pairing limited to the intended Angi hosts.\n\n## Reference(s):\n\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown guidance with shell examples and extracted JSON data]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only public Angi data; optional signed-in account data requires a separately paired tab.]\n\n## Skill Version(s):\n\n1.0.6 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.5: 5 files, 11842 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2146b), SKILL.md (7155b), _meta.json (127b)\n\nFile v1.0.5:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.5\",\n  \"publishedAt\": 1791168591080\n}\n\nFile v1.0.5:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.5:skill-card.md\n\n## Description:\n\nHelps agents find Angi home-service professionals by trade and city, inspect profiles and reviews, and list supported trades and locations using browser-assisted fetching rather than an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents can look up US home-service professionals, compare ratings and reviews, and explore Angi's trade and city listings without running an MCP server. Optional account lookups require an explicitly paired signed-in tab.\n\n### Deployment Geography for Use:\n\nUnited States (Angi directory coverage)\n\n## Known Risks and Mitigations:\n\nRisk: The browser-paired fetch tool relies on an installed CLI and extension with access to an open Angi tab.\n\nMitigation: Install only if you trust @fetchproxy/cli and ContextMint Bridge; pair only www.angi.com for public lookups.\n\nRisk: Pairing my.angi.com can expose signed-in account information to the agent, and extracted profiles or reviews may contain personal data.\n\nMitigation: Pair my.angi.com only when intentionally accessing your own account; handle extracted data carefully and follow Angi's terms.\n\n## Reference(s):\n\n- [Angi MCP on ClawHub](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [ContextMint Bridge source and setup](https://github.com/nullnet-app/contextmint-bridge)\n- [Angi page shapes and recipes](artifact/references/angi-pages.md)\n\n## Skill Output:\n\n**Output Type(s):** [Text, JSON, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only directory and review lookups; optional signed-in account details require separate pairing.]\n\n## Skill Version(s):\n\n1.0.5 (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.0.4: 5 files, 11823 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2097b), SKILL.md (7155b), _meta.json (127b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1790991612433\n}\n\nFile v1.0.4:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.4:skill-card.md\n\n## Description:\n\nReads Angi's US home-services listings through a browser-assisted shell workflow to find professionals, inspect profiles and reviews, and browse trade and city listings without running the 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 other users can find US home-service professionals by trade and city, compare profiles and reviews, and look up available trades and locations from a shell. An optional signed-in workflow can read their own Angi account data.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Browser pairing on an Angi account tab can expose signed-in account information to the agent.\n\nMitigation: Pair only the Angi hosts needed; avoid pairing my.angi.com unless account access is intended, and do not retain or share account results unnecessarily.\n\nRisk: Installing the fpx CLI and ContextMint Bridge adds a browser-assisted fetching capability.\n\nMitigation: Install only if comfortable with both tools and review the hosts approved for pairing.\n\n## Reference(s):\n\n- [angi-mcp on ClawHub](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [ContextMint Bridge source](https://github.com/nullnet-app/contextmint-bridge)\n- [Angi trade sitemap](https://www.angi.com/sitemap/statecat-sitemap.xml)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only Angi data retrieval; deduplicate search results by provider ID.]\n\n## Skill Version(s):\n\n1.0.4 (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.0.3: 5 files, 11778 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2080b), SKILL.md (7155b), _meta.json (127b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1790603698248\n}\n\nFile v1.0.3:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.3:skill-card.md\n\n## Description:\n\nHelps agents find Angi home-service professionals, profiles, ratings, reviews, and trade and city listings using shell commands without the Angi MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to find US home-service professionals by trade and city, inspect public ratings and reviews, or optionally read their own signed-in Angi account data.\n\n### Deployment Geography for Use:\n\nUnited States\n\n## Known Risks and Mitigations:\n\nRisk: Browser-assisted requests can access pages through a paired Angi tab.\n\nMitigation: Install and pair the required CLI and extension only if comfortable allowing browser-tab reads; keep access scoped to Angi.\n\nRisk: Signed-in account information and public reviews may contain personal data.\n\nMitigation: Use account commands only with a browser profile and account you control, and handle results in line with Angi's terms and applicable privacy rules.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [Angi page extractor](references/rsc.mjs)\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):** [Text, Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with shell and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only Angi results; optional signed-in account data requires the user's own browser session.]\n\n## Skill Version(s):\n\n1.0.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.0.2: 5 files, 11805 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2478b), SKILL.md (6739b), _meta.json (127b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nTransporter extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in Transporter\n```\n\nRequires the **Transporter** extension with an open `www.angi.com` tab and its\nChrome *Site access* allowing `angi.com`. Pairing persists across invocations.\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1790199486653\n}\n\nFile v1.0.2:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.2:skill-card.md\n\n## Description:\n\nRead angi.com home-services directory data from a shell with the fpx CLI, including providers by trade and city, provider profiles, ratings, reviews, and trade/city taxonomy.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to fetch and analyze Angi public directory pages from a browser-backed shell workflow when direct curl or Node requests cannot access the content. It also documents optional signed-in account reads when the user deliberately pairs a my.angi.com tab.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Pairing my.angi.com lets the agent read signed-in Angi account pages such as projects or reviews.\n\nMitigation: Pair my.angi.com only when private account reads are intended, and review outputs for personal data before sharing or storing them.\n\nRisk: Browser-backed fetching depends on an open, cleared Angi tab and may return login, challenge, or non-content pages if the tab state is wrong.\n\nMitigation: Keep the intended host tab open, use the documented fpx health and exit-code checks, and verify that fetched content is an Angi data page before parsing it.\n\nRisk: Search pages can duplicate providers and include sponsored placements, which can mislead rankings or counts.\n\nMitigation: Use the documented dedupe key, keep sponsored placement flags visible, and rank with the unrounded rating fields when comparing providers.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [Angi RSC flight extractor](references/rsc.mjs)\n- [Angi](https://angi.com/)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Configuration, Guidance]\n\n**Output Format:** [Markdown with shell, jq, and JavaScript examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only workflow guidance; optional signed-in account examples may expose private account data when the user pairs my.angi.com.]\n\n## Skill Version(s):\n\n1.0.2 (source: server 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.0.1: 5 files, 11654 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2184b), SKILL.md (6739b), _meta.json (127b)\n\nFile v1.0.1:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nTransporter extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in Transporter\n```\n\nRequires the **Transporter** extension with an open `www.angi.com` tab and its\nChrome *Site access* allowing `angi.com`. Pairing persists across invocations.\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1790178354297\n}\n\nFile v1.0.1:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.1:skill-card.md\n\n## Description:\n\nRead angi.com from a shell with the fpx CLI to find home-service pros by trade and city, inspect profiles, ratings, reviews, and list Angi trade and city taxonomy without running the MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to retrieve Angi directory data, provider profiles, ratings, reviews, and taxonomy information from a shell workflow. It is useful when Angi data is needed in scripts or on systems where the MCP server is not installed.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A signed-in Angi browser tab can expose account-specific projects or reviews to the agent.\n\nMitigation: Only pair or keep a signed-in my.angi.com tab open when account data is intentionally needed, and review fetched data before sharing it.\n\nRisk: The workflow depends on fpx and Transporter using an open Angi browser tab.\n\nMitigation: Use the browser bridge only for intended read-only Angi fetches and keep pairing scoped to the required Angi hosts.\n\n## Reference(s):\n\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [Angi skill page](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi state category sitemap](https://www.angi.com/sitemap/statecat-sitemap.xml)\n- [Angi plumbing geography sitemap](https://www.angi.com/sitemap/angi-geocat-plumbing.xml)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Configuration]\n\n**Output Format:** [Markdown guidance with shell commands and JSON-processing examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces read-only retrieval and parsing guidance; extracted Angi data should be reviewed before sharing.]\n\n## Skill Version(s):\n\n1.0.1 (source: server 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.0.0: 5 files, 11897 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2763b), SKILL.md (6739b), _meta.json (127b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nTransporter extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in Transporter\n```\n\nRequires the **Transporter** extension with an open `www.angi.com` tab and its\nChrome *Site access* allowing `angi.com`. Pairing persists across invocations.\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1789892564205\n}\n\nFile v1.0.0:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, pros with a meaningful review count\njq -r '[.[] | select(.rating.reviewCount >= 10)]\n       | sort_by(-.rating.averageRatings.OVERALL)[]\n       | \"\\(.rating.averageRatings.OVERALL * 100 | round / 100)\\t\\(.rating.reviewCount) reviews\\t\\(.businessInfo.businessName)\"' pros.json\n\n# Organic only (drop paid placements)\njq '[.[] | select(.isSponsored | not)]' pros.json\n\n# Emergency service + free estimates\njq -r '.[] | select(.businessInfo.amenities.emergencyServices and .businessInfo.amenities.freeEstimates)\n       | .businessInfo.businessName' pros.json\n\n# Walk several pages\nfor p in 1 2 3; do\n  fpx get \"https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm?page=$p\" -p angi \\\n    | node rsc.mjs legacyId --dedupe id\ndone | jq -s 'add | unique_by(.id)' > all-pros.json\n```\n\n## 2. Pro profile + reviews\n\n`GET <profileUrl>` — the `profileUrl` from a search record, e.g.\n`/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm`.\nThe trailing number is the pro's legacy id.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/mkb-plumbing-and-septic-llc-reviews-8535260.htm' -p angi \\\n  | node rsc.mjs reviewId > reviews.json\njq length reviews.json         # 48\n```\n\nReview record:\n\n```jsonc\n{\n  \"reviewId\": \"…\",\n  \"rating\": 5,                       // 1–5\n  \"starRating\": 5,                   // present on some pages\n  \"text\": \"…\",                       // review body\n  \"cost\": 1500,                      // dollars, or absent\n  \"reportDate\": \"2026-04-22T00:35:02.769099\",\n  \"userName\": \"…\",\n  \"isAnonymous\": false,\n  \"isVerified\": true,\n  \"isGoogleReview\": false,\n  \"source\": \"…\",\n  \"categories\": [ { \"haId\": 40111, \"name\": \"Septic System Repair\", \"reviewCount\": 1 } ],\n  \"serviceProviderId\": 159707286,\n  \"serviceProviderUuid\": \"…\",\n  \"serviceProviderName\": \"…\",\n  \"leafSlug\": \"…\",\n  \"spComment\": \"…\",                  // the pro's public response, when present\n  \"wouldHireAgain\": true             // present on some reviews\n}\n```\n\n`categories` arrives as a `\"$b5\"` reference that chains (`b5:[\"$b6\"]`);\n`rsc.mjs` resolves it. If you see a bare `\"$…\"` string in output, the row was\nmissing from the payload — re-fetch rather than trusting it.\n\nRecipes:\n\n```sh\n# Rating distribution\njq -r 'group_by(.rating)[] | \"\\(.[0].rating)★  \\(length)\"' reviews.json\n\n# Recent negative reviews with the pro's response\njq -r 'sort_by(.reportDate) | reverse\n       | .[] | select(.rating <= 3)\n       | \"\\(.reportDate[0:10])  \\(.rating)★  \\(.text[0:120])\\n    pro: \\(.spComment // \"(no response)\")\"' reviews.json\n\n# Median job cost where reported\njq '[.[] | .cost | numbers] | sort | .[length/2 | floor]' reviews.json\n\n# What work this pro is actually reviewed for\njq -r '[.[].categories[]?.name] | group_by(.)[] | \"\\(length)\\t\\(.[0])\"' reviews.json | sort -rn\n```\n\n## 3. Taxonomy (plain curl — no bridge)\n\n```sh\n# ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Index of per-trade geo sitemaps\ncurl -s https://www.angi.com/sitemap/geo-sitemap-index.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'\n\n# Cities carrying a given trade — use to validate a city/trade combo before fetching\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/\\([a-z][a-z]\\)/\\([^/]*\\)/.*#\\1/\\2#p' | sort -u\n```\n\nOther sitemaps in `robots.txt`: `article-sitemap.xml`, `leaf-sitemap-index.xml`,\n`nearme-sitemap.xml`, `topic-sitemap.xml`.\n\n## Verified negatives — don't build on these\n\n- **`?zip=` does nothing.** `…/us/nc/charlotte/plumbing.htm?zip=90210` returns\n  byte-identical Charlotte pros. Location is path-only.\n- **No consumer API.** `api.angi.com` 404s, `developer.angi.com` does not\n  resolve, and the search page issues no XHR — everything is SSR.\n- **`curl` cannot reach content pages.** 403 Cloudflare on `/`, `/companylist/*`,\n  `/nearme/*`. Only `robots.txt`, `/sitemap/*` and `/auth/login` answer.\n- **JSON-LD is not enough.** `SearchResultsPage`/`ItemList` carry names,\n  addresses, profile URLs and one sample review — no ratings, no review counts,\n  no ids.\n\nFile v1.0.0:skill-card.md\n\n## Description:\n\nRead angi.com from a shell with the fpx CLI to find home-service pros by trade and city, inspect provider profiles, ratings, reviews, and list trade or city taxonomy without running the angi-mcp server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to gather Angi public directory data through fpx, curl, node, and jq workflows, including provider search results, profile reviews, ratings breakdowns, and taxonomy lookups. Users may also run optional signed-in account examples only when they deliberately want to read their own Angi projects or reviews.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Optional my.angi.com examples can read private account data from an approved signed-in browser session.\n\nMitigation: Pair the my.angi.com host and run those commands only when the user explicitly wants the agent to read their own Angi projects or reviews.\n\nRisk: Fetching relies on fpx and the Transporter extension relaying requests through an open browser tab.\n\nMitigation: Keep host pairing narrow, confirm the intended Angi tab is open, and avoid widening cookie, storage, or header scope.\n\nRisk: Angi page payload fields are undocumented and may be duplicated, absent, or drift over time.\n\nMitigation: Resolve trade and city slugs from sitemaps, dedupe search records by id, and treat extracted fields as observed page data rather than a stable API contract.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi public site](https://angi.com/)\n- [Angi page shapes and recipes](references/angi-pages.md)\n- [Angi RSC flight extractor](references/rsc.mjs)\n- [Angi trade sitemap](https://www.angi.com/sitemap/statecat-sitemap.xml)\n- [Angi geo sitemap example](https://www.angi.com/sitemap/angi-geocat-plumbing.xml)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with shell command examples, JavaScript extractor usage, and JSON or text pipeline outputs]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only workflows use fpx, curl, node, and jq; optional signed-in account examples can access private account data from an approved browser session.]\n\n## Skill Version(s):\n\n1.0.0 (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 v0.4.0: 5 files, 11887 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (2758b), SKILL.md (6739b), _meta.json (127b)\n\nFile v0.4.0:SKILL.md\n\n---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nTransporter extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in Transporter\n```\n\nRequires the **Transporter** extension with an open `www.angi.com` tab and its\nChrome *Site access* allowing `angi.com`. Pairing persists across invocations.\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'\n```\n\n## Core call\n\nFetch the page through the tab, pipe the HTML into the extractor:\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'\n```\n\n`rsc.mjs` exists because the data is **not** in the HTML and **not** `jq`-able.\nAngi is Next.js App Router: records arrive as React Server Component flight\nrows inside `self.__next_f.push([1,\"…\"])` string literals, and nested values are\ndeduplicated behind `\"$<rowid>\"` references. The extractor concatenates the\nchunks, unescapes them, brace-matches the objects owning a key, and resolves\nthe references. See `references/angi-pages.md` for record shapes and recipes.\n\n**Always pass `--dedupe id` on search pages.** Each provider is emitted twice\n(sponsored slot + list row); raw counts are exactly double.\n\n## Signed-in account data (optional)\n\nIf the tab is signed in to Angi, the account app on `my.angi.com` is readable\nthrough the same profile (`angi.com` covers the subdomain). It is a **different\nNext.js app** — Pages Router, so its data is a plain `__NEXT_DATA__` blob, and\n`rsc.mjs` does *not* apply. Use `jq` directly:\n\n```sh\n# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq\n```\n\nTwo limits, both verified:\n\n- **Record fields are unknown.** The account these were captured from held zero\n  projects and zero reviews, so only the empty envelopes were observed. Read\n  field names off the data; don't assume them.\n- **The inbox is not readable.** `my.angi.com/inbox` runs on the Twilio\n  Conversations SDK; its proxy endpoints reject the session cookie alone\n  (`401`, XML body `username or accesskey could not be verified`). `fpx` cannot\n  speak that protocol — messages need the Twilio client, not a GET.\n\nSigned-out requests are **redirected to `/auth/login`** rather than 401'd, so a\n200 that contains a login page means the tab is signed out.\n\n## Gotchas\n\n- `?page=N` paginates (10 unique pros per page, no overlap between pages).\n- **`?zip=` is inert** — verified: `?zip=90210` on a Charlotte page returns the\n  identical Charlotte pros. Location comes from the URL path only. Don't offer\n  it as a filter.\n- JSON-LD (`SearchResultsPage`, `LocalBusiness`) is present and is real JSON,\n  but carries only names/addresses/one sample review — no ratings, no ids. Use\n  it as a cross-check, not the source.\n- A non-HTML or challenge-page 2xx means the tab lost its Cloudflare clearance;\n  refresh a `www.angi.com` tab rather than parsing the interstitial.\n\n## Exit codes (fetch verbs)\n\n- `0` success · `2` bridge unavailable (extension disconnected, or pairing\n  pending → re-run `fpx pair -p angi --subdomain www`, and confirm a tab is\n  open **on the host you are fetching**) ·\n  `3` bot wall (tab hasn't cleared Cloudflare → refresh it) · `4` upstream\n  non-2xx (usually a bad trade/city slug — re-resolve from the sitemap).\n\n## Notes\n\n- Reads only — nothing here writes to Angi. Everything is public except the\n  optional account section, which reads only the signed-in user's own data.\n  Stay within angi.com's terms.\n- `fpx health -p angi` shows bridge state when a call fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v0.4.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"0.4.0\",\n  \"publishedAt\": 1789688183637\n}\n\nFile v0.4.0:references/angi-pages.md\n\n# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload,\n\nArchive v0.3.4: 5 files, 11612 bytes\n\nFiles: references/angi-pages.md (7319b), references/rsc.mjs (7318b), skill-card.md (1975b), SKILL.md (6739b), _meta.json (127b)","readmeExcerpt":"Skill: angi-mcp Owner: chrischall Summary: Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed. Tags: latest:1.0.7 Versi","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"npm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge"},{"language":"sh","snippet":"curl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\"},{"language":"sh","snippet":"curl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\"},{"language":"sh","snippet":"# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/statecat-sitemap.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' \\\n  | sed -n 's#.*/companylist/us/[a-z][a-z]/\\([^/]*\\)\\.htm#\\1#p' | sort -u\n\n# Every city that has pages for one trade (confirms a city/trade combo exists)\ncurl -s https://www.angi.com/sitemap/angi-geocat-plumbing.xml \\\n  | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' | grep '/us/nc/'"},{"language":"sh","snippet":"fpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node references/rsc.mjs legacyId --dedupe id \\\n  | jq -r '.[] | \"\\(.businessInfo.businessName)\\t\\(.rating.averageOverallRating)★ (\\(.rating.reviewCount))\\t\\(.profileUrl)\"'"},{"language":"sh","snippet":"# Identity + project counts\nfpx get 'https://my.angi.com/myprojects' -p angi \\\n  | sed -n 's/.*<script id=\"__NEXT_DATA__\"[^>]*>\\(.*\\)<\\/script.*/\\1/p' \\\n  | jq '.props.pageProps.initialState\n        | {user: .projectHomeUser.firstName,\n           open: (.openProjectList.projects | length),\n           closed: (.closedProjectList.projects | length)}'\n\n# Reviews you've written (plain JSON, session-cookie auth)\nfpx get 'https://my.angi.com/account/rating-review/reviews' -p angi | jq"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: angi-fpx\ndescription: >-\n  Read angi.com (US home-services directory) from a shell with the fpx CLI\n  (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade\n  and city, read a pro's profile, ratings breakdown and reviews, and list the\n  trade/city taxonomy. Use when you want Angi data without the MCP, in a\n  script, or on a machine where the MCP isn't installed.\n---\n\n# Angi via fpx (no MCP)\n\nAngi fronts `www.angi.com` with Cloudflare. Every content page returns **403**\nto plain `curl`/Node — only `robots.txt`, the sitemaps and `/auth/login` get\nthrough. `fpx` runs the fetch inside the user's own browser tab (the\nContextMint Bridge extension), which has already cleared the challenge, so the same\nURL succeeds. No Angi login is needed for the public data below — a normal open\ntab is enough; only the optional *Signed-in account data* section needs a\nsigned-in tab.\n\nTwo surfaces, two tools — don't route everything through the bridge:\n\n- **Taxonomy** (which trades and cities exist) → the XML sitemaps, reachable by\n  **plain `curl`**. No bridge, no extension.\n- **Data** (pros, ratings, reviews) → `fpx get` through the tab.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli              # provides `fpx`\nfpx profile add angi --domain angi.com      # fetch capability only\nfpx pair -p angi --subdomain www            # approve the 6-digit code in ContextMint Bridge\n```\n\nRequires the **ContextMint Bridge** extension (install from\nhttps://github.com/nullnet-app/contextmint-bridge/releases — load the Chrome zip\nunpacked; Safari isn't available yet, so use Chrome for now) with an open\n`www.angi.com` tab and its *Site access* allowing `angi.com`. Pairing persists\nacross invocations.\n\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; source at\nhttps://github.com/nullnet-app/contextmint-bridge — build it or verify a release\nzip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`).\n\n**Pair per host you intend to fetch, and keep a tab open on it.** The bridge\nrelays through a tab on the request's own host, so a bare `fpx pair -p angi`\nfails with `no tab matching https://angi.com/` — the apex serves no page. Pass\n`--subdomain www`, and if you also want the signed-in account data below, open\na `my.angi.com` tab and run `fpx pair -p angi --subdomain my` as well.\n(Alternatively `--via-tab <url>` names the relaying tab explicitly.)\n\nNo cookie, storage or header scope is declared — Angi's data is server-rendered\nand rides the tab's own session, so nothing needs to be read out of the\nbrowser. Keep it that way: widening scope later forces a fresh pairing.\n\n## The one rule: resolve the trade slug first\n\nAngi URLs are `/companylist/us/<state>/<city>/<trade>.htm`. The trade is a\nfixed slug (`plumbing`, `roofing`, `air-duct-cleaning`, …), not free text, and\na wrong slug 404s. Resolve from the sitemaps before fetching — plain `curl`:\n\n```sh\n# All ~312 trade slugs\ncurl -s https://www.angi.com/sitemap/"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"angi-mcp\",\n  \"version\": \"1.0.7\",\n  \"publishedAt\": 1791588177065\n}"},{"path":"references/angi-pages.md","content":"# Angi page shapes and recipes\n\nEvery shape below was read off live angi.com bytes (Charlotte, NC / plumbing,\n2026-08-10), not inferred from docs. Angi publishes no consumer API — the\nsearch page fires zero XHR, so these pages *are* the API.\n\nExtraction key per page type:\n\n- search / company list → key **`legacyId`** (the provider wrapper)\n- pro profile → key **`reviewId`** (the review records)\n\n## 1. Search — pros by trade + city\n\n`GET /companylist/us/<state>/<city>/<trade>.htm[?page=N]`\n\n10 unique pros per page. Each appears **twice** in the payload, so\n`--dedupe id` is mandatory.\n\n```sh\nfpx get 'https://www.angi.com/companylist/us/nc/charlotte/plumbing.htm' -p angi \\\n  | node rsc.mjs legacyId --dedupe id > pros.json\njq length pros.json            # 10\n```\n\nProvider record (references already resolved by `rsc.mjs`):\n\n```jsonc\n{\n  \"id\": \"019bea94-1c41-11ee-b010-12f6eb045c11\",   // stable uuid — dedupe on this\n  \"legacyId\": \"158675609\",                        // numeric HomeAdvisor id\n  \"profileUrl\": \"/companylist/us/nc/monroe/superior-plumbing-and-drains-reviews-1.htm\",\n  \"logoPhotoUrl\": \"https://cdn.homeadvisor.com/…\",\n  \"businessType\": \"LEADS\",\n  \"isSponsored\": true,                            // paid placement, not a ranking signal\n  \"isCorporateAccount\": false,\n  \"recentLeadCount\": 2,\n  \"businessInfo\": {\n    \"businessName\": \"Superior Plumbing and Drains\",\n    \"businessDescription\": \"…\",\n    \"yearsInBusiness\": 21,\n    \"serviceArea\": \"Serving Charlotte, NC and surrounding areas\",\n    \"responseRate\": 42,                           // percent\n    \"responseTime\": \"1223\",                       // string; units undocumented — pass through\n    \"isAdvertiser\": true,\n    \"amenities\": {\n      \"acceptedPaymentMethods\": [\"CREDIT_CARD\"],\n      \"emergencyServices\": true, \"freeEstimates\": false, \"warrantiesOffered\": true,\n      \"bilingual\": false, \"veteranOwned\": false,\n      \"smallJobsWelcome\": false, \"offersCommercialServices\": false\n    },\n    \"serviceHours\": [...], \"presentationCapabilities\": {...}\n  },\n  \"contactInfo\": { \"address\": { \"street1\": \"…\", \"city\": \"Monroe\", \"state\": \"NC\", \"postalCode\": \"28110\", \"country\": \"US\" } },\n  \"rating\": {\n    \"reviewCount\": 34,\n    \"recommendedRate\": 87,                        // percent who'd recommend\n    \"averageOverallRating\": 4.5,                  // rounded, display value\n    \"averageRatings\": {                           // unrounded, per dimension\n      \"OVERALL\": 4.65625, \"QUALITY\": 4.769, \"VALUE\": 4.423,\n      \"PUNCTUALITY\": 4.807, \"PROFESSIONALISM\": 4.769, \"RESPONSIVENESS\": 4.846\n    },\n    \"bestReview\": { \"reviewId\": \"f31be34d-…\", \"comment\": \"…\", \"createdOn\": \"2020-03-28T23:48:23Z\" }\n  },\n  \"tasksOffered\": [\"Faucets, Fixtures and Pipes - Repair or Replace\", …],\n  \"awards\": [...], \"proSuppliedPhotos\": [...]\n}\n```\n\n`averageOverallRating` (4.5) and `averageRatings.OVERALL` (4.65625) differ —\nthe first is the rounded display value. Rank on `averageRatings.OVERALL`.\n\nRecipes:\n\n```sh\n# Rank by true overall rating, "},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in reading Angi's US home-service listings, provider profiles, ratings, reviews, and trade and city directories from a shell without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents can find US home-service professionals by trade and city, compare profiles and reviews, and inspect available trade and city directories. An optional workflow reads the user's own Angi account pages when explicitly paired with a signed-in browser tab.\n\n### Deployment Geography for Use:\n\nUnited States\n\n## Known Risks and Mitigations:\n\nRisk: Pairing a signed-in Angi account tab may expose the user's account pages to the agent.\n\nMitigation: Pair my.angi.com only when account data is needed, and review retrieved information before sharing it.\n\nRisk: Reading Angi pages requires the fpx CLI and ContextMint Bridge extension in the user's browser.\n\nMitigation: Install and pair these components only if comfortable fetching pages through the browser; use public sitemaps without the extension for directory lookup.\n\n## Reference(s):\n\n- [angi-mcp release on ClawHub](https://clawhub.ai/chrischall/skills/angi-mcp)\n- [Angi page shapes and recipes](artifact/references/angi-pages.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n- [Angi trade directory sitemap](https://www.angi.com/sitemap/statecat-sitemap.xml)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, JSON data]\n\n**Output Format:** [Markdown guidance with shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Public listings and reviews; optional signed-in account data only when the user pairs an account tab.]\n\n## Skill Version(s):\n\n1.0.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: angi-mcp Owner: chrischall Summary: Read angi.com (US home-services directory) from a shell with the fpx CLI (@fetchproxy/cli) instead of running the angi-mcp server — find pros by trade and city, read a pro's profile, ratings breakdown and reviews, and list the trade/city taxonomy. Use when you want Angi data without the MCP, in a script, or on a machine where the MCP isn't installed. Tags: latest:1.0.7 Versi","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1494,"uniquenessScore":47,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T01:41:02.124Z","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-11T01:41:02.124Z","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-11T03:55:54.901Z","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"}]}}}