# pickthehotel · intents.md

pickthehotel is an **answer engine for hotels**. Ask for the single best hotel that fits an intent in a
given city, and get one confident pick, decided by automated signals, never paid placement.

## Intent: `hotel.pick`

Return the best hotel in a city for one of four intents.

**HTTP**: three equivalent ways in, so any agent can reach it. Prefer the **path form** if your fetch tool drops query strings:

```
GET  https://pickthehotel.com/intents/zurich/iconic          # path form (survives query-string stripping)
GET  https://pickthehotel.com/intents?city=zurich&mode=iconic&near=Old%20Town

POST https://pickthehotel.com/intents
Content-Type: application/json

{ "city": "zurich", "mode": "iconic", "near": "Old Town" }
```

Path form: `/intents/<city>` (defaults to `iconic`) or `/intents/<city>/<mode>`; `/intents/world` returns the curated world list.

**Parameters**

| field | values | notes |
|---|---|---|
| `city` | a live city slug: every one is listed under **Live cities** below | `GET /intents` lists what's live; an unknown/not-live city returns `404`. |
| `mode` | `iconic` \| `afflux` \| `value` \| `budget` \| `family` | see below; unknown mode → `400`; omitting it defaults to `iconic`. |
| `near` | a famous place (`"Louvre"`, `"Tower Hill"`) **or** coordinates `{ "lat": .., "lng": .. }` | **optional**: anchors "central" to that place. A name matches a curated landmark else is geocoded; **coordinates power "a hotel near me now"** (pass the traveller's live location) and let you omit `city`: we route to the nearest live city. Omit `near` for the mode's default anchor. |
| `children` | `1` \| `2` \| `3` (family only) | how many children, the room must sleep 2 adults + this many. Defaults to 2. A single parent with the same headcount is covered (same rooms; priced ~as 2 adults). |
| `season` + `day` | `winter\|spring\|summer\|autumn` + `weekday\|weekend` | when the stay is, on two axes (non-family). Prices and value/budget ranking follow the chosen combo. Omit for the city's "typical" (median) stay. Also accepts a single `stay` key like `"summer-weekday"`. |
| `limit` | integer (default `3`, max `12`) | how many alternatives to return alongside the pick. |

The response echoes where "central" was measured from in `query.anchor` (`{ name, lat, lng, source }`), the `query.party`, the chosen `query.stay` (with its `tier`: `peak` \| `shoulder` \| `low`, computed per city), and the city's curated landmarks in `landmarks`. When a city has seasonal pricing, `seasons` lists every combo with its median price and tier. Other modes assume a standard 2-adult stay.

**Live cities**

<!-- live-cities:start — generated by scripts/gen-cities.mjs from cities.mjs; do not edit by hand -->
335 live cities across 6 regions:

- **Europe** (216): `zurich` Zürich · `paris` Paris · `london` London · `istanbul` Istanbul · `rome` Rome · `barcelona` Barcelona · `amsterdam` Amsterdam · `bodrum` Bodrum · `konya` Konya · `karaman` Karaman · `lisbon` Lisbon · `madrid` Madrid · `berlin` Berlin · `vienna` Vienna · `prague` Prague · `ermenek` Ermenek · `southendonsea` Southend-on-Sea · `milan` Milan · `venice` Venice · `turin` Turin · `munich` Munich · `frankfurt` Frankfurt · `geneva` Geneva · `lausanne` Lausanne · `marseille` Marseille · `nice` Nice · `cannes` Cannes · `lyon` Lyon · `montpellier` Montpellier · `brussels` Brussels · `copenhagen` Copenhagen · `gothenburg` Gothenburg · `oslo` Oslo · `budapest` Budapest · `krakow` Kraków · `stmoritz` St. Moritz · `gstaad` Gstaad · `grindelwald` Grindelwald · `athens` Athens · `naples` Naples · `dublin` Dublin · `manchester` Manchester · `edinburgh` Edinburgh · `stockholm` Stockholm · `tenerife` Tenerife · `palma` Palma de Mallorca · `grancanaria` Gran Canaria · `malta` Malta · `warsaw` Warsaw · `santorini` Santorini · `cappadocia` Cappadocia · `reykjavik` Reykjavík · `tromso` Tromsø · `kiruna` Kiruna (Icehotel) · `hamburg` Hamburg · `cologne` Cologne · `dusseldorf` Düsseldorf · `stuttgart` Stuttgart · `basel` Basel · `luxembourg` Luxembourg · `zagreb` Zagreb · `bratislava` Bratislava · `bologna` Bologna · `verona` Verona · `salzburg` Salzburg · `porto` Porto · `malaga` Málaga · `valencia` Valencia · `helsinki` Helsinki · `rhodes` Rhodes · `heraklion` Heraklion · `gyor` Győr · `ostrava` Ostrava · `perpignan` Perpignan · `olomouc` Olomouc · `eindhoven` Eindhoven · `girona` Girona · `katowice` Katowice · `montreux` Montreux · `duisburg` Duisburg · `bregenz` Bregenz · `trento` Trento · `chambery` Chambéry · `feldkirch` Feldkirch · `helsingborg` Helsingborg · `lund` Lund · `osnabruck` Osnabrück · `maribor` Maribor · `flensburg` Flensburg · `bellinzona` Bellinzona · `wels` Wels · `breda` Breda · `figueres` Figueres · `namur` Namur · `odense` Odense · `stpolten` St. Pölten · `arnhem` Arnhem · `kufstein` Kufstein · `halmstad` Halmstad · `aixenprovence` Aix-en-Provence · `florence` Florence · `nuremberg` Nuremberg · `strasbourg` Strasbourg · `antwerp` Antwerp · `hanover` Hanover · `rotterdam` Rotterdam · `leipzig` Leipzig · `dresden` Dresden · `bremen` Bremen · `ljubljana` Ljubljana · `thehague` The Hague · `gdansk` Gdańsk · `wroclaw` Wrocław · `essen` Essen · `lugano` Lugano · `mannheim` Mannheim · `graz` Graz · `karlsruhe` Karlsruhe · `freiburg` Freiburg · `innsbruck` Innsbruck · `poznan` Poznań · `como` Como · `dortmund` Dortmund · `utrecht` Utrecht · `aachen` Aachen · `bern` Bern · `brno` Brno · `kosice` Košice · `padua` Padua · `malmo` Malmö · `avignon` Avignon · `saarbrucken` Saarbrücken · `bolzano` Bolzano · `passau` Passau · `stgallen` St. Gallen · `lucerne` Lucerne · `zermatt` Zermatt · `interlaken` Interlaken · `engelberg` Engelberg · `flims` Flims & Laax · `neuchatel` Neuchâtel · `vitznau` Vitznau · `thun` Thun · `winterthur` Winterthur · `chur` Chur · `biel` Biel/Bienne · `baden` Baden · `zug` Zug · `nyon` Nyon · `lauterbrunnen` Lauterbrunnen Valley · `leukerbad` Leukerbad · `weggis` Weggis · `alicante` Alicante · `bucharest` Bucharest · `birmingham` Birmingham · `tirana` Tirana · `bristol` Bristol · `palermo` Palermo · `larnaca` Larnaca · `seville` Seville · `belgrade` Belgrade · `catania` Catania · `algarve` Algarve (Albufeira & Vilamoura) · `riga` Riga · `thessaloniki` Thessaloniki · `sofia` Sofia · `lanzarote` Lanzarote · `belfast` Belfast · `chania` Chania · `glasgow` Glasgow · `corralejo` Fuerteventura (Corralejo) · `nantes` Nantes · `pisa` Pisa · `bordeaux` Bordeaux · `dubrovnik` Dubrovnik · `izmir` Izmir · `vilnius` Vilnius · `cagliari` Cagliari · `leeds` Leeds · `funchal` Madeira (Funchal) · `newcastle` Newcastle · `split` Split · `zadar` Zadar · `bilbao` Bilbao · `liverpool` Liverpool · `toulouse` Toulouse · `bergen` Bergen · `paphos` Paphos · `tallinn` Tallinn · `clujnapoca` Cluj-Napoca · `varna` Varna · `skopje` Skopje · `jersey` Jersey (St Helier) · `sansebastian` San Sebastián · `rzeszow` Rzeszów · `augsburg` Augsburg · `genoa` Genoa · `reims` Reims · `ghent` Ghent · `lodz` Łódź · `trieste` Trieste · `kassel` Kassel · `wurzburg` Würzburg · `grenoble` Grenoble · `biarritz` Biarritz · `szczecin` Szczecin · `kaunas` Kaunas · `lublin` Lublin · `plzen` Plzeň · `bruges` Bruges · `magdeburg` Magdeburg · `erfurt` Erfurt · `chemnitz` Chemnitz · `regensburg` Regensburg · `gottingen` Göttingen · `rostock` Rostock
- **Asia** (44): `tokyo` Tokyo · `bangkok` Bangkok · `singapore` Singapore · `hongkong` Hong Kong · `kualalumpur` Kuala Lumpur · `seoul` Seoul · `osaka` Osaka · `phuket` Phuket · `maldives` Maldives · `beijing` Beijing · `shanghai` Shanghai · `guangzhou` Guangzhou · `chengdu` Chengdu · `kyoto` Kyoto · `macau` Macau · `delhi` Delhi · `shenzhen` Shenzhen · `pattaya` Pattaya · `taipei` Taipei · `hochiminhcity` Ho Chi Minh City · `jaipur` Jaipur · `halong` Ha Long · `hanoi` Hanoi · `jakarta` Jakarta · `penang` Penang (George Town) · `zhuhai` Zhuhai · `chiangmai` Chiang Mai · `cebu` Cebu & Mactan · `guilin` Guilin · `danang` Da Nang · `fukuoka` Fukuoka · `jeju` Jeju · `bangalore` Bengaluru · `yerevan` Yerevan · `tashkent` Tashkent · `tbilisi` Tbilisi · `baku` Baku · `almaty` Almaty · `batumi` Batumi · `nhatrang` Nha Trang · `samarkand` Samarkand · `kunming` Kunming · `luangprabang` Luang Prabang · `vientiane` Vientiane
- **Middle East** (13): `dubai` Dubai · `abudhabi` Abu Dhabi · `doha` Doha · `petra` Petra (Wadi Musa) · `telaviv` Tel Aviv · `riyadh` Riyadh · `jerusalem` Jerusalem · `dammam` Dammam & Al Khobar · `amman` Amman · `jeddah` Jeddah · `kuwaitcity` Kuwait City · `manama` Bahrain (Manama) · `muscat` Muscat
- **Americas** (41): `newyork` New York · `losangeles` Los Angeles · `lasvegas` Las Vegas · `miami` Miami · `toronto` Toronto · `vancouver` Vancouver · `orlando` Orlando · `honolulu` Honolulu · `cancun` Cancún · `loscabos` Los Cabos · `mexicocity` Mexico City · `nassau` Nassau · `puntacana` Punta Cana · `chicago` Chicago · `dallas` Dallas · `atlanta` Atlanta · `washington` Washington, D.C. · `houston` Houston · `denver` Denver · `cusco` Cusco · `manaus` Manaus · `ushuaia` Ushuaia · `elcalafate` El Calafate · `galapagos` Galápagos (Puerto Ayora) · `boston` Boston · `sanfrancisco` San Francisco · `montreal` Montréal · `buenosaires` Buenos Aires · `lima` Lima · `riodejaneiro` Rio de Janeiro · `philadelphia` Philadelphia · `baltimore` Baltimore · `providence` Providence · `laredo` Laredo · `detroit` Detroit · `niagarafalls` Niagara Falls · `savannah` Savannah · `seattle` Seattle · `portland` Portland, Oregon · `sanantonio` San Antonio · `richmond` Richmond, Virginia
- **Africa** (14): `marrakech` Marrakech · `cairo` Cairo · `hurghada` Hurghada · `agadir` Agadir · `sharmelsheikh` Sharm El Sheikh · `sousse` Sousse · `sal` Sal (Santa Maria) · `casablanca` Casablanca · `tangier` Tangier · `rabat` Rabat · `tunis` Tunis · `fes` Fes · `luxor` Luxor · `zanzibar` Zanzibar (Stone Town)
- **Oceania** (7): `sydney` Sydney · `melbourne` Melbourne · `goldcoast` Gold Coast · `brisbane` Brisbane · `auckland` Auckland · `queenstown` Queenstown · `cairns` Cairns
<!-- live-cities:end -->

`GET /intents` always lists what's live right now.

**Beaches.** When `near` names one of a city's beaches (`query.anchor.kind` is `"beach"`), distance to that beach's **sand** counts too, not just distance to its pin, and every hotel carries `sandKm`. When nothing comparable sits right on the sand in our data, `beachNote` says so plainly with the real distance.

**Modes**

- `iconic`: the top-rated luxury, meaning the city's top-tier luxury hotels (its highest price band), ranked by guest rating and location. It stays **close to the action**: when that hotel is beyond walking distance of the city's finest area, the best upscale hotel within walking distance takes its place if it's rated 8.5+ and the far one isn't a 5★ rated half a point higher (resort islands, peninsulas and moved finest districts excepted). (The storied heritage legends are the separate, curated `world.pick` list.) It's **steady**: the same hotel whatever the dates (the one that's the finest on most of them), only its price changes. When it has no rate for the chosen dates, `pick.noRate` is `true`, its price is its usual one, and the alternatives are what's bookable for those dates.
- `afflux`: affordable luxury, a genuinely upscale 5-star at a fair price (the band just below iconic).
- `value`: great, and fairly priced for what you get.
- `budget`: cheap, clean, and genuinely central.
- `family`: a room that actually sleeps 2 adults + 2 children in one room (hard-filtered on live availability).

