--- name: agentres description: Book, manage, and cancel Resy restaurant reservations — instant bookings, reservation jobs that book hard-to-get tables the moment reservations open, live wait times from camera-based crowd data, and Blackbird restaurant discovery with daily-refreshed specials. Pay per use in USDC stablecoins via x402 or MPP wallet auth — no signup, a funded wallet is the account. Use when the user asks to find or book a table, get restaurant recommendations, check availability or how long a line is, create a reservation job, link Resy, list or cancel reservations, or set up API access — even without naming Resy, e.g. "get me a table at ...", "how long is the line at ...", "recommend a spot in the West Village". --- # Resy Booking Read fully before acting. Two parts: **Setup** (how to pay, authenticate, link Resy — once) and **Instructions** (normal booking, reservation jobs, managing reservations, live wait times). Base URL: `https://agentres.dev` Use this guide for the workflow; use `https://agentres.dev/openapi.json` only when you need exact schemas, enum values, pricing metadata, or error details. ## 1. Setup ### 1.1 How You Pay This document covers the pay-per-use wallet path: $0.01 USDC per instant booking, $3.00 USDC per reservation job, $0.01 USDC per wait-time query, paid onchain via x402 (Base/Solana) or MPP (Tempo). No signup — a funded wallet is the account. ### 1.2 Install and Wallet Setup When the user says `set up https://agentres.dev/skill.md`, `setup`, or otherwise asks to install or configure Agentic Reservations, install or update this exact document from `https://agentres.dev/skill.md` as the user-level `agentres` skill immediately — do not summarize it, present a plan, or ask whether to proceed. Detect the current client and use its personal skill directory: - Claude Code: `~/.claude/skills/agentres/SKILL.md`; the user invokes it as `/agentres`. - Codex: `~/.codex/skills/agentres/SKILL.md`; the user invokes it as `$agentres`. - Another Agent Skills-compatible client: use that client's documented user-level skills directory. Use the current client's supported skill installer when available. If it cannot install an HTTP skill URL, write the fetched document to the exact path above. Keep both the directory name and frontmatter `name` exactly `agentres`. Do not install copies for other clients or into the current project unless the user asks. There is no credential to store on this path — a funded wallet is the account — so after installing, confirm the installed path and the invocation for the user's client, then continue below. If the API later returns an error code or field this document does not explain, the installed copy is stale — re-fetch `https://agentres.dev/skill.md` and reinstall it the same way before improvising. Make requests through your wallet client (an x402/MPP-capable HTTP client or payments tool) — it handles the payment challenge automatically, so call every endpoint like a normal HTTP API. x402 settles USDC on Base/Solana mainnet; MPP settles on Tempo. Use whichever protocol your wallet supports. - Every endpoint on this path is payment-gated, including the $0 identity endpoints — route **all** calls through the wallet client, not just the paid ones. A 402 on an uninstrumented request is normal, not an error. The only exception is the endpoint explicitly marked public below (`GET /api/reservation-jobs/options`), which needs no auth at all. - Paid endpoints: `POST /api/book` ($0.01 USDC), `POST /api/reservation-jobs/create` ($3.00 USDC), the wait-time queries `GET /api/wait-times` / `GET /api/wait-times/history` ($0.01 USDC each), and the restaurant-discovery queries under `GET /api/discover/restaurants` ($0.01 USDC each). Everything else is a free identity call. - Identity calls accept two proofs; either works, and wallet clients handle both automatically: - **SIWX (preferred)**: sign the CAIP-122 message from the `sign-in-with-x` extension in the identity endpoint's $0 402 `PAYMENT-REQUIRED` header (SIWE on Base, SIWS on Solana) and retry with the `SIGN-IN-WITH-X` header. Nonces are single-use — fetch a fresh challenge per request. Paid 402s never carry this extension — a paid endpoint can only be satisfied by payment. - **Zero-dollar payment proof**: a $0 `PAYMENT-SIGNATURE` payload signed against the challenge accepts (cryptographically verified, never settled) — works with any x402 payment-capable client, no SIWX support needed. - Pay only for success: a failed instant booking is refunded automatically in the response; a canceled/failed/timed-out reservation job becomes `refund_pending` — claim it via `POST /api/reservation-jobs//refund`. - Never send `x-agent-key` or `userId` on this path. - No wallet client available? The raw challenge/retry handshake is documented in `https://agentres.dev/openapi.json` (402 response headers and security schemes). ### 1.3 Account and Resy Link 1. `POST /api/account` with body `{ "email": "resy@example.com" }` — the user's Resy.com email (ask once if not already given; email is contact metadata, never identity). Treat this as internal setup — frame it to the user as "linking Resy", not creating an account. Use the returned `account_id` and `email` as the account identity. 2. `GET /api/me` — returns `account_id`, `email`, `resy_linked`. If `resy_linked: true`, setup is complete. 3. If not linked, link Resy via email OTP, reusing the email from step 1: - Request code: `POST /api/link-resy` with `{ "em_address": "user@example.com" }`. Tell the user to check their email for the 6-digit Resy code. Stop and wait. - Verify: `POST /api/link-resy` with `{ "em_address": "user@example.com", "code": "123456" }`. Use field `em_address`, not `email`. Strip spaces and dashes from pasted codes. If verification fails, show the raw error and ask for the code again. Auth errors during setup: on `401`, retry `POST /api/account` with the user's Resy.com email, then retry `GET /api/me`. ## 2. Instructions Precondition for every flow below: Setup (section 1) is complete — `GET /api/me` returns `resy_linked: true`. If you have not verified this in the current session, do it before starting, so auth problems surface here rather than after the user has confirmed a booking. A `401` mid-flow means setup is missing — go back to section 1. Exception: wait-time queries (2.4) and restaurant discovery (2.5) need only payment auth (sections 1.1–1.2) — no Resy link. Once setup is complete, if the user has not already said what they want, present these options and wait for a choice — do not pick one for them: 1. **Book a table now** — instant booking at any restaurant on Resy (section 2.1). 2. **Request a hard-to-get table** — a reservation job for restaurants whose tables are not bookable yet; we book the moment reservations open (section 2.2). Offer to show which restaurants support this — if the user wants the list, fetch `GET /api/reservation-jobs/options` (public, no auth) and present the restaurant names. 3. **Check live wait times** — current or historical walk-in wait times at monitored locations (section 2.4). If the user asks how busy a restaurant is or how long the line is right now, skip the menu and go straight there. 4. **Discover restaurants** — recommendations from the Blackbird restaurant network, filterable by neighborhood or city, with daily-refreshed specials/offers (section 2.5). If the user asks for recommendations ("find me a spot in the West Village"), skip the menu and go straight there. If the user already named a restaurant, date, or goal, skip the menu and start the matching flow. Rules for every request: - Every POST uses `content-type: application/json`. - Do not guess IDs, dates, tokens, or config IDs. Ask one question at a time. - Resolve relative dates from the current date/timezone, state the absolute date before booking, and ask if ambiguous. - Show raw API errors: HTTP status, `error.code`, `error.message`, and `error.next_step` if present. Follow `error.next_step` unless it conflicts with a confirmation rule. - Never replay a payment credential: a reused settlement proof returns `409 DUPLICATE_PAYMENT` with the original `charge_id` and its status — report that outcome instead of retrying. - If a booking or job-creation POST fails at the transport level (timeout, dropped connection — no HTTP response), the request — and its payment — may still have gone through. Before any retry, check `GET /api/reservations` (or `GET /api/reservation-jobs`) to confirm the first attempt did not land; never sign a fresh payment for the same request until then. - Booking, reservation-job creation, and cancellation always require a fresh, explicit user confirmation. Only "yes", "confirm", "book it", "go ahead", or an exact restatement of the offered slot after a single confirmation summary (for example, "book 9pm") counts. If the answer is ambiguous, clarify. Do not proceed. ### 2.1 Normal Booking 1. `GET /api/search?query=&city=` — `query` required; `city` optional (default `nyc`) and must be one of: nyc, los-angeles, san-francisco, chicago, miami, washington-dc, boston, austin, seattle, las-vegas, philadelphia, atlanta, denver, nashville, san-diego, new-orleans, houston, dallas, cape-coral, fort-myers, naples, portland, minneapolis, detroit, charleston, oakland, phoenix, scottsdale, san-antonio, salt-lake-city, sacramento, san-jose, honolulu, charlotte, raleigh, durham, savannah, asheville, memphis, louisville, baltimore, pittsburgh, columbus, cleveland, cincinnati, indianapolis, kansas-city, st-louis, milwaukee, richmond, tampa, st-petersburg, orlando, jacksonville, fort-lauderdale, palm-beach, sarasota, hamptons, london, toronto, paris, mexico-city, sydney, amsterdam, berlin, madrid, barcelona, tokyo, dubai, montreal. Use the returned `venue_id`; never invent one. 2. `GET /api/availability?venue_id=&party_size=&day=` — all params required. Present the `slots[]` clearly and remember the chosen slot's `config_id`. For broad requests, search candidate venues, check availability in parallel, and filter by the user's time/date/party constraints. 3. Confirm: show a summary — venue, date, time, party size — and disclose that booking costs $0.01 USDC, refunded only if it fails. Wait for an explicit yes. 4. Fetch fresh `GET /api/availability` immediately before booking (config IDs expire), reselect the confirmed slot, then `POST /api/book` with `{ "venue_id": "92613", "config_id": "rgs://...", "party_size": 2, "day": "2026-10-04" }`. Returns `success`, `kind`, `charge_id`, `reservation_id`, and `resy_token`. A failed paid booking includes an automatic `refund` object — fetch availability again before offering new slots. If a supported restaurant has no availability because its booking window has not opened yet, offer a reservation job instead (2.2). Otherwise offer nearby venues or another time/date. ### 2.2 Reservation Jobs (book when reservations open) Use when the user wants a table that is not bookable yet — these restaurants open reservations on a schedule. Reservation jobs are best-effort. We attempt the booking the moment reservations open; if your preferred time is taken, we book the closest available time. A booking is not guaranteed. 1. `GET /api/reservation-jobs/options` (public, no auth) — returns the supported restaurants, how many days in advance each booking window opens (`drop_days_in_advance`), and optional `closed_days`. Service hours are not listed: pick a normal dining `preferred_hour` and, if the venue is not serving then, the create call fails with `OUTSIDE_SERVICE_HOURS` before any payment is taken and returns that day's bookable windows — retry with a time from those. Exact booking-window opening times are not published; the create response returns the scheduled `drop_time`. Only listed restaurants support reservation jobs; tell the user if theirs is not. 2. Ask for the user's preferred reservation time if they have not given one — do not silently fall back to the 18:00 default on a paid job. 3. Confirm: show a summary — restaurant, reservation date, party size, preferred time (state that the job is best-effort: it targets the preferred time, books the closest available slot if that time is taken, and a booking is not guaranteed), that the job fires automatically when the booking window opens (`drop_days_in_advance` days before the reservation date), and the price ($3.00 USDC). Wait for an explicit yes. 4. `POST /api/reservation-jobs/create` with either `venue_id` or `restaurant` (the name) plus the details, e.g. `{ "restaurant": "4 Charles", "party_size": 2, "reservation_date": "2026-10-13", "preferred_hour": 19, "preferred_minute": 30 }` (`preferred_hour`/`preferred_minute` default to 18:00 if omitted). Report the returned `job_id` and `drop_time` (ISO UTC — convert it to the user's local time when reporting). 5. Check progress with `GET /api/reservation-jobs/`; list jobs with `GET /api/reservation-jobs?limit=10&status=queued`. 6. Cancel a pending job (confirm with the user first): `POST /api/reservation-jobs//cancel` — running jobs become `cancel_requested`. No automatic refund: the charge becomes `refund_pending`; claim it with `POST /api/reservation-jobs//refund` (idempotent — a double-claim returns the existing refund). Claim the same way when a job ends `failed` or `timed_out`. 7. `409 ACTIVE_JOB_EXISTS`: an active job already covers that venue and booking window; the duplicate payment was refunded automatically. Report the existing `job_id`; do not retry. ### 2.3 Managing Reservations - List: `GET /api/reservations?type=upcoming&limit=10` (`type` is `upcoming` or `past`; optional `limit`, `offset`). Returns venue details, local display fields (`local_date`, `local_time`, `timezone`), confirmation-ready summaries, and `resy_token` — use `resy_token` only for cancellation. - Cancel a booked reservation: list first, match the user's request (ask a clarifying question if more than one could match), show the cancellation summary, wait for an explicit yes, then `POST /api/cancel` with `{ "resy_token": "" }`. Never guess a `resy_token` or reuse one from another conversation. - `POST /api/cancel` is for booked reservations; `POST /api/reservation-jobs//cancel` is for queued reservation jobs. ### 2.4 Live Wait Times Current and historical walk-in wait times for monitored restaurant locations (camera-based crowd data). Coverage is separate from Resy — wait-time location slugs are NOT Resy venue IDs. Each query costs $0.01 USDC; failed paid queries are refunded automatically. The Resy link (section 1.3) is not required, and these are read-only queries — no user confirmation needed, but do not poll them in a loop. 1. `GET /api/wait-times` — every monitored location with its slug, name, hours, and current status (`is_open`, `current_count`, `wait_minutes`, `captured_at`). This one call answers "how busy is X right now": find the restaurant by name in `locations` and read its status. 2. `GET /api/wait-times/history?location=&since=&until=&interval=1h` — line counts for one location over a UTC time range (slug from the call above). `interval` is `raw`, `1m`, `5m`, or `1h`; prefer `5m` or `1h` aggregates (`people_mean`/`min`/`max` per bucket) for questions like "when is it least busy". Buckets exist only for times the camera captured — gaps are normal. Bucket times are UTC; convert to the location's `timezone` when reporting. Reading the status: when `is_open` is false or `wait_minutes` is null there is no current wait reading — report the crowd count if present, otherwise say no reading is available. Always say how fresh the reading is (`captured_at`). An unknown slug on history returns `404 UNKNOWN_RESTAURANT` (the charge is refunded); `503 FEATURE_DISABLED` means wait times are temporarily unavailable. ### 2.5 Restaurant Discovery (Blackbird) Restaurant recommendations from the Blackbird restaurant network (a curated set of ~2,000 restaurants, mostly NYC and San Francisco). Each query costs $0.01 USDC; failed paid queries are refunded automatically. No Resy link required. Blackbird restaurant IDs are NOT Resy venue IDs — to book a discovered restaurant, feed its name into `GET /api/search` (section 2.1); prefer the shared city slugs (`nyc`, `san-francisco`), the only slugs `/api/search` accepts. 1. `GET /api/discover/restaurants` — search and filter the catalog. Parameters: - `query`: free text over name, cuisine, and area (e.g. `italian west village`). Bake every preference the user already stated in here — cuisine is in the full-text vector — instead of fetching everything and filtering by eye. - `city`: a shared slug (`nyc`, `san-francisco`) or a free-text address city name (`new york`, `brooklyn`). - `neighborhood`: matches exactly first, then retries once via full-text search; `neighborhood_match: "exact" | "fuzzy"` reports which. If both miss, the response carries `available_neighborhoods` for the city instead of an empty result. - `has_specials=true`: only restaurants with a confirmed current offer (~8% of the catalog) — reach for it whenever the user asks about deals. `false` means CONFIRMED none; never-checked restaurants are excluded from both, since their offers are unknown rather than absent. - `fields`: rows are SLIM BY DEFAULT — `{id, name, specials, specials_checked_at}` with specials trimmed to `{label, emoji?, fly_reward_bips?}`, at default limit 50 (max 200) — so a wide sweep is cheap. Pass `fields=full` when you need locations, cuisine, price, or complete specials on list rows (smaller 20/100 limits) — but only on an already-narrowed set, never to sweep a catalog (see **Recommend, don't list** below). 2. `GET /api/discover/restaurants/` — full detail: locations with addresses, coordinates, time zone, and live `open_hours` per location (`open_hours: null` = hours lookup failed). 3. `GET /api/discover/restaurants//specials` — the complete cached offers for one restaurant, e.g. Fly-reward multipliers ("Earn 10x Fly when you pay with Blackbird", `fly_reward_bips: 1000`). Reading specials, everywhere they appear: specials are served from a daily-refreshed cache, never fetched live. `specials: []` means checked with no current offers; `specials: null` means never checked — say "offers unavailable right now", never claim "no deals". `specials_checked_at` reports freshness (up to 24h stale). **Recommend, don't list.** You are the ranking layer — the API returns the neighborhood's full catalog ordered **alphabetically**, not by quality or relevance. It carries no quality signal at all: rank from external evidence when you have it, or clearly label the ordering as your own editorial judgment. - Check `total` before presenting. A popular neighborhood can hold 40+ restaurants, and alphabetical paging means page 1 is just the A–M names — curating from one page silently ignores the rest. When `total` exceeds the page and the request is broad, narrow with `query`, ask ONE clarifying question (cuisine or budget — not both), or page the slim rows through the remaining offsets so you have seen every candidate before choosing. - For broad recommendation requests ("10 good restaurants in X"), sweep-then-shortlist is the fixed order — it overrides any per-field urge to see detail early. Fetch ALL candidates with the default slim rows first (paging offsets as needed), shortlist from that sweep, then fetch detail only for the finalists — the per-restaurant detail route, or one `fields=full` query narrowed to the shortlist. Never request `fields=full` across the entire catalog. - Present a shortlist of ~5 diverse picks, each with name and any current specials; lead with restaurants that have current offers when the user cares about deals. Mention how many candidates the neighborhood had ("40 spots in the West Village — here are 5 standouts") and offer to go deeper or narrower. - When the ask needs cuisine/price curation ("cheap italian"), bake the terms into `query` — cuisine is in the full-text vector — so the slim sweep arrives pre-filtered; reach for `fields=full` only on that narrowed set, never as the first fetch. For "recs in the West Village, mention specials", one call with `neighborhood=west+village` is enough: slim rows already show every candidate's offers. The catalog refreshes weekly (`synced_at`); open hours on the detail route ARE still fetched live. Every response includes a `charge_id` (each page is its own paid query). These are read-only queries — no user confirmation needed, but do not poll in a loop.