{"service":"booking-layer","version":"0.4.0","summary":"Programmatic booking. Live: hotels. Coming soon: flights. Submit a signed proposal (Intent mandate: budget + constraints + details); search within its scope; book if a match exists. Pay via x402/USDC or a card gateway. One-shot or standing/revocable; non-custodial escrow; signed receipts. No login.","catalogue":"GET /v1/catalogue — all bookable services, fees, and required details.","conventions":{"money":"All monetary amounts are STRINGS in USDC atomic units (6 decimals): $1 = '1000000'. Every such field ends in the suffix `Atomic` (e.g. priceAtomic, amountAtomic, feeAtomic, perBookingAtomic) — USDC is the only asset (see payment.asset).","time":"Timestamps are ISO-8601 UTC strings; TTLs/windows/durations are seconds (integers).","identifiers":"Wallet addresses are EVM hex (checksummed). Search tokens, quote tokens, and booking ids are opaque strings — pass them back verbatim.","success":"A successful action returns `{ ok: true, ... }`.","errors":"A failure returns `{ ok: false, code, error }`: `code` is a STABLE machine-branchable string (e.g. 'budget_exceeded', 'details_required', 'authorization_revoked'); `error` is human-readable. HTTP status mirrors the class — 400 bad input · 402 payment required · 403 auth/revoked · 409 conflict · 410 expired · 422 missing details · 429 rate-limited · 502 supplier failure."},"flow":[{"step":1,"name":"discover","call":"GET /v1/schema","does":"Read this document; copy the intent EIP-712 type + example."},{"step":2,"name":"quote","call":"POST /v1/quote","send":{"kind":"hotel","location":"Paris","when":"2026-07-10","until":"2026-07-12","partySize":2},"get":"{ quote: { quoteToken, offer, payment } } — offer.priceAtomic is the price; payment is the x402 requirement."},{"step":3,"name":"human signs intent (ONCE)","actor":"human wallet","does":"EIP-712 sign `intent.eip712` (domain+types) over your filled-in intent. Reusable across many bookings within its budget+scope+expiry."},{"step":4,"name":"agent signs payment","actor":"agent wallet","does":"Sign an EIP-3009 TransferWithAuthorization over USDC matching quote.payment. The `value` may exceed the price by a MARGIN (up to budget) to absorb small price changes; the layer captures the actual price and refunds the rest."},{"step":5,"name":"book","call":"POST /v1/book","send":{"quoteToken":"<from step 2>","signedIntent":"<from step 3>"},"header":"X-PAYMENT: base64(payment payload from step 4)","get":"{ ok:true, booking } with a signed receipt. Without X-PAYMENT you get HTTP 402 + the requirement — sign and resend."},{"step":6,"name":"(optional) basket","call":"POST /v1/plans then POST /v1/plans/:id/book","does":"Quote several services into a plan; the SAME one signed intent authorizes them all; each books independently (own escrow/capture/refund)."}],"intent":{"humanSignsOnce":true,"eip712":{"domain":{"name":"BookingLayerMandate","version":"1","chainId":84532,"verifyingContract":"0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff"},"types":{"IntentMandate":[{"name":"principal","type":"address"},{"name":"agent","type":"address"},{"name":"constraints","type":"BookingConstraints"},{"name":"budget","type":"Budget"},{"name":"detailsCommitment","type":"bytes32"},{"name":"nonce","type":"string"},{"name":"expiry","type":"uint256"}],"Budget":[{"name":"items","type":"ItemBudget[]"}],"ItemBudget":[{"name":"kind","type":"string"},{"name":"budgetAtomic","type":"uint256"}],"BookingConstraints":[{"name":"kinds","type":"string[]"},{"name":"where","type":"Place"},{"name":"origin","type":"Place"},{"name":"when","type":"When"},{"name":"party","type":"Party"},{"name":"facets","type":"Facets"}],"Place":[{"name":"continent","type":"string"},{"name":"country","type":"string"},{"name":"region","type":"string"},{"name":"city","type":"string"},{"name":"district","type":"string"},{"name":"postcode","type":"string"},{"name":"venue","type":"string"},{"name":"code","type":"string"}],"When":[{"name":"from","type":"string"},{"name":"to","type":"string"},{"name":"timeFrom","type":"string"},{"name":"timeTo","type":"string"},{"name":"minDurationDays","type":"uint256"},{"name":"maxDurationDays","type":"uint256"},{"name":"daysOfWeek","type":"string[]"}],"Party":[{"name":"maxTravelers","type":"uint256"},{"name":"maxRooms","type":"uint256"}],"Facets":[{"name":"cabinClass","type":"string"},{"name":"nonstopOnly","type":"bool"},{"name":"minStars","type":"uint256"},{"name":"cuisine","type":"string"},{"name":"seating","type":"string"},{"name":"refundableOnly","type":"bool"},{"name":"freeCancellation","type":"bool"},{"name":"board","type":"string"},{"name":"amenities","type":"string[]"},{"name":"propertyType","type":"string"}]},"primaryType":"IntentMandate"},"example":{"principal":"0xYOUR_HUMAN_WALLET_ADDRESS______________","agent":"0xYOUR_AGENT_WALLET_ADDRESS______________","constraints":{"kinds":["hotel","restaurant"],"where":{"continent":"","country":"FR","region":"","city":"Paris","district":"Le Marais","postcode":"","venue":"","code":""},"origin":{"continent":"","country":"","region":"","city":"","district":"","postcode":"","venue":"","code":""},"when":{"from":"2026-07-01","to":"2026-07-31","timeFrom":"19:00","timeTo":"","minDurationDays":1,"maxDurationDays":4,"daysOfWeek":["Fri","Sat"]},"party":{"maxTravelers":2,"maxRooms":1},"facets":{"cabinClass":"","nonstopOnly":false,"minStars":4,"cuisine":"italian","seating":"","refundableOnly":false,"freeCancellation":true,"board":"","amenities":[],"propertyType":""}},"budget":{"items":[{"kind":"hotel","budgetAtomic":"300000000"},{"kind":"restaurant","budgetAtomic":"150000000"}]},"details":{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","phoneCountryCode":"44","phoneNumber":"2080160508","dateOfBirth":"1990-01-01","gender":"F","nationality":"GB","passport":{"number":"X1234567","issueCountry":"GB","expiryDate":"2032-01-01","nationality":"GB"}},"nonce":"intent_c3f475ac69e6c3307aea3717","expiry":1783579854},"fieldGuide":{"agent":"The agent wallet you'll pay from. 0x000…000 = any bearer (less safe).","constraints.kinds":"Allowed booking types; subset of [hotel, flight]. [] = any.","constraints.where":"Graduated geography; set any levels: continent → country → region → city → district → postcode → venue. Empty level = wildcard.","constraints.origin":"Flights only: departure. Use `code` for an airport/station (e.g. LHR).","constraints.when":"from/to = date range (YYYY-MM-DD); timeFrom/timeTo = time-of-day window (HH:MM); min/maxDurationDays = nights/length; daysOfWeek = e.g. ['Fri','Sat'].","constraints.party":"maxTravelers (diners/guests/passengers), maxRooms (hotels). 0 = any.","constraints.facets":"Per-service: cabinClass & nonstopOnly (flight), minStars (hotel), cuisine & seating (restaurant), refundableOnly & freeCancellation (any). ''/0/false = unset.","budget.items":"Per-ITEM budgets: [{ kind, budgetAtomic }] — each booking kind has its OWN cap (covers the item + its add-ons). You deposit the SUM of them, so one item can never consume another's budget.","expiry":"Unix seconds; the intent is invalid after this."}},"details":{"description":"Traveler/customer details — part of the SIGNED Intent mandate (the human confirms them). Required fields depend on the kind; a booking is rejected (422) before any payment if required fields are missing.","fields":{"firstName":"string (or sign with `name` = 'First Last')","lastName":"string","email":"required for hotels and flights","phoneCountryCode":"e.g. '44' — required for flights","phoneNumber":"national part — required for flights","dateOfBirth":"YYYY-MM-DD — required for flights","gender":"'M' | 'F' | 'X' — required for flights","nationality":"ISO-2 — required for flights","passport.number":"required for flights","passport.issueCountry":"ISO-2 — required for flights","passport.expiryDate":"YYYY-MM-DD — required for flights","passport.nationality":"ISO-2 — required for flights"},"requiredByKind":{"hotel":["firstName","lastName","email"],"flight":["firstName","lastName","email","phoneCountryCode","phoneNumber","dateOfBirth","gender","nationality","passport.number","passport.issueCountry","passport.expiryDate","passport.nationality"],"restaurant":["firstName","lastName"]}},"payment":{"protocol":"x402","scheme":"exact","network":"eip155:84532","asset":"0x036CbD53842c5426634e7929541eC2318f3dCF7e","assetName":"USDC","header":"X-PAYMENT (base64-encoded JSON payment payload)","note":"Authorize `value` >= price; a higher value is your margin and is refunded if unused.","fundingModels":{"self":"scheme 'exact' — sign an EIP-3009 USDC authorization from the agent's own wallet.","pull":"scheme 'spend-permission' — pull from the human's wallet within a standing allowance they signed once (refunds return to the human)."}},"budget":{"minUsdc":1,"maxUsdc":100,"note":"Escrow a budget (min $1; BETA cap $100 per budget). Bookings draw down from it; the unused remainder is refunded on completion. $1 fee only if you rescind/abandon while inventory existed."},"addOns":{"board":"Hotel rates expose their meal basis at `offer.attributes.facets.board` (e.g. 'Room Only', 'Bed and Breakfast', 'Half Board'); each board is a distinct offer — pick the rate whose board you want. `offer.addOns[]` lists the included/purchasable extras.","requireBoard":"A human can REQUIRE a board in the signed intent via `constraints.facets.board` (keyword, e.g. 'breakfast'); it's enforced at book time — a non-matching offer is rejected.","requests":"At POST /v1/book you may attach `specialRequests` (free-text, e.g. 'late check-in, high floor') and `addOnCodes` (selected from `offer.addOns`); these flow to the supplier as remarks (best-effort, may not be honored)."},"postBooking":{"retrieve":"GET /v1/bookings/:id — the confirmed booking + its signed receipt.","cancel":"POST /v1/bookings/:id/cancel — sign an EIP-712 BookingCancellation (principal or paying agent); we cancel with the supplier and refund the agent per the cancellation policy (free-cancellation → full; non-refundable → nothing).","cancelBudgetPart":"POST /v1/budget/:token/cancel { kind } — cancel ONE part of a budget-funded plan (sign BookingCancellation over `<token>:<kind>`); refunded per policy (pre-settlement it's returned at settlement; post-settlement it's sent back)."},"authorizationModes":{"oneShot":"Default: the signed intent is reusable until its expiry.","standing":"POST /v1/intents/signing-link with standing:true → 'always allow' (long-lived).","revoke":"POST /v1/authorizations/revoke (human signs a revocation of the nonce) turns it off instantly."},"escrow":{"model":"non-custodial","contract":"0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff","behavior":"capture(actual) → merchant / refund(rest+failures+timeout) → agent; the agent cannot pull funds once committed."},"serviceFees":{"hotel":"2 USDC","flight":"3 USDC"},"outcomes":[{"code":"ok","http":200,"meaning":"Booked. `booking` includes a signed receipt."},{"code":"payment_required","http":402,"meaning":"No/again-needed X-PAYMENT. Sign quote.payment and resend with the header."},{"code":"authorization_failed","http":403,"meaning":"The intent doesn't cover this booking (scope, delegated agent, kind, or budget). `reasons` lists each violation precisely."},{"code":"details_required","http":422,"meaning":"Required traveler `details` for this kind are missing. `reasons` lists exactly which fields — fill them and retry. Checked before any payment. See `details.requiredByKind`."},{"code":"payment_invalid","http":402,"meaning":"Payment failed verify/settle (cancelled, replayed, insufficient, wrong destination). Nothing booked or locked."},{"code":"price_exceeded","http":409,"meaning":"The live price rose above your authorized amount (margin too small) or the budget. Fully refunded; re-quote and authorize more."},{"code":"quote_invalid","http":410,"meaning":"Quote unknown or expired. Re-quote."},{"code":"booking_failed","http":502,"meaning":"Supplier failed to confirm. You were auto-refunded."},{"code":"bad_request","http":400,"meaning":"Malformed input. `hint` says what to fix."}],"endpoints":{"schema":"GET /v1/schema","discovery":"GET /v1/discovery","search":"POST /v1/search","quote":"POST /v1/quote","book":"POST /v1/book","booking":"GET /v1/bookings/:id","verifyReceipt":"POST /v1/receipts/verify","createPlan":"POST /v1/plans","bookPlan":"POST /v1/plans/:id/book","bookPlanItem":"POST /v1/plans/:id/items/:itemId/book"}}