{"id":"d9254c01-87d4-425a-a2e3-42d0ef6773ca","entityType":"agent","slug":"clawhub-chrischall-opentable-fpx","name":"opentable-fpx","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-opentable-fpx","canonicalPath":"/agent/clawhub-chrischall-opentable-fpx","generatedAt":"2026-10-10T07:42:36.073Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T01:16:04.960Z","emptyReason":null},"description":"Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or actions without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: opentable-fpx Owner: chrischall Summary: Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or act","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.8K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:opentable-fpx","sourceUrl":"https://clawhub.ai/chrischall/opentable-fpx","homepage":"https://clawhub.ai/chrischall/skills/opentable-fpx","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/opentable-fpx","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/opentable-fpx","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server —"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T01:16:04.960Z","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-10T01:16:04.960Z","emptyReason":null},"stars":null,"forks":null,"downloads":1811,"packageName":null,"latestVersion":"1.3.3","tractionLabel":"1.8K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T01:16:04.930Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T01:16:04.960Z","lastCrawledAt":"2026-10-10T01:16:04.930Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T01:16:04.930Z","lastVerifiedAt":null,"highlights":[{"version":"1.3.3","createdAt":"2026-10-09T23:29:52.933Z","changelog":"- Removed the file skill-card.md. - No other changes to code or documentation content.","fileCount":5,"zipByteSize":11997},{"version":"1.3.2","createdAt":"2026-10-08T14:40:14.092Z","changelog":"- Removed the skill-card.md file. - No user-facing functionality changes; documentation and behavior remain unchanged.","fileCount":5,"zipByteSize":11999},{"version":"1.3.1","createdAt":"2026-10-07T13:40:57.279Z","changelog":"- Removed the skill-card.md file. - No changes to core functionality or usage.","fileCount":5,"zipByteSize":11980},{"version":"1.3.0","createdAt":"2026-10-07T10:50:01.922Z","changelog":"opentable-fpx 1.3.0 - Removed the skill-card.md file. - No user-facing changes to functionality or behavior.","fileCount":5,"zipByteSize":11966},{"version":"1.2.4","createdAt":"2026-10-05T02:53:48.798Z","changelog":"- Removed the redundant skill-card.md file. - No user-facing functionality or documentation changes.","fileCount":5,"zipByteSize":12073},{"version":"1.2.3","createdAt":"2026-10-03T01:44:03.006Z","changelog":"- Removed the sample skill card file (skill-card.md). - No changes to functionality or core documentation.","fileCount":5,"zipByteSize":12015},{"version":"1.2.2","createdAt":"2026-09-28T13:57:55.016Z","changelog":"- Updated all references from \"Transporter\" extension to the renamed \"ContextMint Bridge\" extension, including new install and pairing guidance. - Added instructions and links for downloading, building, and verifying ContextMint Bridge, with notes on browser compatibility (Chrome only; Safari not yet available). - Removed mention of the old extension throughout setup steps and informational sections. - Deleted obsolete file skill-card.md.","fileCount":5,"zipByteSize":11975},{"version":"1.2.1","createdAt":"2026-09-25T15:52:07.898Z","changelog":"- Removed the sample file skill-card.md. - No changes to skill functionality or documentation beyond file cleanup.","fileCount":5,"zipByteSize":11719}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:opentable-fpx","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-opentable-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T07:42:36.070Z"}},"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-opentable-fpx/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-opentable-fpx/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-10T01:16:04.960Z","emptyReason":null},"readme":"Skill: opentable-fpx\n\nOwner: chrischall\n\nSummary: Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or actions without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:1.3.3\n\nVersion history:\n\nv1.3.3 | 2026-10-09T23:29:52.933Z | auto\n\n- Removed the file skill-card.md.\n- No other changes to code or documentation content.\n\nv1.3.2 | 2026-10-08T14:40:14.092Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing functionality changes; documentation and behavior remain unchanged.\n\nv1.3.1 | 2026-10-07T13:40:57.279Z | auto\n\n- Removed the skill-card.md file.\n- No changes to core functionality or usage.\n\nv1.3.0 | 2026-10-07T10:50:01.922Z | auto\n\nopentable-fpx 1.3.0\n\n- Removed the skill-card.md file.\n- No user-facing changes to functionality or behavior.\n\nv1.2.4 | 2026-10-05T02:53:48.798Z | auto\n\n- Removed the redundant skill-card.md file.\n- No user-facing functionality or documentation changes.\n\nv1.2.3 | 2026-10-03T01:44:03.006Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No changes to functionality or core documentation.\n\nv1.2.2 | 2026-09-28T13:57:55.016Z | auto\n\n- Updated all references from \"Transporter\" extension to the renamed \"ContextMint Bridge\" extension, including new install and pairing guidance.\n- Added instructions and links for downloading, building, and verifying ContextMint Bridge, with notes on browser compatibility (Chrome only; Safari not yet available).\n- Removed mention of the old extension throughout setup steps and informational sections.\n- Deleted obsolete file skill-card.md.\n\nv1.2.1 | 2026-09-25T15:52:07.898Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to skill functionality or documentation beyond file cleanup.\n\nv1.2.0 | 2026-09-24T15:12:21.823Z | auto\n\nOpenTable-fpx v1.2.0\n\n- Updated documentation in SKILL.md for clarity on booking/modify/cancel actions: emphasizes the lack of confirm-gate in fpx and details differences vs. MCP tool prompts and preview steps.\n- Refreshed references/opentable-fpx-requests.md content.\n- Removed deprecated skill-card.md file.\n\nv1.1.4 | 2026-09-23T21:43:16.008Z | auto\n\n- Removed the file skill-card.md.\n- No changes to user-facing functionality or documentation.\n\nv1.1.3 | 2026-09-23T15:41:28.320Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing features or functionality were changed.\n\nv1.1.2 | 2026-09-21T05:02:54.527Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation content.\n- Maintains existing usage instructions and technical details.\n\nv1.1.1 | 2026-09-21T04:12:59.284Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional or user-visible changes — documentation and usage remain unchanged.\n\nv1.1.0 | 2026-09-20T02:50:00.589Z | auto\n\n- Removed redundant file: skill-card.md\n- No user-facing or functional changes; overall behavior remains the same.\n\nv1.0.0 | 2026-09-19T11:19:46.053Z | auto\n\n- Removed sample file skill-card.md.\n- No changes to functionality, documentation, or interface in this release.\n\nv0.19.5 | 2026-09-15T18:45:01.097Z | auto\n\n- Removed the file skill-card.md.\n- No functional or documentation changes to the skill itself.\n\nv0.19.4 | 2026-09-14T14:07:41.763Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing functionality changes; documentation and usage remain unchanged.\n\nv0.19.3 | 2026-09-10T17:51:10.150Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing changes to features or documentation.\n\nv0.19.2 | 2026-09-09T21:16:36.806Z | auto\n\n- Removed the obsolete skill-card.md file.\n- No user-facing functionality or documentation changes.\n\nv0.19.1 | 2026-09-05T00:51:42.198Z | auto\n\n- Removed the file: skill-card.md\n- No user-facing changes to features or documentation.\n\nv0.19.0 | 2026-09-04T22:21:29.155Z | auto\n\n- Removed the file skill-card.md.\n- No other functional or documentation changes in this release.\n\nv0.18.2 | 2026-09-03T00:57:54.871Z | auto\n\nVersion 0.18.2 of opentable-fpx\n\n- No file changes detected in this release.\n- No user-facing changes; documentation and behavior remain unchanged.\n\nv0.18.1 | 2026-09-02T18:12:03.869Z | auto\n\n- Removed the file: skill-card.md\n- No other user-facing changes in this version.\n\nv0.18.0 | 2026-08-30T21:29:30.068Z | auto\n\n- Removed obsolete documentation file: skill-card.md.\n- Updated references/opentable-fpx-requests.md (details not shown).\n- No feature, behavior, or interface changes to the underlying skill.  \n- Documentation and reference organization improvements only.\n\nv0.17.0 | 2026-08-29T13:54:21.746Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or documentation apart from file removal.\n\nv0.16.6 | 2026-08-28T21:07:43.247Z | auto\n\n- Removed the file skill-card.md.\n- No functional or user-facing changes to the skill itself.\n\nv0.16.5 | 2026-08-09T21:03:08.810Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing changes to core functionality or instructions.\n- Documentation and usage remain unchanged.\n\nv0.16.4 | 2026-08-06T00:43:25.311Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing functional changes; documentation and primary usage remain unchanged.\n\nv0.16.3 | 2026-07-30T12:57:46.115Z | auto\n\n- skill-card.md file has been removed.\n- No changes to functionality or user-facing commands.\n- Documentation and usage details remain in SKILL.md.\n\nv0.16.2 | 2026-07-30T10:41:51.235Z | auto\n\n- Introduced a detailed SKILL.md documentation for opentable-fpx, covering setup, usage patterns, and troubleshooting.\n- Clarified requirements and one-time setup for bridging fpx CLI with a signed-in OpenTable browser session via the Transporter extension.\n- Outlined procedures for searching, booking, modifying, and cancelling reservations, including important constraints like resolving dining area IDs before booking.\n- Added explanations for interpreting responses and exit codes, including how to detect sign-in issues and bot protection.\n- Provided practical usage examples, jq recipes, and referenced a full request catalogue for advanced operations.\n\nArchive index:\n\nArchive v1.3.3: 5 files, 11997 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2114b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.3.3:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.3.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.3.3\",\n  \"publishedAt\": 1791588592933\n}\n\nFile v1.3.3:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=CancelReservation' \\\n  @/tmp/ot-cancel.json -p opentable \\\n  -H 'ot-page-type: network_confirmation' -H 'ot-page-group: booking' \\\n  | jq '.data.cancelReservation | {statusCode, state: .data.reservationState, errors}'\n```\n\nA cancel succeeded when `statusCode == 200`, `state` matches\n`/cancel/i`, and `errors` is null/empty — a 200 HTTP status alone isn't\nproof.\n\n---\n\n## Persisted-query hashes (re-capture if `PersistedQueryNotFound`)\n\n| Operation | Hash |\n|---|---|\n| `RestaurantsAvailability` | `cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e` |\n| `BookDetailsStandardSlotLock` | `1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa` |\n| `BookDetailsExperienceSlotLock` | `363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504` |\n| `CancelReservation` | `4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e` |\n\nThese are Apollo persisted queries — OpenTable's client sends only the\noperation name + `sha256Hash`, never GraphQL text. If OpenTable redeploys\nand invalidates one, the response is `PersistedQueryNotFound` instead of\ndata. Re-capture in the bridged Chrome tab: navigate to the page where the\nop fires (e.g. `/booking/details` for the slot-lock ops), trigger it once,\nthen in DevTools run\n`window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId`\n(mutations) or iterate `queryManager.queries.forEach(q =>\nconsole.log(q.document.documentId))` (queries) — Apollo's `documentId` IS\nthe persisted-query `sha256Hash`. `make-reservation` and the wishlist\nadd/remove endpoints are plain JSON REST, not GraphQL — they have no hash\nto re-capture.\n\nFile v1.3.3:skill-card.md\n\n## Description:\n\nHelps agents search OpenTable, check availability, and manage reservations and favorites through a signed-in browser session using shell commands.\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\nPeople and developers using OpenTable can search restaurants, inspect availability and existing reservations, and request bookings, modifications, cancellations, or favorite changes through their signed-in browser session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A signed-in session can make real bookings, modifications, cancellations, and favorite changes without an enforced final confirmation.\n\nMitigation: Prefer read-only use; require the user to review and explicitly approve each account-changing command before execution.\n\nRisk: Bookings or cancellations may involve fees, saved cards, experiences, or 3-D Secure steps.\n\nMitigation: Review booking details and cancellation terms before acting; involve the user for payment or 3-D Secure steps.\n\n## Reference(s):\n\n- [OpenTable FPX request reference](references/opentable-fpx-requests.md)\n- [Initial-state extraction helper](references/extract-initial-state.mjs)\n- [ContextMint Bridge](https://github.com/nullnet-app/contextmint-bridge)\n- [FetchProxy CLI and extension](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [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:** [May return restaurant, availability, reservation, profile, and favorite information from the signed-in account.]\n\n## Skill Version(s):\n\n1.3.3 (source: server-resolved 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.3.2: 5 files, 11999 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2111b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.3.2:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.3.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.3.2\",\n  \"publishedAt\": 1791470414092\n}\n\nFile v1.3.2:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=CancelReservation' \\\n  @/tmp/ot-cancel.json -p opentable \\\n  -H 'ot-page-type: network_confirmation' -H 'ot-page-group: booking' \\\n  | jq '.data.cancelReservation | {statusCode, state: .data.reservationState, errors}'\n```\n\nA cancel succeeded when `statusCode == 200`, `state` matches\n`/cancel/i`, and `errors` is null/empty — a 200 HTTP status alone isn't\nproof.\n\n---\n\n## Persisted-query hashes (re-capture if `PersistedQueryNotFound`)\n\n| Operation | Hash |\n|---|---|\n| `RestaurantsAvailability` | `cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e` |\n| `BookDetailsStandardSlotLock` | `1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa` |\n| `BookDetailsExperienceSlotLock` | `363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504` |\n| `CancelReservation` | `4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e` |\n\nThese are Apollo persisted queries — OpenTable's client sends only the\noperation name + `sha256Hash`, never GraphQL text. If OpenTable redeploys\nand invalidates one, the response is `PersistedQueryNotFound` instead of\ndata. Re-capture in the bridged Chrome tab: navigate to the page where the\nop fires (e.g. `/booking/details` for the slot-lock ops), trigger it once,\nthen in DevTools run\n`window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId`\n(mutations) or iterate `queryManager.queries.forEach(q =>\nconsole.log(q.document.documentId))` (queries) — Apollo's `documentId` IS\nthe persisted-query `sha256Hash`. `make-reservation` and the wishlist\nadd/remove endpoints are plain JSON REST, not GraphQL — they have no hash\nto re-capture.\n\nFile v1.3.2:skill-card.md\n\n## Description:\n\nHelps agents search OpenTable restaurants and availability, review reservations and favorites, and manage bookings through a signed-in browser session using fpx commands.\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\nPeople and agents managing an OpenTable account can look up restaurants and available tables, then prepare or perform reservation and favorites actions using their signed-in session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Commands can book, modify, or cancel real reservations and change favorites without a built-in final confirmation gate.\n\nMitigation: Before each account-changing command, show the restaurant, date, time, party size, fees, card requirement, and cancellation policy; require explicit user approval before execution.\n\nRisk: Paired shell access can use the user's signed-in OpenTable session.\n\nMitigation: Install only if comfortable granting that access, and remove or revoke the pairing when it is no longer needed.\n\n## Reference(s):\n\n- [OpenTable via fpx on ClawHub](https://clawhub.ai/chrischall/skills/opentable-fpx)\n- [OpenTable request catalogue](references/opentable-fpx-requests.md)\n- [Initial-state extraction helper](references/extract-initial-state.mjs)\n- [ContextMint Bridge source](https://github.com/nullnet-app/contextmint-bridge)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Text]\n\n**Output Format:** [Markdown with shell commands and structured reservation details]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Booking and account-change commands act on the user's signed-in OpenTable session.]\n\n## Skill Version(s):\n\n1.3.2 (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.3.1: 5 files, 11980 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2074b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.3.1:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.3.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.3.1\",\n  \"publishedAt\": 1791380457279\n}\n\nFile v1.3.1:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=CancelReservation' \\\n  @/tmp/ot-cancel.json -p opentable \\\n  -H 'ot-page-type: network_confirmation' -H 'ot-page-group: booking' \\\n  | jq '.data.cancelReservation | {statusCode, state: .data.reservationState, errors}'\n```\n\nA cancel succeeded when `statusCode == 200`, `state` matches\n`/cancel/i`, and `errors` is null/empty — a 200 HTTP status alone isn't\nproof.\n\n---\n\n## Persisted-query hashes (re-capture if `PersistedQueryNotFound`)\n\n| Operation | Hash |\n|---|---|\n| `RestaurantsAvailability` | `cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e` |\n| `BookDetailsStandardSlotLock` | `1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa` |\n| `BookDetailsExperienceSlotLock` | `363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504` |\n| `CancelReservation` | `4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e` |\n\nThese are Apollo persisted queries — OpenTable's client sends only the\noperation name + `sha256Hash`, never GraphQL text. If OpenTable redeploys\nand invalidates one, the response is `PersistedQueryNotFound` instead of\ndata. Re-capture in the bridged Chrome tab: navigate to the page where the\nop fires (e.g. `/booking/details` for the slot-lock ops), trigger it once,\nthen in DevTools run\n`window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId`\n(mutations) or iterate `queryManager.queries.forEach(q =>\nconsole.log(q.document.documentId))` (queries) — Apollo's `documentId` IS\nthe persisted-query `sha256Hash`. `make-reservation` and the wishlist\nadd/remove endpoints are plain JSON REST, not GraphQL — they have no hash\nto re-capture.\n\nFile v1.3.1:skill-card.md\n\n## Description:\n\nHelps agents search restaurants, check availability, and manage OpenTable reservations and favorites using a signed-in browser session and shell commands.\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\nPeople and agents managing restaurant plans can find tables, review booking details, and make, change, or cancel reservations through the user's signed-in OpenTable session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Booking, changing, cancelling, or favoriting can take effect on the signed-in account without an enforced confirmation step.\n\nMitigation: Before each action, require explicit user approval after showing the restaurant, time, party size, fees, cancellation policy, card involvement, and exact action.\n\nRisk: A persistent browser pairing can continue to expose the signed-in OpenTable session to agent actions.\n\nMitigation: Keep the pairing active only for the time needed to complete the requested task.\n\n## Reference(s):\n\n- [OpenTable via fpx on ClawHub](https://clawhub.ai/chrischall/skills/opentable-fpx)\n- [OpenTable request catalogue](artifact/references/opentable-fpx-requests.md)\n- [ContextMint Bridge](https://github.com/nullnet-app/contextmint-bridge)\n- [fetchproxy extension guidance](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [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:** [Commands use the user's signed-in OpenTable browser session; write actions take effect immediately.]\n\n## Skill Version(s):\n\n1.3.1 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.3.0: 5 files, 11966 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2074b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.3.0:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1791370201922\n}\n\nFile v1.3.0:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=CancelReservation' \\\n  @/tmp/ot-cancel.json -p opentable \\\n  -H 'ot-page-type: network_confirmation' -H 'ot-page-group: booking' \\\n  | jq '.data.cancelReservation | {statusCode, state: .data.reservationState, errors}'\n```\n\nA cancel succeeded when `statusCode == 200`, `state` matches\n`/cancel/i`, and `errors` is null/empty — a 200 HTTP status alone isn't\nproof.\n\n---\n\n## Persisted-query hashes (re-capture if `PersistedQueryNotFound`)\n\n| Operation | Hash |\n|---|---|\n| `RestaurantsAvailability` | `cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e` |\n| `BookDetailsStandardSlotLock` | `1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa` |\n| `BookDetailsExperienceSlotLock` | `363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504` |\n| `CancelReservation` | `4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e` |\n\nThese are Apollo persisted queries — OpenTable's client sends only the\noperation name + `sha256Hash`, never GraphQL text. If OpenTable redeploys\nand invalidates one, the response is `PersistedQueryNotFound` instead of\ndata. Re-capture in the bridged Chrome tab: navigate to the page where the\nop fires (e.g. `/booking/details` for the slot-lock ops), trigger it once,\nthen in DevTools run\n`window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId`\n(mutations) or iterate `queryManager.queries.forEach(q =>\nconsole.log(q.document.documentId))` (queries) — Apollo's `documentId` IS\nthe persisted-query `sha256Hash`. `make-reservation` and the wishlist\nadd/remove endpoints are plain JSON REST, not GraphQL — they have no hash\nto re-capture.\n\nFile v1.3.0:skill-card.md\n\n## Description:\n\nGuides agents in searching OpenTable restaurants and availability and managing reservations and favorites through shell commands using a signed-in browser session.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople and agents with a signed-in OpenTable session can search restaurants, check availability, and manage reservations or favorites without running an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The browser bridge and fpx use the user's signed-in OpenTable session.\n\nMitigation: Use only when comfortable granting both tools access to that session; review the bridge and its permissions before pairing.\n\nRisk: Booking, modification, cancellation, and favorites commands can make real changes without a final confirmation gate.\n\nMitigation: Require the agent to show the exact restaurant, date, time, party size, cancellation policy, fees, and payment involvement and obtain user approval before any change.\n\n## Reference(s):\n\n- [OpenTable fpx skill on ClawHub](https://clawhub.ai/chrischall/skills/opentable-fpx)\n- [OpenTable request examples](references/opentable-fpx-requests.md)\n- [ContextMint Bridge](https://github.com/nullnet-app/contextmint-bridge)\n- [fetchproxy](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands may read account data or immediately change reservations and favorites through the user's signed-in session.]\n\n## Skill Version(s):\n\n1.3.0 (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.2.4: 5 files, 12073 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2237b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.2.4:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.2.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.2.4\",\n  \"publishedAt\": 1791168828798\n}\n\nFile v1.2.4:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=CancelReservation' \\\n  @/tmp/ot-cancel.json -p opentable \\\n  -H 'ot-page-type: network_confirmation' -H 'ot-page-group: booking' \\\n  | jq '.data.cancelReservation | {statusCode, state: .data.reservationState, errors}'\n```\n\nA cancel succeeded when `statusCode == 200`, `state` matches\n`/cancel/i`, and `errors` is null/empty — a 200 HTTP status alone isn't\nproof.\n\n---\n\n## Persisted-query hashes (re-capture if `PersistedQueryNotFound`)\n\n| Operation | Hash |\n|---|---|\n| `RestaurantsAvailability` | `cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e` |\n| `BookDetailsStandardSlotLock` | `1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa` |\n| `BookDetailsExperienceSlotLock` | `363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504` |\n| `CancelReservation` | `4ee53a006030f602bdeb1d751fa90ddc4240d9e17d015fb7976f8efcb80a026e` |\n\nThese are Apollo persisted queries — OpenTable's client sends only the\noperation name + `sha256Hash`, never GraphQL text. If OpenTable redeploys\nand invalidates one, the response is `PersistedQueryNotFound` instead of\ndata. Re-capture in the bridged Chrome tab: navigate to the page where the\nop fires (e.g. `/booking/details` for the slot-lock ops), trigger it once,\nthen in DevTools run\n`window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId`\n(mutations) or iterate `queryManager.queries.forEach(q =>\nconsole.log(q.document.documentId))` (queries) — Apollo's `documentId` IS\nthe persisted-query `sha256Hash`. `make-reservation` and the wishlist\nadd/remove endpoints are plain JSON REST, not GraphQL — they have no hash\nto re-capture.\n\nFile v1.2.4:skill-card.md\n\n## Description:\n\nHelps agents search OpenTable restaurants and availability, view account details, and manage reservations through a signed-in browser session using the fpx CLI.\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 restaurants and available tables or manage a user's OpenTable reservations from shell-based workflows without running the OpenTable MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Raw booking, modification, cancellation, and favorites requests can change the signed-in account without an enforced confirmation step.\n\nMitigation: Require explicit final confirmation before each write action; show restaurant, date, time, party size, cancellation policy, fees, experience price, and saved-card requirement.\n\nRisk: The skill gives an agent direct access to a signed-in OpenTable browser session, including actions involving saved cards.\n\nMitigation: Install only when that session access is acceptable; prefer the MCP tools when built-in preview and confirmation-token safeguards are needed.\n\n## Reference(s):\n\n- [OpenTable via fpx on ClawHub](https://clawhub.ai/chrischall/skills/opentable-fpx)\n- [OpenTable request examples](artifact/references/opentable-fpx-requests.md)\n- [Initial-state extraction helper](artifact/references/extract-initial-state.mjs)\n- [ContextMint Bridge extension](https://github.com/nullnet-app/contextmint-bridge)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and structured response guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read requests may return restaurant or reservation data; write requests may change the signed-in account.]\n\n## Skill Version(s):\n\n1.2.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.2.3: 5 files, 12015 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2208b), SKILL.md (5715b), _meta.json (132b)\n\nFile v1.2.3:SKILL.md\n\n---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc avoids shell-quoting the nested JSON),\nthen `fpx post-json <url> @file -p opentable`.\n\n## The one rule: resolve `dining_area_id` before you book\n\nThe numeric `dining_area_id` needed to lock a slot and make a reservation\nis **not** in the search or find-slots responses — it only appears on the\n`/booking/details` SSR page's `timeSlot.diningAreasBySeating[]`. Always\nfetch that page first and take `diningAreasBySeating[0].diningAreaId` (or\nmatch `.tableCategory` to the seating you want) before locking or booking.\nThat page is also where you learn whether the slot is CC-required, has a\ncancellation-fee policy, or requires picking an Experience — read it before\ncommitting to a booking, same as the MCP's `opentable_book_preview`.\n\n## Full request catalogue\n\nReady-to-run bodies for every read + write (search, restaurant detail,\nfind-slots, reservations/profile/favorites, book preview→commit, modify,\ncancel) with `jq` recipes and the persisted-query hashes are in\n`references/opentable-fpx-requests.md`.\n\n## Sign-in detection\n\nA response is the sign-in interstitial, not real data, when the fetched\nURL contains `/authenticate/` or the HTML body contains\n`continue-with-email-button` / `header-sign-in-button`. Sign into\nopentable.com in the bridged tab and retry.\n\n## Exit codes (fetch verbs)\n\n- `0` — success. A GraphQL response can still carry a top-level `errors`\n  array in a `0`-exit body — check `jq '.errors // empty'`.\n- `2` — bridge unavailable: extension not connected / pairing pending →\n  `fpx pair -p opentable`, confirm an opentable.com tab is open.\n- `3` — bot wall: the tab hasn't cleared Akamai → open/refresh a\n  `www.opentable.com` tab and retry.\n- `4` — upstream non-2xx from OpenTable — the body usually names the\n  invalid/missing field.\n\n## Notes\n\n- **Booking, modifying, and cancelling are real actions with no\n  confirm-gate here.** The MCP's `opentable_book`/`opentable_modify`/\n  `opentable_cancel` tools ask the user to confirm first (a prompt, or a\n  preview + single-use `confirmToken`) plus a mandatory preview step; raw\n  `fpx` calls have none of that — a `make-reservation` POST\n  commits immediately. Fetch `/booking/details` and read the cancellation\n  policy first (§5 of the reference) before calling it.\n- Same-day double-booking, CC-required slots, 3-D Secure, Experience-\n  mandatory slots, and non-NA `databaseRegion` shards are all real\n  OpenTable constraints — see `references/opentable-fpx-requests.md` and\n  this repo's `CLAUDE.md` → \"Hot spots / gotchas\" for the full detail.\n- `fpx health -p opentable` shows bridge connection state when a call\n  fails.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.2.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.2.3\",\n  \"publishedAt\": 1790991843006\n}\n\nFile v1.2.3:references/opentable-fpx-requests.md\n\n# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),\n`slotHash`, `slotAvailabilityToken`, `type` (`Standard`|`Experience`|`POP`),\n`attributes` (`default`|`bar`|`highTop`|`outdoor`), and — for Experience\nslots — `experienceIds`.\n\n```sh\ncat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'\n```\n\nNotes:\n- `restaurantAvailabilityTokens` is one array entry per id in\n  `restaurantIds`, in the same order — the literal\n  `eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ` value works for a single\n  fresh lookup (it's a generic anonymous token, not tied to the\n  restaurant).\n- `databaseRegion` defaults to `\"NA\"` (North America). Non-NA\n  (UK/EU/APAC) restaurants shard elsewhere — OpenTable doesn't surface the\n  shard id anywhere reachable from search/availability/booking-details, so\n  if a booking/cancel against a non-US venue opaquely fails, that's the\n  first thing to suspect (the MCP has the same limitation — see the repo's\n  `CLAUDE.md` → \"Hot spots / gotchas\").\n- Convert `timeOffsetMinutes` to an absolute time yourself: add it to the\n  `time` you sent (wrapping past midnight rolls the date forward a day).\n\n## 4. Reservations, profile, favorites (all SSR)\n\nOne dashboard page serves both reservations and profile:\n\n```sh\nfpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json\n```\n\nFavorites live on a separate SSR page (`state.userProfile.favorites.restaurants[]`):\n\n```sh\nfpx get 'https://www.opentable.com/user/favorites' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.userProfile.favorites.restaurants[] | {id: (.id // .restaurantId), name: (.name // .restaurantName)}'\n```\n\nAdd / remove a favorite — plain JSON POST (204 No Content on success, no\nbody to parse). A fresh add can take ~10s to show up in the SSR page above\n— treat the 204 as authoritative, don't round-trip to verify.\n\n```sh\nfpx post-json 'https://www.opentable.com/dapi/wishlist/add' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\nfpx post-json 'https://www.opentable.com/dapi/wishlist/remove' \\\n  '{\"restaurantId\": 54232, \"wishListName\": \"Favorites\"}' -p opentable\n```\n\n## 5. Book — step 1: preview (`/booking/details` + slot-lock)\n\nBooking is two POSTs after one SSR fetch. Do the SSR fetch first — it's\nalso where the numeric `diningAreaId` comes from (not in §3's response at\nall):\n\n```sh\nfpx get 'https://www.opentable.com/booking/details?rid=54232&datetime=2026-08-01T19:00&covers=2&partySize=2&seating=default&slotHash=<slot_hash>&slotAvailabilityToken=<reservation_token>' \\\n  -p opentable | node extract-initial-state.mjs > /tmp/ot-details.json\n\n# CC-required? cancellation policy? saved card? dining areas?\njq '{\n  ccRequired: .timeSlot.creditCardRequired,\n  policy: .messages.cancellationPolicyMessage.cancellationMessage.message,\n  defaultCard: (.wallet.savedCards[] | select(.default == true)),\n  diningAreas: .timeSlot.diningAreasBySeating,\n  conflicts: .upcomingReservationConflicts\n}' /tmp/ot-details.json\n```\n\nAdd `&diningAreaId=<id>` to the URL to pin a specific room (otherwise\nOpenTable still returns the full `diningAreasBySeating[]` list — take\n`[0].diningAreaId`, or match `.tableCategory` to the seating you want).\n\n**Experience-mandatory slots** (§3's `type == \"Experience\"`, one or more\n`experienceIds`) need four extra query params:\n`&experienceIds=<id>&selectedExperience=<id>&tableCategory=default&st=Experience&isMandatory=true`\n— this lands on the same page the seating-options/specials click-through\nwould. Read `.experiences.experiences[]` for the bookable Experience's\n`name`/`pricePerCover`/`version` (the `version` is required on the slot-lock\nbelow).\n\n**Same-day conflict check** — before locking, check\n`.upcomingReservationConflicts` for any entry whose `dateTime` falls on the\ndate you're booking. OpenTable hard-refuses two reservations on the same\nday; catching it here avoids an opaque 409 later.\n\nThen slot-lock (holds the slot ~90s). Standard vs Experience are\n**different GraphQL input types** — don't mix fields:\n\n```sh\n# Standard\ncat > /tmp/ot-lock.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsStandardSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"reservationType\": \"STANDARD\",\n      \"diningAreaId\": 12345\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"1100bf68905fd7cb1d4fd0f4504a4954aa28ec45fb22913fa977af8b06fd97fa\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsStandardSlotLock' \\\n  @/tmp/ot-lock.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n```sh\n# Experience — note the different field set (experienceId/experienceVersion/\n# bookingType/slotAvailabilityToken; NO reservationType)\ncat > /tmp/ot-lock-exp.json <<'JSON'\n{\n  \"operationName\": \"BookDetailsExperienceSlotLock\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"seatingOption\": \"DEFAULT\",\n      \"reservationDateTime\": \"2026-08-01T19:00\",\n      \"partySize\": 2,\n      \"databaseRegion\": \"NA\",\n      \"slotHash\": \"<slot_hash>\",\n      \"experienceId\": 987,\n      \"experienceVersion\": 1,\n      \"diningAreaId\": 12345,\n      \"bookingType\": \"Table\",\n      \"slotAvailabilityToken\": \"<reservation_token>\"\n    }\n  },\n  \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"363af9e3bd17efa82ad71c5808c5272603b5f1abe13b535d3beed1e6258ce504\" } }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=mutation&opname=BookDetailsExperienceSlotLock' \\\n  @/tmp/ot-lock-exp.json -p opentable \\\n  -H 'ot-page-type: network_details' -H 'ot-page-group: booking' \\\n  | jq '.data.lockExperienceSlot | {success, slotLockId: .slotLock.slotLockId}'\n```\n\n## 6. Book — step 2: commit (`/dapi/booking/make-reservation`)\n\nPlain JSON POST (not a persisted query). Consumes the `slotLockId` from\n§5, plus your own profile fields (from §4's `.header.userProfile` —\n`mobilePhoneNumber.number` with the country code stripped) and a fresh\n`correlationId` (any UUID). **This makes a real reservation — no\ndry-run.**\n\n```sh\ncat > /tmp/ot-book.json <<'JSON'\n{\n  \"restaurantId\": 54232,\n  \"reservationDateTime\": \"2026-08-01T19:00\",\n  \"partySize\": 2,\n  \"slotHash\": \"<slot_hash>\",\n  \"slotAvailabilityToken\": \"<reservation_token>\",\n  \"slotLockId\": 999999,\n  \"diningAreaId\": 12345,\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Doe\",\n  \"email\": \"jane@example.com\",\n  \"phoneNumber\": \"5551234567\",\n  \"phoneNumberCountryId\": \"US\",\n  \"country\": \"US\",\n  \"reservationAttribute\": \"default\",\n  \"pointsType\": \"Standard\",\n  \"points\": 100,\n  \"tipAmount\": 0,\n  \"tipPercent\": 0,\n  \"confirmPoints\": true,\n  \"optInEmailRestaurant\": false,\n  \"additionalServiceFees\": [],\n  \"nonBookableExperiences\": [],\n  \"katakanaFirstName\": \"\",\n  \"katakanaLastName\": \"\",\n  \"correlationId\": \"<uuid>\",\n  \"isModify\": false,\n  \"reservationType\": \"Standard\"\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/booking/make-reservation' \\\n  @/tmp/ot-book.json -p opentable \\\n  | jq '{confirmationNumber, reservationId, securityToken, points, errorCode, partnerScaRequired}'\n```\n\nVariants (merge these keys into the body above — never send all three sets\nat once):\n\n- **Experience booking**: replace `\"reservationType\": \"Standard\"` with\n  `\"reservationType\": \"Experience\"` and add `\"experienceId\": 987,\n  \"experienceVersion\": 1`. Do **not** send `tableCategory` here — it\n  belongs on the slot-lock body only and 400s make-reservation.\n- **CC-required slot** (from §5's `ccRequired: true`): add\n  `\"creditCardToken\": \"<wallet card id>\", \"creditCardLast4\": \"4242\",\n  \"creditCardMMYY\": \"1028\", \"creditCardProvider\": \"spreedly\",\n  \"scaRedirectUrl\": \"https://www.opentable.com/booking/payments-sca\"`. All\n  four card fields come from §5's `.wallet.savedCards[]` entry (`cardId`,\n  `last4`, `expiryMonth`+`expiryYear` → `MMYY`); `creditCardProvider` is\n  always the literal `\"spreedly\"` (OpenTable's tokenization vendor — saved\n  cardIds are already Spreedly tokens, nothing to tokenize yourself).\n- **Modify an existing reservation**: replace `\"isModify\": false` with\n  `\"isModify\": true, \"securityToken\": \"<existing security_token>\",\n  \"confnumber\": <existing confirmation_number>` (note: lowercase, no\n  underscore — OpenTable's own inconsistency). Do **not** send\n  `reservationId` on a modify — it 400s (\"reservationId is not allowed\")\n  even though it looks like the natural identifier. Everything else\n  (slotHash/slotLockId/diningAreaId/etc.) is the **new** slot's values —\n  re-run §5 for the new slot's `/booking/details` + slot-lock first, and\n  re-check `ccRequired`/the cancellation policy since they can differ from\n  the original booking.\n\nResponse fields worth checking before trusting `confirmationNumber`:\n- `partnerScaRequired: true` — the card needs 3-D Secure; can't be\n  completed outside a real browser. `partnerScaRedirectUrl` is where a\n  human would have to go to finish it.\n- `errorCode` (with `errorMessage`) — `SLOT_LOCK_EXPIRED` means the ~90s\n  lock from §5 timed out; re-run §3 → §5 → §6 with a fresh slot.\n\n## 7. Cancel a reservation\n\n`POST /dapi/fe/gql?optype=mutation&opname=CancelReservation` — persisted\nquery. Needs the `restaurantId` + `confirmationNumber` + `securityToken`\ntriple from §4's reservation list (or from §6's response).\n\n```sh\ncat > /tmp/ot-cancel.json <<'JSON'\n{\n  \"operationName\": \"CancelReservation\",\n  \"variables\": {\n    \"input\": {\n      \"restaurantId\": 54232,\n      \"confirmationNumber\": 123456789,\n      \"securityToken\": \"<security_token>\",\n      \"databaseRegion\": \"NA\",\n      \"reservationSource\": \"Online\"\n    }\n  }\n\nArchive v1.2.2: 5 files, 11975 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2094b), SKILL.md (5715b), _meta.json (132b)\n\nArchive v1.2.1: 5 files, 11719 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (1872b), SKILL.md (5256b), _meta.json (132b)\n\nArchive v1.2.0: 5 files, 11948 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16188b), skill-card.md (2522b), SKILL.md (5256b), _meta.json (132b)\n\nArchive v1.1.4: 5 files, 11971 bytes\n\nFiles: references/extract-initial-state.mjs (2412b), references/opentable-fpx-requests.md (16173b), skill-card.md (2558b), SKILL.md (5194b), _meta.json (132b)","readmeExcerpt":"Skill: opentable-fpx Owner: chrischall Summary: Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or act","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"npm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge"},{"language":"sh","snippet":"fpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'"},{"language":"sh","snippet":"fpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'"},{"language":"sh","snippet":"fpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'"},{"language":"sh","snippet":"cat > /tmp/ot-avail.json <<'JSON'\n{\n  \"operationName\": \"RestaurantsAvailability\",\n  \"variables\": {\n    \"onlyPop\": false,\n    \"forwardDays\": 0,\n    \"requireTimes\": false,\n    \"requireTypes\": [],\n    \"useCBR\": false,\n    \"privilegedAccess\": [\"UberOneDiningProgram\", \"VisaDiningProgram\", \"VisaEventsProgram\", \"ChaseDiningProgram\"],\n    \"restaurantIds\": [54232],\n    \"restaurantAvailabilityTokens\": [\"eyJ2IjoyLCJtIjoxLCJwIjowLCJzIjowLCJuIjowfQ\"],\n    \"date\": \"2026-08-01\",\n    \"time\": \"19:00\",\n    \"partySize\": 2,\n    \"databaseRegion\": \"NA\"\n  },\n  \"extensions\": {\n    \"persistedQuery\": { \"version\": 1, \"sha256Hash\": \"cbcf4838a9b399f742e3741785df64560a826d8d3cc2828aa01ab09a8455e29e\" }\n  }\n}\nJSON\nfpx post-json 'https://www.opentable.com/dapi/fe/gql?optype=query&opname=RestaurantsAvailability' \\\n  @/tmp/ot-avail.json -p opentable \\\n  -H 'ot-page-type: home' -H 'ot-page-group: seo-landing-home' \\\n  | jq -r '.data.availability[].availabilityDays[].slots[] | select(.isAvailable) | \"\\(.slotHash)\\t\\(.type)\\t\\(.timeOffsetMinutes)min\\t\\(.slotAvailabilityToken)\"'"},{"language":"sh","snippet":"fpx get 'https://www.opentable.com/user/dining-dashboard' -p opentable \\\n  | node extract-initial-state.mjs > /tmp/ot-dash.json\n\n# Reservations — state.diningDashboard.{upcomingReservations,pastReservations}[]\njq -r '.diningDashboard.upcomingReservations[] | \"\\(.confirmationNumber)\\t\\(.dateTime)\\t\\(.restaurantName)\\tparty \\(.partySize)\\tsecurityToken=\\(.securityToken)\"' /tmp/ot-dash.json\n\n# Profile — state.header.userProfile\njq '.header.userProfile | {name: \"\\(.firstName) \\(.lastName)\", email, mobile: .mobilePhoneNumber, points, metro: .metro.displayName}' /tmp/ot-dash.json"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: opentable-fpx\ndescription: >-\n  Query and manage OpenTable (opentable.com) restaurant reservations from a\n  shell with the fpx CLI (@fetchproxy/cli) instead of running the\n  opentable-mcp server — search restaurants, check slot availability, list\n  reservations/favorites, and book/modify/cancel a table via one-shot\n  GraphQL + REST calls through a signed-in browser tab. Use when you want\n  OpenTable data or actions without the MCP, in a script, or on a machine\n  where the MCP isn't installed.\n---\n\n# OpenTable via fpx (no MCP)\n\nOpenTable fronts `www.opentable.com` with Akamai bot protection, and every\nbooking/cancel action rides the user's own signed-in session (their\nloyalty account, saved cards) — there's no API key and no server-side\nlogin. `fpx` routes requests through the user's own signed-in browser tab\n(the ContextMint Bridge extension), which already carries a cleared session, so\nthe same requests the opentable.com web app makes succeed.\n\nThis is the same data/actions the `opentable_*` MCP tools expose, reached\nwith one-shot CLI calls instead of a running server. Every body/header/hash\nbelow is transcribed verbatim from opentable-mcp's `src/client.ts` and\n`src/tools/*.ts` — not guessed.\n\n## One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli            # provides `fpx`\nfpx profile add opentable --domain opentable.com\nfpx pair -p opentable                     # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: the **ContextMint Bridge** browser extension installed\n(https://github.com/nullnet-app/contextmint-bridge/releases; Chrome: load the zip unpacked; Safari isn't available yet, so use Chrome for now), an open\n`www.opentable.com` tab **signed in**, and its Chrome **Site access**\nallowing `opentable.com`. Pairing persists — after the first approval every\nlater `fpx` call reuses it.\nContextMint Bridge is the fetchproxy extension renamed, same maintainer (https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n## Core call pattern\n\nTwo response families:\n\n- **SSR HTML** (`/s`, `/r/{slug}`, `/user/dining-dashboard`,\n  `/user/favorites`, `/booking/details`) embeds the page's data as a\n  `window.__INITIAL_STATE__ = {...}` (or `\"__INITIAL_STATE__\":{...}`) JSON\n  blob inside the HTML — extract it with `references/extract-initial-state.mjs`\n  before `jq`.\n- **`/dapi/...` JSON/GraphQL** returns plain JSON on stdout — pipe straight\n  to `jq`.\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node references/extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, bookable: (.type != \"Listing\")}'\n```\n\nMost GraphQL calls here are **Apollo persisted queries** — you send an\noperation name + a pre-registered `sha256Hash`, never GraphQL text. Write\nthe body to a file first (a heredoc"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"opentable-fpx\",\n  \"version\": \"1.3.3\",\n  \"publishedAt\": 1791588592933\n}"},{"path":"references/opentable-fpx-requests.md","content":"# OpenTable requests for fpx\n\nAll paths are relative to `https://www.opentable.com`. Bodies, headers, and\npersisted-query hashes below are transcribed verbatim from opentable-mcp's\n`src/client.ts`, `src/tools/*.ts`, and `src/tools/booking-flow.ts` — not\nguessed. If a shape here stops working, re-check those files (or re-capture\nper the note at the end) before assuming this doc is stale.\n\nTwo response families:\n- **SSR HTML** pages embed the page's data as `window.__INITIAL_STATE__ =\n  {...};` or `\"__INITIAL_STATE__\":{...}` inside the HTML. Pipe through\n  `extract-initial-state.mjs` (same directory) before `jq`.\n- **`/dapi/...` JSON/GraphQL** endpoints return plain JSON on stdout — `jq`\n  directly.\n\nEvery write here (favorites add/remove, slot-lock, make-reservation,\ncancel) is a REAL action against the signed-in account — there is no\nconfirm-gate or preview-then-`confirmToken` step like the MCP's write tools\nhave. Preview before you commit.\n\n---\n\n## 1. Search restaurants\n\n`GET /s` — SSR. Data lives at `state.multiSearch.restaurants[]`\n(`state.multiSearch.totalRestaurantCount`, `.metro.name`, `.metroId` for\nthe search's resolved metro). No slot/availability data here — that's §3.\n\n```sh\nfpx get 'https://www.opentable.com/s?term=Italian%20Charlotte&covers=2&dateTime=2026-08-01T19:00' \\\n  -p opentable | node extract-initial-state.mjs \\\n  | jq '.multiSearch.restaurants[] | {id: .restaurantId, name, cuisine: .primaryCuisine.name, url: .urls.profileLink.link, rating: .statistics.reviews.ratings.overall.rating}'\n```\n\nQuery params (all optional, build the ones you have): `term` (free text —\njoin `\"<term> <location>\"` yourself, OpenTable does), `covers` (party\nsize), `dateTime` (`YYYY-MM-DDTHH:MM`, defaults the ranking not the\nresults), `latitude`, `longitude`, `metroId` (e.g. `8` = SF Bay Area, `31`\n= Charlotte).\n\n## 2. Restaurant detail\n\n`GET /r/{slug}` — SSR. Data lives at `state.restaurantProfile.restaurant`.\nA subset of (older) listings 404 here and are served at the **root**\npath `/{slug}` instead (e.g. `/the-cellar-at-duckworths`) — try `/r/{slug}`\nfirst, fall back to `/{slug}` on a 404. **Numeric ids route to\n`/restaurant/profile/{id}`** instead of either — you\nneed the slug (from §1's `urls.profileLink.link`, or the venue's known\nURL).\n\n```sh\nfpx get 'https://www.opentable.com/r/state-of-confusion-charlotte' -p opentable \\\n  | node extract-initial-state.mjs \\\n  | jq '.restaurantProfile.restaurant | {id: .restaurantId, name, type, bookable: (.type != \"Listing\"), phone: .contactInformation.formattedPhoneNumber}'\n```\n\n`type: \"Listing\"` (vs `\"GuestCenter\"`) means info-only — no slot picker,\nno booking flow; surface the phone number instead of trying §6–8.\n\n## 3. Find available slots\n\n`POST /dapi/fe/gql?optype=query&opname=RestaurantsAvailability` — Apollo\npersisted query (hash only, no GraphQL text sent). Response:\n`data.availability[].availabilityDays[].slots[]`, each slot carrying\n`timeOffsetMinutes` (relative to the `time` you sent, NOT absolute),"},{"path":"skill-card.md","content":"## Description:\n\nHelps agents search OpenTable, check availability, and manage reservations and favorites through a signed-in browser session using shell commands.\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\nPeople and developers using OpenTable can search restaurants, inspect availability and existing reservations, and request bookings, modifications, cancellations, or favorite changes through their signed-in browser session.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A signed-in session can make real bookings, modifications, cancellations, and favorite changes without an enforced final confirmation.\n\nMitigation: Prefer read-only use; require the user to review and explicitly approve each account-changing command before execution.\n\nRisk: Bookings or cancellations may involve fees, saved cards, experiences, or 3-D Secure steps.\n\nMitigation: Review booking details and cancellation terms before acting; involve the user for payment or 3-D Secure steps.\n\n## Reference(s):\n\n- [OpenTable FPX request reference](references/opentable-fpx-requests.md)\n- [Initial-state extraction helper](references/extract-initial-state.mjs)\n- [ContextMint Bridge](https://github.com/nullnet-app/contextmint-bridge)\n- [FetchProxy CLI and extension](https://github.com/chrischall/fetchproxy#extension)\n\n## Skill Output:\n\n**Output Type(s):** [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:** [May return restaurant, availability, reservation, profile, and favorite information from the signed-in account.]\n\n## Skill Version(s):\n\n1.3.3 (source: server-resolved 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":"Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or actions without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: opentable-fpx Owner: chrischall Summary: Query and manage OpenTable (opentable.com) restaurant reservations from a shell with the fpx CLI (@fetchproxy/cli) instead of running the opentable-mcp server — search restaurants, check slot availability, list reservations/favorites, and book/modify/cancel a table via one-shot GraphQL + REST calls through a signed-in browser tab. Use when you want OpenTable data or act","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1541,"uniquenessScore":45,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T01:16:04.960Z","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-10T01:16:04.960Z","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-10T07:42:36.073Z","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"}]}}}