Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4f0f109
Add opentable.com restaurant search, profile and review tools
yummybomb Sep 29, 2026
e4fee55
Restrict opentable.com url input to restaurant pages and cap string f…
yummybomb Sep 29, 2026
49f3289
Report Akamai challenge pages as challenge and bound review ids, urls…
yummybomb Sep 29, 2026
5255411
Record independent QA sign-off and mark opentable.com tools verified
yummybomb Sep 29, 2026
7e31582
Add read-only availability and exact reservation preparation
yummybomb Sep 30, 2026
52d625f
Align page tool source formatting
yummybomb Sep 30, 2026
93dd9c6
Honor cancellation and fail closed on exact inventory shapes
yummybomb Sep 30, 2026
0a71fc8
Record source QA and live verification limits
yummybomb Sep 30, 2026
59e333d
Merge remote-tracking branch 'origin/main' into hypeship/opentable-re…
yummybomb Oct 20, 2026
3e5f8cb
Conform to spec v1: README tool table and url_patterns_reason
yummybomb Oct 20, 2026
5997fae
Merge remote-tracking branch 'origin/main' into hypeship/opentable-re…
yummybomb Oct 20, 2026
f4c3d6b
opentable.com: read restaurant pages from a refused document and poin…
yummybomb Oct 21, 2026
571c664
opentable.com: read availability preparation from a refused document,…
yummybomb Oct 22, 2026
6ce466e
opentable.com: judge dates by the restaurant's calendar and accept sh…
yummybomb Oct 22, 2026
5c7ace1
Merge remote-tracking branch 'origin/main' into hypeship/opentable-re…
yummybomb Oct 23, 2026
4665ff4
opentable.com: share availability and restaurant-page reads through l…
yummybomb Oct 23, 2026
e5f1622
opentable.com: keep the availability query id per build, leave out-of…
yummybomb Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions domains/opentable.com/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# opentable.com