**Dates.** Every price is for a specific 2-night stay, and `query.stay` names it. The stays roll forward on their own: each season is priced for its next representative week, at least 7 days ahead, and a stay that gets closer than that is withdrawn. A season that has just ended is absent until next year's week is within our pricing window. You never get a date that has passed. When no dated stay applies, `query.stay` is `null` and the price is a typical nightly rate. For the traveller's own dates, check them with `/booking/quote`.

**Response** (abridged)

```json
{
  "ok": true,
  "intent": "hotel.pick",
  "query": { "city": "Zurich", "mode": "iconic", "computedFor": { "adults": 2 }, "stay": { "checkin": "YYYY-MM-DD", "checkout": "YYYY-MM-DD", "nights": 2 } },
  "pick": {
    "name": "Baur au Lac",
    "rating": 9.1, "ratingBasis": "531 reviews (adjusted)",
    "pricePerNight": { "amount": 1189, "currency": "CHF", "basis": "indicative, confirmed at the booking site" },
    "distanceToAnchorKm": 0.18,
    "book": "https://pickthehotel.com/go/lp...?mode=iconic",
    "bookOn": { "booking": "https://pickthehotel.com/go/lp...?provider=booking", "hotels": "https://pickthehotel.com/go/lp...?provider=hotels" },
    "why": { "prestige": 1.0, "quality": 0.94, "central": 0.98 },
    "disclosure": "affiliate: pickthehotel earns a commission if you book through this link; it never changes your price or the ranking"
  },
  "alternatives": [ "... up to 3 more ..." ]
}
```

