{"openapi":"3.1.0","info":{"title":"Agent Restaurant Reservation API","version":"1.0.0","description":"Agent-first API for searching and booking restaurant reservations via Resy. Supports MPP (Tempo) and x402 (Base) wallet auth, plus API key auth. Booking costs $0.01 USDC.","x-guidance":"This API lets an agent book restaurant reservations on Resy on behalf of a user. Authentication: use a per-user API key (x-agent-key header, starts with agentres_) or a crypto wallet (x402/MPP). API key auth carries user identity — no userId parameter needed. See /skill.md for full setup. IMPORTANT: Start by calling GET /api/me to check if the user already has an account and linked Resy. If resy_linked is true, skip straight to searching. If you get a 401 (wallet not linked), start setup. Setup is presented to the user as a single action: \"link your Resy account\". Ask the user for their Resy email ONCE and reuse it for every setup call. Never mention \"creating an account\" in user-facing messages — it's internal plumbing. Setup (only if needed): 1) POST /api/account with { \"email\": \"<user's Resy email>\" } — creates the account and links the wallet. SKIP this call if GET /api/me returned 200 (account already exists). Do not narrate this call to the user. 2) POST /api/link-resy with { \"em_address\": \"<same Resy email>\" } — sends an OTP code to the user's Resy email.    Then POST /api/link-resy with { \"em_address\": \"<same Resy email>\", \"code\": \"<code>\" } — submit the code to complete linking. Booking workflow (after setup): 3) GET /api/search?query=<name> — search for restaurants by name. 4) GET /api/availability?venue_id=<id>&party_size=<n>&day=<YYYY-MM-DD> — check available time slots. 5) POST /api/book with { \"venue_id\", \"config_id\", \"party_size\", \"day\" } — book a reservation (costs $0.01 USDC via wallet, free with API key). Ad-hoc: GET /api/reservations?type=upcoming — use when the user asks \"what reservations do I have?\" or similar. Returns the user's existing reservations with full venue details inlined. Identity-only ($0). Cancel flow: when the user asks to cancel a reservation, you MUST first call GET /api/reservations to locate the reservation and extract its resy_token field, then call POST /api/cancel with { \"resy_token\" } to cancel. Never guess resy_token. Always confirm with the user before cancelling. Identity-only ($0). CRITICAL: Before calling /api/book, always present a full booking summary (restaurant, date, time, party size, price) and wait for an EXPLICIT \"yes\" confirmation from the user. Never book on implicit intent. All error responses include \"retryable\" (boolean) and \"next_step\" (what to do) fields. Follow next_step to recover from errors."},"x-service-info":{"categories":["reservations","restaurants"]},"servers":[{"url":"https://agentres.dev"}],"paths":{"/api/me":{"get":{"operationId":"getProfile","summary":"Get user profile and Resy link status","description":"Returns the authenticated user's profile including whether their Resy account is linked. Identity-only ($0 auth).","x-guidance":"Call this FIRST to check if the user is already set up. If resy_linked is true, skip account creation and link-resy — go straight to search. If you get 401, the wallet isn't linked yet — start with POST /api/account.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false,"description":"No input required — user identity is derived from the auth header."}}}},"responses":{"200":{"description":"User profile","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"email":{"type":"string","nullable":true},"resy_linked":{"type":"boolean"},"resy_linked_at":{"type":"string","nullable":true}}}}}},"401":{"description":"Wallet not linked to any account","x-error-codes":["WALLET_NOT_LINKED","UNAUTHORIZED"]},"402":{"description":"Payment Required"}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}},"/api/account":{"post":{"operationId":"createAccount","summary":"Create account and link wallet","description":"Create a new user account. With wallet auth, also links the calling wallet to the account. Identity-only ($0 auth).","x-guidance":"Internal setup step for the Resy linking flow — not a user-facing action. Call this with the user's RESY email (the same one you'll pass to /api/link-resy) when GET /api/me returns 401 (wallet not linked). Do NOT narrate this call to the user — frame the overall setup as 'linking Resy', not 'creating an account'. Skip entirely if GET /api/me returned 200 (account already exists). A 200 response (already exists) is fine — continue to link-resy.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Email address for the account"}}}}}},"responses":{"200":{"description":"Account already exists","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"created":{"type":"boolean"}}}}}},"201":{"description":"Account created"},"402":{"description":"Payment Required"},"409":{"description":"Account or wallet already linked","x-error-codes":["ACCOUNT_ALREADY_EXISTS","WALLET_ALREADY_LINKED"]}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}},"/api/link-resy":{"post":{"operationId":"linkResyAccount","summary":"Link a Resy account via email OTP","description":"Two-step OTP flow. Step 1: send { em_address } to request a code. Step 2: send { em_address, code } to verify and link. Identity-only ($0 auth).","x-guidance":"Two calls required, same em_address on both. First call (no code): sends OTP to the user's Resy email. Ask the user for the 6-digit code. Second call (with code): completes linking. Use the SAME email you passed to /api/account — the user should only ever be prompted for one email across the entire setup flow. The user experiences this as 'linking Resy', not as a multi-step account flow.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["em_address"],"properties":{"em_address":{"type":"string","format":"email","description":"Resy account email address"},"code":{"type":"string","description":"OTP code from email (omit for step 1)"}}}}}},"responses":{"200":{"description":"Code sent or account linked","content":{"application/json":{"schema":{"type":"object","properties":{"step":{"type":"string","enum":["code_sent","linked"]},"message":{"type":"string"},"resy_user_id":{"type":"integer"}}}}}},"402":{"description":"Payment Required"},"422":{"description":"Resy account has no payment method","x-error-codes":["RESY_NO_PAYMENT_METHOD"]},"502":{"description":"Resy OTP request or verification failed","x-error-codes":["RESY_VERIFICATION_FAILED"]}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}},"/api/search":{"get":{"operationId":"searchVenues","summary":"Search Resy for restaurants by name","description":"Searches the Resy venue database in a given city and returns the top 5 matches with venue IDs. Results are ranked by geographic proximity to the city center, so passing the correct `city` is important when the user mentions one. Identity-only ($0 auth).","x-guidance":"Use this to find venue IDs. Extract the city from the user's natural-language request ('sushi in LA' → city=los-angeles, 'dinner in SoHo' → city=nyc). If the user doesn't mention a city, city defaults to nyc. Remember the venue_id for the next steps. If city is not in the supported enum, do NOT guess lat/long — pick the closest supported city and tell the user, or ask them to clarify.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string"},"description":"Restaurant name, cuisine, or keyword to search for"},{"name":"city","in":"query","required":false,"schema":{"type":"string","enum":["nyc","los-angeles","san-francisco","chicago","miami","washington-dc","boston","austin","seattle","las-vegas","philadelphia","atlanta","denver","nashville","san-diego","new-orleans","houston","dallas","london","toronto","paris","mexico-city","sydney"],"default":"nyc"},"description":"City slug for geo-ranked search. Extract from the user's prompt. Defaults to nyc if omitted."}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"venue_id":{"type":"integer"},"name":{"type":"string"},"neighborhood":{"type":"string"},"cuisine":{"type":"array","items":{"type":"string"}},"rating":{"type":"number","nullable":true}}}},"city":{"type":"string","enum":["nyc","los-angeles","san-francisco","chicago","miami","washington-dc","boston","austin","seattle","las-vegas","philadelphia","atlanta","denver","nashville","san-diego","new-orleans","houston","dallas","london","toronto","paris","mexico-city","sydney"],"description":"Echo of the resolved city slug used for this search."}}}}}},"400":{"description":"Missing query parameter","x-error-codes":["INVALID_INPUT"]}}}},"/api/availability":{"get":{"operationId":"checkAvailability","summary":"Check available reservation time slots","description":"Returns available reservation slots for a venue on a given day. Response includes venue_name for display. Identity-only ($0 auth).","x-guidance":"Use the venue_id from /api/search. Present the results clearly to the user — show time, seating type. Remember the config_id of the slot they choose, you'll need it for /api/book. Config IDs expire — always fetch fresh availability before booking.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"parameters":[{"name":"venue_id","in":"query","required":true,"schema":{"type":"string"},"description":"Resy venue ID (from /api/search)"},{"name":"party_size","in":"query","required":true,"schema":{"type":"integer","minimum":1},"description":"Number of guests"},{"name":"day","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Date in YYYY-MM-DD format"}],"responses":{"200":{"description":"Available slots","content":{"application/json":{"schema":{"type":"object","properties":{"venue_id":{"type":"string"},"venue_name":{"type":"string","nullable":true,"description":"Restaurant name for display"},"day":{"type":"string"},"party_size":{"type":"integer"},"slots":{"type":"array","items":{"type":"object","properties":{"config_id":{"type":"string","description":"Use this value when booking"},"type":{"type":"string"},"time_start":{"type":"string","description":"e.g. '7:30 PM'"}}}}}}}}},"400":{"description":"Invalid input","x-error-codes":["INVALID_INPUT"]},"402":{"description":"Payment Required"},"502":{"description":"Failed to fetch from Resy","x-error-codes":["NO_AVAILABILITY"]}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}},"/api/reservations":{"get":{"operationId":"listReservations","summary":"List the user's existing Resy reservations","description":"Returns the authenticated user's upcoming or past reservations with full venue details (name, address, neighborhood, cuisine, phone) inlined. Requires a linked Resy account. Identity-only ($0 auth).","x-guidance":"Use this when the user asks what reservations they have, wants to review an upcoming booking, or references an existing reservation. Default type=upcoming. Each reservation includes the full venue object — no extra calls needed to display restaurant details. If the user has no linked Resy account, you'll get 400 NO_LINKED_ACCOUNT — direct them through /api/link-resy first.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["upcoming","past"],"default":"upcoming"},"description":"Which reservations to list. Defaults to upcoming."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":10},"description":"Max reservations to return (1-50)."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"Pagination offset."}],"responses":{"200":{"description":"User's reservations","content":{"application/json":{"schema":{"type":"object","properties":{"reservations":{"type":"array","items":{"type":"object","properties":{"reservation_id":{"type":"integer"},"resy_token":{"type":"string","description":"Pass to POST /api/cancel to cancel this reservation"},"venue":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"cuisine":{"type":"string","nullable":true},"rating":{"type":"number","nullable":true},"price_range_id":{"type":"integer","nullable":true},"url_slug":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"image_url":{"type":"string","nullable":true},"currency":{"type":"string"},"location":{"type":"object","properties":{"address_1":{"type":"string","nullable":true},"address_2":{"type":"string","nullable":true},"locality":{"type":"string","nullable":true},"region":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"neighborhood":{"type":"string","nullable":true},"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"time_zone":{"type":"string","nullable":true}}}}},"day":{"type":"string","format":"date"},"time_slot":{"type":"string","description":"HH:MM:SS local time"},"when":{"type":"string","description":"Combined datetime from Resy"},"num_seats":{"type":"integer"},"config_type":{"type":"string","description":"e.g. 'Main Dining Room'"},"status":{"type":"object","properties":{"finished":{"type":"boolean"},"no_show":{"type":"boolean"}}},"cancellation":{"type":"object","properties":{"allowed":{"type":"boolean"},"refund_cutoff":{"type":"string","nullable":true},"fee_amount":{"type":"number"},"fee_applies":{"type":"boolean"}}},"change":{"type":"object","properties":{"allowed":{"type":"boolean"},"cutoff":{"type":"string","nullable":true}}},"price":{"type":"number"},"payment_method":{"type":"object","nullable":true,"properties":{"card_type":{"type":"string","nullable":true},"last_4":{"type":"string","nullable":true}}},"share_link":{"type":"string","nullable":true},"is_pickup":{"type":"boolean"}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"type":{"type":"string","enum":["upcoming","past"]}}}}}},"400":{"description":"Invalid input or no linked Resy account","x-error-codes":["INVALID_INPUT","NO_LINKED_ACCOUNT"]},"402":{"description":"Payment Required"},"502":{"description":"Failed to fetch from Resy","x-error-codes":["INTERNAL_ERROR"]}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}},"/api/book":{"post":{"operationId":"bookReservation","summary":"Book a reservation ($0.01 USDC)","description":"Book a reservation at the given venue. Requires a linked Resy account. Use a config_id from /api/availability. Costs $0.01 USDC.","x-guidance":"CRITICAL: Before calling this endpoint, you MUST present a full booking summary (restaurant name, date, time, party size, price) and receive an EXPLICIT user confirmation ('yes', 'confirm', 'book it', or equivalent). Never book on implicit intent. If the user's response is ambiguous, a question, or non-affirmative — do NOT book, clarify instead. This rule has no exceptions — real money is charged. Use a fresh config_id from /api/availability (they expire quickly). If you get 409 (already_booked), tell the user they already have a reservation there.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0.010000"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["venue_id","config_id","party_size","day"],"properties":{"venue_id":{"type":"string","description":"Resy venue ID"},"config_id":{"type":"string","description":"Slot config token from /api/availability"},"party_size":{"type":"integer","minimum":1,"description":"Number of guests"},"day":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Date in YYYY-MM-DD format"}}}}}},"responses":{"200":{"description":"Reservation booked","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"kind":{"type":"string"},"resy_token":{"type":"string"},"reservation_id":{"type":"integer"}}}}}},"400":{"description":"Invalid input or no linked Resy account","x-error-codes":["INVALID_INPUT","NO_LINKED_ACCOUNT"]},"402":{"description":"Payment Required"},"409":{"description":"Already booked at this venue","x-error-codes":["ALREADY_BOOKED"]},"502":{"description":"Booking failed upstream","x-error-codes":["BOOKING_FAILED"]}},"security":[{"walletAuth":[]},{"x402Auth":[]}]}},"/api/cancel":{"post":{"operationId":"cancelReservation","summary":"Cancel an existing Resy reservation","description":"Cancels a reservation by resy_token. You MUST first call GET /api/reservations to get the resy_token for the reservation the user wants to cancel. Requires a linked Resy account. Identity-only ($0 auth).","x-guidance":"CRITICAL FLOW: (1) Call GET /api/reservations?type=upcoming to list the user's reservations, (2) Identify the reservation the user wants to cancel and extract its `resy_token` field from the response, (3) Present a confirmation summary to the user (restaurant, date, time, party size) and wait for explicit 'yes', (4) Call POST /api/cancel with { resy_token }. Never guess or fabricate resy_token — it must come from /api/reservations. Never cancel on implicit intent.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0"},"protocols":[{"mpp":{"method":"tempo","intent":"charge","currency":"0x20C000000000000000000000b9537d11c60E8b50"}},{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resy_token"],"properties":{"resy_token":{"type":"string","description":"Reservation token from GET /api/reservations response (field: resy_token)"}}}}}},"responses":{"200":{"description":"Reservation cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid input or no linked Resy account","x-error-codes":["INVALID_INPUT","NO_LINKED_ACCOUNT"]},"402":{"description":"Payment Required"},"502":{"description":"Resy cancel failed upstream","x-error-codes":["INTERNAL_ERROR"]}},"security":[{"agentKey":[]},{"walletAuth":[]},{"x402Auth":[]}]}}},"components":{"securitySchemes":{"agentKey":{"type":"apiKey","in":"header","name":"x-agent-key","description":"Per-user API key (starts with `agentres_`). The key carries user identity — no userId parameter needed. See /skill.md for setup instructions."},"walletAuth":{"type":"http","scheme":"payment","description":"MPP wallet auth (Tempo). Server returns 402 with WWW-Authenticate challenge. Sign and retry with Authorization: Payment header. Identity endpoints: amount '0'. Booking: 0.01 USDC. Chain: Tempo Mainnet (4217). Currency: USDC (0x20C000000000000000000000b9537d11c60E8b50). Recipient: 0xC295e19eB630E29a4Dd81f7242E6b51B49486d93."},"x402Auth":{"type":"apiKey","in":"header","name":"PAYMENT-SIGNATURE","description":"x402 wallet auth (Base). Send base64-encoded PaymentPayload via PAYMENT-SIGNATURE header. Identity endpoints: $0 payment. Booking: 0.01 USDC. Chain: Base (eip155:8453). Asset: USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913). Recipient: 0x2DD94d7CB82cb882E7599f020661A77AdF9fDaF7. Facilitator: https://api.cdp.coinbase.com/platform/v2/x402. 402 responses include PAYMENT-REQUIRED header."}}}}