| tool | description |
|------|-------------|
| get_account_summary | Read the authorized OpenTable account’s current dining points, points eligibility and counts of upcoming/past reservations and invitations in the current dashboard page. |
| get_availability | Read current standard table availability for one restaurant_id from search_restaurants, with restaurant-local date/time, party_size and a window of up to 120 minutes either side. |
| get_restaurant | Reads one OpenTable restaurant's profile by restaurant_id or url: details, hours, booking limits, ratings, awards and the site's review summary. |
| get_reviews | Reads one page of diner reviews for an OpenTable restaurant in the site's default order, which follows the dining date. |
| list_saved_restaurants | Read the explicitly authorized OpenTable account’s Favorites restaurant IDs, 50 per page, with total_count and has_more. |
| prepare_reservation | Prepare an exact standard OpenTable reservation for restaurant_id, restaurant-local date/time, party_size and seating_type (default means standard dining). |
| search_restaurants | Search OpenTable (US site) for restaurants by free text, the same search the site's results page runs. |
168 changes: 168 additions & 0 deletions domains/opentable.com/get_account_summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
[{
"kind": "page",
"match": {
"url_patterns": [
"https://www.opentable.com/*"
]
},
"tool": {
"name": "get_account_summary",
"description": "Read the authorized OpenTable account’s current dining points, points eligibility and counts of upcoming/past reservations and invitations in the current dashboard page. Requires manual sign-in and expected_account_email. The past count is for the current server-rendered page, not all history; has_more_past signals another page. Does not expose names, email, phone, reservation identifiers, invitation content or tracking state, open unread notifications, redeem points, edit settings or navigate. Populated reservation item schemas are not implemented; use the site for full details. Counts do not prove booking validity or cancellation terms. No private state is returned on identity mismatch.",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": [
"expected_account_email"
],
"properties": {
"expected_account_email": {
"type": "string",
"minLength": 3,
"maxLength": 254,
"description": "Email of the account the user explicitly authorized. Compared with fresh server-rendered account state before returning data; not echoed in output."
}
}
},
"outputSchema": {
"type": "object",
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"points",
"eligible_to_earn_points",
"upcoming_reservation_count",
"past_reservation_count_on_page",
"upcoming_invitation_count",
"past_invitation_count_on_page",
"has_more_past",
"observed_at"
],
"properties": {
"status": {
"const": "ok"
},
"points": {
"type": "integer",
"minimum": 0
},
"eligible_to_earn_points": {
"type": "boolean"
},
"upcoming_reservation_count": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"past_reservation_count_on_page": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"upcoming_invitation_count": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"past_invitation_count_on_page": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"has_more_past": {
"type": "boolean"
},
"observed_at": {
"type": "string"
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"error_kind",
"error_message"
],
"properties": {
"status": {
"const": "error"
},
"error_kind": {
"enum": [
"invalid_input",
"wrong_page",
"login_required",
"wrong_account",
"challenge",
"rate_limited",
"site_changed",
"site_error",
"canceled"
]
},
"error_message": {
"type": "string",
"maxLength": 300,
"minLength": 1
}
}
}
]
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true
}
},
execute: async (input, { signal } = {}) => {
const ORIGIN = "https://www.opentable.com";
const fail = (kind, message) => ({ status: "error", error_kind: kind, error_message: message });
const allowed = ["expected_account_email"];
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(k => !allowed.includes(k))) return fail("invalid_input", "Unknown or missing input fields.");
if (typeof input.expected_account_email !== "string" || input.expected_account_email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.expected_account_email)) return fail("invalid_input", "expected_account_email must be the email of the explicitly authorized account.");
if (signal?.aborted) return fail("canceled", "The account read was canceled before any request.");
if (window.location.origin !== ORIGIN) return fail("wrong_page", "Open a https://www.opentable.com page first.");
if (/access denied/i.test(document.title || "") || document.querySelector("#sec-if-cpt-container, #sec-bc-tile-parent")) return fail("challenge", "OpenTable is challenging this browser; no request was sent.");
const check = (res, text) => {
if (res.status === 429) return fail("rate_limited", "OpenTable rate limited the read; nothing was retried.");
if (res.status === 401) return fail("login_required", "Sign in manually to the authorized OpenTable account first.");
const hasState = /<script\b[^>]*\bid="primary-window-vars"/.test(text);
if (res.status === 403 || (!hasState && (/<title[^>]*>\s*Access Denied/i.test(text) || /sec-if-cpt-container|sec-bc-tile-parent/.test(text.slice(0, 10000))))) return fail("challenge", "OpenTable refused or challenged the read.");
if (!res.ok) return fail("site_error", "OpenTable returned HTTP " + res.status + ".");
return null;
};
const dashboard = async () => {
const res = await fetch("/user/dining-dashboard", { credentials: "same-origin", redirect: "error", signal, headers: { accept: "text/html" } });
const html = await res.text();
const problem = check(res, html);
if (problem) return { error: problem };
if (html.length > 2000000) return { error: fail("site_changed", "The account document exceeded the supported bound.") };
const tag = /<script\b[^>]*\bid="primary-window-vars"[^>]*>([\s\S]*?)<\/script>/.exec(html);
if (!tag) return { error: fail("site_changed", "The account state script was not found.") };
const vars = JSON.parse(tag[1]).windowVariables;
const state = vars?.__INITIAL_STATE__;
if (state?.authentication?.isAuthenticated !== true || !state.header?.userProfile) return { error: fail("login_required", "Sign in manually to the authorized OpenTable account first.") };
const email = state.header.userProfile.email;
if (typeof email !== "string" || email.toLowerCase() !== input.expected_account_email.toLowerCase()) return { error: fail("wrong_account", "The signed-in account does not match expected_account_email; no account data was returned.") };
if (!state.diningDashboard || state.diningDashboard.hasError !== false) return { error: fail("site_error", "OpenTable did not provide a successful dining dashboard.") };
return { vars, state };
};
try {
const current = await dashboard();
if (current.error) return current.error;
const d = current.state.diningDashboard;
const profile = current.state.header.userProfile;
const arrays = [d.upcomingReservations, d.pastReservations, d.upcomingInvites, d.pastInvites];
if (arrays.some(x => !Array.isArray(x) || x.length > 10000) || !Number.isSafeInteger(profile.points) || profile.points < 0 || typeof profile.eligibleToEarnPoints !== "boolean" || (d.nextPastReservationsUrl !== null && typeof d.nextPastReservationsUrl !== "string")) return fail("site_changed", "The dining account summary shape changed.");
return { status: "ok", points: profile.points, eligible_to_earn_points: profile.eligibleToEarnPoints, upcoming_reservation_count: arrays[0].length, past_reservation_count_on_page: arrays[1].length, upcoming_invitation_count: arrays[2].length, past_invitation_count_on_page: arrays[3].length, has_more_past: !!d.nextPastReservationsUrl, observed_at: new Date().toISOString() };
} catch {
return signal?.aborted ? fail("canceled", "The account read was canceled.") : fail("site_error", "The account read failed; nothing was changed or retried.");
}
}
}]
Loading
Loading