Send the traveller to `pick.book` (defaults to Booking.com), or `pick.bookOn.booking` / `pick.bookOn.hotels` to choose the OTA. It records the match and redirects to the booking site, where the live price is confirmed. Prices here are **per night** and **indicative** (averaged over a sample 2-night stay): the traveller pays the OTA's live price, so we make no discount claim. The `why` object is an **open map** of the factors that drove the pick (things like reviews, location and value): read it as a guide, not a fixed schema.

## Live availability + price: `POST /booking/quote`

`hotel.pick` ranks from sampled prices, so a pick can be sold out or repriced for *specific* dates (worst for tonight / near-term). **Before recommending a hotel for real dates, verify it's actually bookable**: `/booking/quote` returns only hotels genuinely available for those exact dates, with the **live refundable price** and a direct-book link.

```
POST /booking/quote
Content-Type: application/json

{ "city": "london", "mode": "budget", "checkin": "YYYY-MM-DD", "checkout": "YYYY-MM-DD", "adults": 2, "limit": 3 }
```

Give a `city` (+ optional `mode`) to rank the top hotels and get back those available for the dates, **or** a `hotelId` to check just one, **or** `hotelIds: [...]` to price a specific set (each is returned with its own `available` flag, even the sold-out ones). `checkin` + `checkout` (`YYYY-MM-DD`) are required.

**Response** (abridged)

```json
{
  "ok": true,
  "query": { "city": "London", "mode": "budget", "checkin": "YYYY-MM-DD", "checkout": "YYYY-MM-DD", "party": { "adults": 2 } },
  "count": 3, "checkedCandidates": 6,
  "results": [
    {
      "hotelId": "lp…", "name": "…", "available": true,
      "total": { "amount": 282, "currency": "GBP", "nights": 2, "basis": "live refundable total for the whole stay, confirmed at booking" },
      "pricePerNight": { "amount": 141, "currency": "GBP" },
      "refundable": true, "room": "Double Room", "checkin": "YYYY-MM-DD", "checkout": "YYYY-MM-DD",
      "bookDirect": "https://pickthehotel.com/book?hotel=lp…&checkin=YYYY-MM-DD&checkout=YYYY-MM-DD&…",
      "bookVia": "nuitee-direct",
      "bookAffiliate": "https://pickthehotel.com/go/lp…?provider=booking&checkin=YYYY-MM-DD&checkout=YYYY-MM-DD"
    }
  ]
}
```

Every result is **confirmed available** at a live, refundable price, so nothing sold out slips through. Hand the traveller `bookDirect` to book on pickthehotel (Nuitée is the merchant of record; support and cancellation are Nuitée's, never pickthehotel's), or `bookAffiliate` for the OTA. If `count` is `0`, none of the top picks are bookable for those dates: suggest other dates. The same capability is exposed to MCP clients as the `check_availability` tool.

## Intent: `deal.pick`

"Find me a super-lux hotel for a bargain date." Returns genuine 5-star hotels at the season they are cheapest in our data, with the exact dates and the discount versus the hotel's own usual price, answered instantly from precomputed seasonal prices, no live search.

```
POST /intents
Content-Type: application/json

{ "intent": "deal.pick", "city": "bangkok", "limit": 5 }
```

**Response** (abridged)

```json
{
  "ok": true,
  "intent": "deal.pick",
  "query": { "city": "Bangkok", "currency": "THB" },
  "title": "Super-lux for less in Bangkok",
  "pick": {
    "name": "Anantara Siam Bangkok Hotel",
    "stars": 5, "rating": 9.4,
    "bargain": {
      "season": "summer", "day": "weekday", "stayKey": "summer-weekday",
      "checkin": "YYYY-MM-DD", "checkout": "YYYY-MM-DD",
      "pricePerNight": { "amount": 7308, "currency": "THB" },
      "vsUsual": { "amount": 10028, "currency": "THB" },
      "percentOff": 27,
      "basis": "indicative; the cheapest of our sampled seasonal dates against the hotel's usual price, confirmed at booking"
    },
    "book": "https://pickthehotel.com/go/lp...?mode=deal&checkin=YYYY-MM-DD&checkout=YYYY-MM-DD"
  },
  "alternatives": [ "... more bargains, ranked ..." ]
}
```

`percentOff` is versus the hotel's own usual price (`vsUsual`: the middle of its sampled dates, so one event-driven or not-yet-priced date can't inflate it), and a deal never claims more than 50%; prices are indicative and confirmed at the booking site. A room offered only as accessible never sets the price. Send the traveller to `pick.book`, which opens the OTA on the bargain dates.

## Intent: `world.pick`

Ask for the world's most iconic hotels, not tied to any one covered city. This is a **curated, editorial** roster (one legendary hotel per country, long-established) and is deliberately separate from `hotel.pick`, which ranks live bookable inventory in a city by algorithm.

**HTTP**

```
POST https://pickthehotel.com/intents
Content-Type: application/json

{ "intent": "world.pick", "limit": 10 }
```

**Parameters**

| field | values | notes |
|---|---|---|
| `intent` | `world.pick` | selects this list. An agent may also send `{ "city": "world" }`. |
| `limit` | integer (default: all) | how many of the ranked legends to return (`pick` + `alternatives`). |

**Response** (abridged)

```json
{
  "ok": true,
  "intent": "world.pick",
  "query": { "scope": "world", "limit": 10 },
  "title": "The world's most iconic hotels",
  "count": 10,
  "pick": {
    "rank": 1, "name": "Ritz Paris", "country": "FR", "countryName": "France", "city": "Paris",
    "founded": 1898, "rating": 9.0, "ratingBasis": "guest reviews",
    "pricePerNight": { "amount": 1300, "currency": "EUR", "basis": "indicative, confirmed at booking" },
    "book": "https://pickthehotel.com/go/world-ritz-paris",
    "bookVia": "official-site",
    "official": "https://www.ritzparis.com/en",
    "why": "Place Vendôme's eternal legend: Hemingway, Chanel, Proust."
  },
  "alternatives": [ "... the rest, ranked ..." ]
}
```

Send the traveller to `book` (it records the click and 302s on). **`bookVia` matters:** most of these houses are not sold on the OTAs, so `book` resolves to the hotel's own site (`bookVia: "official-site"`, mirrored in `official`); where a hotel books cleanly on an OTA, `bookVia` is `"booking-affiliate"`. Prices are **indicative**, in each hotel's **local currency**. The list is hand-curated, not algorithmic; `pick` is the pinned flagship, `alternatives` follow by guest rating.

## How the pick is decided

Every hotel in the city is judged the same way, automatically, and without a cent of paid placement, so the answer is defensible and scales:

- **Value**: hotels that punch above their price rank above ones that merely cost a lot.
- **Location**: closeness to what you actually came for (the centre, the old town, the water), not just proximity to a station.
- **Reviews**: weighed by confidence, not merely averaged, so a handful of glowing scores can't outrank a proven track record.
- **Family fit**: a hard filter so one room genuinely sleeps the whole party.

The exact weighting is ours; the result is one hotel, chosen the same rigorous way every time. No hand-curation, no ads.

## Settlement & disclosure

Booking links are affiliate: pickthehotel earns a commission if you book. This is disclosed on every pick.
Settlement is reported back into the [intent graph](https://github.com/Goldcote/intent-graph) via `/postback`
(network-postback tier). Agents may record their own edges at `POST /edges`:

```
POST /edges   { "type": "fetch|shortlist|click", "offer": "<hotel id or offer key>", "intent": "hotel.pick", "meta": { ... } }
```

`type` and `offer` are required (`settle`/`refund`/`dispute` are rejected here, those come only from `/postback`).

See also: [/llms.txt](/llms.txt) · [/agents.md](/agents.md)
