Skip to main content
Glama

We're LetsFG — a community of travelers.

Finding a flight or hotel shouldn't mean checking 47 tabs. Or 3 hours of searching. Or having that feeling you could've got a better deal if you'd just waited a little longer.

So we built something about it. One total price. No tracking. No price that goes up because you looked twice.

     

     

Join the community. Help others find cheaper flights. Spread the word.⭐ Star the repo. Share with a friend ✈️

         


Flights and hotels. Both live.

Every airline in the world. Real prices. One function call.

LetsFG gives your AI agent flight and hotel search and booking superpowers. Our server-side engine scans the entire world for the cheapest price. Search is free. Booking is real: the fare is held on your card, a LetsFG booking agent buys the ticket, and you get the airline's PNR.

The same flight costs $20–$50 less because you skip OTA inflation, cookie tracking, and surge pricing.

Agents: add https://letsfg.co/developers/api/mcp as an MCP server. Approving the connection opens letsfg.co/connect: one tap, no card. The card is asked for at your first booking, in a 0.00 Revolut setup (nothing is charged, no Revolut account needed). That token searches for free and books. Scripts: send the same token as Authorization: Bearer to the PFS endpoints. Developer API: a separate paid product for high-volume commercial use; most agents do not need it. → Get started

GitHub stars PyPI npm Connector Health MCPVault: claimed MIT License

Supporters


Real prices: LetsFG vs Google Flights

We searched 5 routes on Google Flights and LetsFG on the same day (2026-08-05), for flights departing 2026-09-16. Same airline, same number of stops — LetsFG was cheaper every time:

Route

Airline

Google Flights

LetsFG

You Save

LAX → Paris (CDG)

JetBlue, 1 stop

$363

$334

$29

SFO → London (LHR)

JetBlue, 1 stop

$349

$333

$16

LA → New York (JFK)

JetBlue, nonstop

$174

$157

$17

London → Singapore (SIN)

Shenzhen Airlines, 1 stop

$395

$380

$15

Chicago → Dubai (DXB)

Air Canada + Emirates, 2 stops

$517

$461

$56

$133 cheaper across 5 routes in a verified comparison (2026-08-05). LetsFG returns the same prices however often you run the search, because it doesn't track you.

Why the difference? LetsFG compares the same flight across many sellers — Skyscanner, Kayak, Momondo, plus airline websites (Ryanair, United, Southwest, EasyJet, Spirit, Norwegian, AirAsia, and more) — and shows the cheapest. No cookie tracking: the same search returns the same prices however often you run it.


Related MCP server: Flights MCP

Real hotel prices: LetsFG vs Booking.com

Same hotel, same room type, same 2-night stay, same free-cancellation policy — checked on the same day (2026-08-05) for a 2026-09-16 check-in:

Hotel

Booking.com

LetsFG

You Save

Hotel Boss, Warsaw

$206

$169

$37

ibis Styles Paris Gare de l'Est

$663

$525

$138

Copthorne Tara Hotel, London Kensington

$379

$347

$32

$207 cheaper across 3 hotels in a verified comparison (2026-08-05), matching each property's own free-cancellation rate against Booking.com's free-cancellation rate for the identical dates and room type. Prices quoted in PLN at booking, converted to USD at that day's rate.

Why the difference? LetsFG's price does not rise with demand or with who is searching, and there is no loyalty-program cross-subsidy. At booking the price is held on your card, not taken, and it is charged only once the hotel confirms. The comparison above uses free-cancellation rates on both sides; LetsFG also sells non-refundable rates, and every offer says which it is.


Try it right now — no install needed

Human users: Use letsfg.co and search flights instantly in your browser:

🌐 Search on letsfg.co

Search any route, compare live results, and book the flights you want — no installation needed.

Agents / scripts (free server-side): Get a Bearer token by putting a payment method on file (nothing is charged) → use POST /api/search and POST /api/agent-book. This is PFS — Programmatic Flight Search powered by the letsfg.co engine. Search is free; the token is short-lived and refreshes itself. See letsfg.co/for-agents for the full guide.

When you're ready to integrate it into your own agent, keep reading.


Three ways to use LetsFG

Path 1 — MCP / CLI / SDK

Path 2 — PFS (Programmatic Flight Search via letsfg.co)

Path 3 — Developer API

Best for

AI agents (Claude, ChatGPT, Cursor, Windsurf), personal use — easiest way in

Scripts/agents calling the API directly with a Bearer token

High-volume commercial integrations that want prepaid billing. Most agents should not use this

Speed

8–10 s to first results

8–10 s to first results

2–5 s (discover) · 8–10 s to first results (full search)

Search cost

Free (card connected once, nothing charged)

Free (card connected once, nothing charged)

Look-to-book: 200 free after every booking, then $0.01

Booking

book_flight — fare held on your card, agent buys the ticket, real PNR

POST /api/agent-book — same flow

POST /flights/book — same flow, no booking fee

Setup

Add https://letsfg.co/developers/api/mcp as an MCP server and approve (one tap, no card)

Same token, sent as Authorization: Bearer — see below

letsfg.co/developers

Runs where

Our servers (ranking local in the SDK)

Our servers

Our servers

  • MCP / CLI / SDK (Path 1): add https://letsfg.co/developers/api/mcp as an MCP server in Claude, ChatGPT, Cursor or Windsurf and approve the connection. The consent step opens letsfg.co/connect, where the person adds a card (any card, or Revolut Pay / Google Pay) in a 0.00 Revolut setup: nothing is charged, no Revolut account is needed, and card details go to Revolut, never to LetsFG. The token you get back is card-backed: it searches for free and it can book. The Python and JS SDKs read that token from LETSFG_BEARER_TOKEN or ~/.letsfg/config.json and apply the open-source ranking algorithm locally. (letsfg auth runs this same connect flow from the terminal: it registers itself as an OAuth client, opens the card screen in a browser for a person to approve, and stores the token.)

  • PFS — Programmatic Flight Search (Path 2): For scripts and agents that call the API directly. letsfg.co is human-only by default (Cloudflare Turnstile + bot protection), so the card-backed token from the connect flow is the only programmatic way in. Send it on every request:

    1. Search: POST https://letsfg.co/api/search with Authorization: Bearer <token> → { search_id }

    2. Poll: GET https://letsfg.co/api/results/<search_id> (never counts against the rate limit)

    3. Book: POST https://letsfg.co/api/agent-book → { booking_ref } within seconds

    4. Wait: POST https://letsfg.co/api/agent-book/status with { booking_ref } every 20–30 s until completed (PNR), failed (hold released, nothing charged) or needs_attention

    POST /api/agent-access/request still answers 402 with add_card_url and these steps as JSON, so an agent that starts from the endpoint lands in the same place. The MPP lane (a wallet, no card) is unchanged: answer the WWW-Authenticate: Payment challenge ($0.01 once) and verify with Authorization: Payment. The Stripe setup_url / SetupIntent lanes were retired on 2026-09-02 and every token they issued was revoked; reconnect at letsfg.co/connect. Full guide and response schema: letsfg.co/for-agents.

  • Developer API (Path 3): Server-side search and booking at letsfg.co/developers. Look-to-book search (200 free after every booking, then $0.01), real booking through POST /flights/book on a connected Revolut method, full NL query parsing, a /discover endpoint that checks 20 destinations in one call (2–5 s), hotels, and a free sandbox at /sandbox/flights/* that simulates booking end to end (states, questions, failures, real timings). Full docs: letsfg.co/developers/api/docs.

Free server-side search: Use Path 1 or PFS — connect a card once at letsfg.co/connect (nothing charged) and searches run free on our servers. No Playwright, no local install beyond the SDK. Booking from your own product: Use the Developer API (Path 3) — look-to-book search, POST /flights/book, and no booking or transaction fee. Hotels work on both the card-backed token and a Developer API key.


Pricing

How you use it

Search

Flight booking

Hotel booking

Runs where?

MCP Server

✅ Free (card connected once at letsfg.co/connect)

Price held, captured on a real PNR. No separate fee

Price held, captured once the hotel confirms. No reservation fee

Our servers

CLI / Python SDK / npm

✅ Free (same token)

Same

Same

Our servers

PFS (raw API via letsfg.co)

✅ Free (same token, or $0.01 once via MPP)

Same

Price held, captured once the hotel confirms. No reservation fee

Our servers

Developer API

200 free per booking, then $0.01

Price held, captured on a real PNR. No booking fee, no transaction fee

Price held, captured once the hotel confirms. No reservation fee

Our servers

MCP / CLI / SDK / PFS = free search, real booking, no separate fee. Connect a card once (a 0.00 Revolut setup, nothing is charged) and searching is free. No credits, no unlock step. Booking works exactly like the website checkout: book_flight / POST /api/agent-book holds the price shown on your card, a LetsFG booking agent buys the ticket from the seller, and the hold is captured only once a real airline PNR exists. If the booking fails the hold is released and nothing is charged. The price you see is the price you pay; nothing is added at booking.

Hotels = the price on the offer, held then captured, on every path. Booking holds the full price on your card, LetsFG books and pays the hotel, and the hold is captured only once the hotel confirms; a failed booking releases it. There is no reservation fee, no deposit and no pay link. Every rate type is sold, and each offer says whether it is refundable and until when. See Hotels below.

Developer API = business use, look-to-book. letsfg.co/developers books flights itself — POST /flights/book holds the fare on your connected Revolut method and a LetsFG booking agent buys the ticket, exactly like the other paths. Search is not priced per call: you get 200 free searches after every booking you make, and booking resets the counter. Past that, blocks of 500 for $5.00 ($0.01 each). No booking fee, no transaction fee on top: the amount the search returned is the amount charged. Minimum top-up: $5.

The monthly per-search tiers ($0.50 / $0.20 / $0.10) were retired on 2026-09-08 along with Stripe. Payments are Revolut: POST /agents/connect-payment returns a one-time link that saves a card, and nothing is charged to connect.

💡 Know someone who travels? The more people discover LetsFG, the more airlines we cover — and the better it gets for everyone. ⭐ Star · Share with a friend


Why developers star this repo

Google Flights / Expedia

LetsFG

Price

Varies by site and search

Stable across repeat searches. $133 cheaper across 5 routes, verified 2026-08-05.

Coverage

One site's sources

Every airline in the world — OTAs, budget carriers, full-service

Speed

30 s+ (page loads, ads, redirects)

CLI/PFS: 8–10 s to first results · API discover: 2–5 s

Repeat search raises price?

Yes

Never

Works in AI agents?

No API

MCP · CLI · PFS (card connected once, free) · Developer API (prepaid)

Booking

Redirects to OTA checkout

Real airline PNR, e-ticket to inbox

Cabin class filter

No

Economy, premium, business, first

Cost to you

Varies by site

CLI/PFS: free search; no booking fee and no transaction fee. Developer API: 200 free searches after every booking, then $0.01/search; no booking fee, no transaction fee.


Get started

Everything runs on our servers. One card connection covers the MCP, the SDKs and the raw API.

🔌 MCP — connect once, search and book

Add the remote server and approve the connection. The consent step opens letsfg.co/connect: one tap, no card, and you are in. The card is asked for at the first booking (0.00 setup, nothing charged).

# Claude Code
claude mcp add --transport http letsfg https://letsfg.co/developers/api/mcp
// Cursor (~/.cursor/mcp.json) — Windsurf uses "serverUrl" instead of "url"
{ "mcpServers": { "letsfg": { "url": "https://letsfg.co/developers/api/mcp" } } }

claude.ai and ChatGPT: add a custom connector with the same URL.

Then, in the chat: "find me the cheapest flight from London to Barcelona on June 15 and book it". search_flights returns offers; book_flight holds the fare on your card and starts a LetsFG booking agent; get_flight_booking reports the PNR when it lands (4–11 minutes).

🖥️ CLI / SDK — same token, ranking runs locally

pip install letsfg
export LETSFG_BEARER_TOKEN=<token from the connect flow>
letsfg search LHR BCN 2026-06-15
letsfg search LHR JFK 2026-06-15 --cabin C   # cabin class: M economy, W premium, C business, F first

The SDKs read the token from LETSFG_BEARER_TOKEN or ~/.letsfg/config.json. letsfg auth performs that connect flow itself: it registers as an OAuth client, opens https://letsfg.co/connect for a person to approve, and writes the token to ~/.letsfg/config.json. Add --no-browser to print the URL instead of opening one.

🔌 PFS — Programmatic Flight Search (free, server-side)

Run LetsFG's full search on our servers from any script. Access requires a card connected to a LetsFG account: letsfg.co is human-only (Cloudflare Turnstile), so the card-backed token from the connect flow is the only programmatic way in. Nothing is charged to connect it.

# 1. Search with the token
curl -X POST https://letsfg.co/api/search \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"origin":"LHR","destination":"BCN","date_from":"2026-06-15"}'
# → {"search_id":"ws_abc123","status":"searching"}

# 2. Poll until done (keep going while split_ticket_pending is true)
curl https://letsfg.co/api/results/ws_abc123 -H "Authorization: Bearer <token>"

# 3. Book — the fare is HELD on the card, a LetsFG agent buys the ticket
curl -X POST https://letsfg.co/api/agent-book \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"search_id":"ws_abc123","offer_id":"wo_abc123","contact_email":"ada@example.com",
       "passenger":{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f",
                    "nationality":"GB","phone_number":"+447700900000","phone_country":"GB",
                    "address_line1":"1 Analytical Way","address_city":"London","address_postal":"N1 9GU","address_country":"GB"}}'
# → {"booking_ref":"eyJ..."} within seconds

# 4. Wait for the PNR (every 20–30 s; a booking takes 4–11 minutes)
curl -X POST https://letsfg.co/api/agent-book/status \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"booking_ref":"eyJ..."}'
# → {"state":"completed","pnr":"ABC123","charged_amount":93,"currency":"EUR"}

failed means the hold was released and nothing was charged; needs_attention means a human at LetsFG is checking it, do not book again. A missing passenger detail returns missing_fields and charges nothing. Without a card the endpoint answers payment_method_required with add_card_url. Full guide and response schema: letsfg.co/for-agents.

⚡ Developer API — server-side search, booking and hotels

A separate product for high-volume commercial integrations; most agents should not use it. Look-to-book search, results in seconds, and real booking through POST /flights/book — plus /discover (20 destinations in one call), async polling, NL query parsing, hotels, and a free sandbox where booking works end to end (same responses, states, questions and timings as production, no money) - build your booking flow there first.

# Register, then search with your API key
curl -X POST https://letsfg.co/developers/api/v1/agents/register \
  -H "Content-Type: application/json" \
  -d '{"agent_name":"my-agent","email":"you@example.com"}'

curl -X POST https://letsfg.co/developers/api/v1/flights/search \
  -H "X-API-Key: letsfg_..." \
  -H "Content-Type: application/json" \
  -d '{"origin":"LHR","destination":"BCN","date_from":"2026-06-15"}'

Pricing: 200 searches free after every booking, then blocks of 500 for $5.00 ($0.01 each). No booking fee, no transaction fee. Minimum top-up $5. Test for free in the sandbox first, booking included. Full docs: letsfg.co/developers/api/docs.

search_flights  LON → BCN, 2026-04-01, return 2026-04-08
get_flight_results  (while more offers are still landing)
book_flight     search_id + offer_id + one traveller's real details + contact_email
                → booking_ref in seconds; price HELD on the connected card
get_flight_booking  every 20–30 s → completed (PNR, charged_amount) | failed (hold released) | needs_attention

Over raw HTTP the same four steps are POST /api/search, GET /api/results/<id>, POST /api/agent-book, POST /api/agent-book/status.

letsfg unlock was retired on 2026-09-08 and its route answers 410 Gone. There is no unlock step on either lane: booking holds the fare and captures only against a real PNR, which is what unlock existed to protect against. See CLI Commands.

💡 Like what you see? Support us — ⭐ Star · Share with a friend


Works everywhere your agent runs

MCP Server (Claude / ChatGPT / Cursor / Windsurf / OpenClaw)

Use the hosted server. It carries your card-backed token for you, and book_flight books for real.

{
  "mcpServers": {
    "letsfg": {
      "url": "https://letsfg.co/developers/api/mcp"
    }
  }
}

Approve the connection when your client asks; the consent step opens letsfg.co/connect: one tap, no card. The card is asked for at the first booking (0.00 setup, nothing charged). Tools: search_flights, get_flight_results, book_flight, get_flight_booking, plus the hotel tools.

The stdio package (npx -y letsfg-mcp) works too if you give it a token in LETSFG_BEARER_TOKEN; its authenticate tool returns the current connect instructions.

{
  "mcpServers": {
    "letsfg": {
      "command": "npx",
      "args": ["-y", "letsfg-mcp"],
      "env": {
        "LETSFG_API_KEY": "letsfg_your_api_key"
      }
    }
  }
}

This is a separate paid product for high-volume commercial use — most agents should stick with the free connect flow above and skip this. A human setting this up deliberately gets a key at letsfg.co/developers.

5-minute quickstarts: Claude Desktop · Cursor · Windsurf

Python SDK

from letsfg import LetsFG

bt = LetsFG()  # reads LETSFG_API_KEY from env
flights = bt.search("LHR", "JFK", "2026-04-15")
print(f"{flights.total_results} offers, cheapest: {flights.cheapest.summary()}")

JavaScript SDK

import { LetsFG } from 'letsfg';

const bt = new LetsFG({ apiKey: 'letsfg_...' });
const flights = await bt.search('LHR', 'JFK', '2026-04-15');
console.log(`${flights.totalResults} offers`);
from letsfg.local import search_local

# Reads LETSFG_BEARER_TOKEN env var or ~/.letsfg/config.json (the token from the connect flow)
result = await search_local("GDN", "BCN", "2026-06-15")

for offer in result.offers[:5]:
    print(f"{offer.airlines[0]}: {offer.currency} {offer.price}")

⇄ Split tickets — two tickets, one trip

A long-haul searched as one journey comes back as one through-fare, because everyone is reselling the same ticket. Searched as two independent legs through a hub, each leg is booked from whatever is cheapest for that hop — whichever airline, whichever seller. Usually that lands on two different airlines (often a low-cost carrier for the short leg and a separate airline for the long one), but the only rule is "cheapest for each leg". Nobody sells the combination as one ticket, so nobody quotes it — which is exactly why it is cheaper.

LetsFG builds that itinerary for you and returns it alongside the through-fares. Split offers are flagged, never disguised:

Field

Value on a split offer

split_ticket

"true"

combo_type

"virtual_interlining"

self_transfer

"unprotected"

for o in offers:
    if o.get("split_ticket") == "true":
        print(o["price"], "— two separate tickets, self-transfer not protected")

Read self_transfer before you present the price. unprotected means the two tickets are not linked: if the first flight is late and the connection is missed, the second airline owes nothing — no rebooking, no refund, no duty of care. That is the trade you are being offered in exchange for the saving, and it has to reach the traveller. We only build a split when the connection has a real buffer, and we say so on the offer — but an agent that relays the price without the condition is misrepresenting it.

The probe is gated. Two extra connector fan-outs cost real money, so a split is only attempted when the through-fare is expensive enough, the journey long enough, and the market has somewhere to break the trip. Most searches never fire it, and a search that fires it does not always find a saving.

It lands after completed. See Polling: completed is not the end.

Where it runs. letsfg.co, and the agent lane behind it — the CLI, the Python and JS SDKs, and the MCP server. The paid Developer API is served by a different backend; self_transfer is returned there, but split-ticket offers are not part of that contract.

What it actually saves

Route

Cheapest single ticket

Split — two tickets

You save

Connect via

Two airlines

Layover

Stuttgart → Shanghai

$552

$461

$91 (16%)

Budapest

Wizz Air + Qatar Airways

6 h

Katowice → Dubai

$283

$216

$67 (24%)

Istanbul

Wizz Air + Pegasus

15 h

Chongqing → Gdansk

$429

$387

$42 (10%)

Stockholm

China Eastern + Ryanair

10 h

Vilnius → Bangkok

$370

$337

$33 (9%)

Athens

Ryanair + Air Arabia

5 h

Shanghai → Vilnius

$406

$377

$29 (7%)

London

Shenzhen Airlines + Wizz Air

12 h

Measured August 2026 on live searches (one adult, one way, ~2 months out). Each split is two separate tickets (here, two different airlines) with an unprotected self-transfer — if the first flight is late, the second airline owes you nothing. Prices move constantly; these won't reproduce exactly. 5 of 26 long-haul routes searched produced a split cheaper than the best single ticket — the win shows up at secondary cities with no cheap direct long-haul. Regenerate with tools/split-comparison.py.

The numbers above come from real searches and are reproducible: the script runs the same public search anyone can run, compares the cheapest split against the cheapest single ticket in the same result set, and writes the raw offers to split-comparison.json so any row can be checked. Routes that produced no split are reported too rather than dropped quietly — most searches never fire a split probe, and a table that hid that would misrepresent how often this happens.

Offers tell you whether the flight has Starlink, in two honest tiers. A solid verdict (confirmed_all) means the airline has fitted every aircraft of that type. A hedged one (likely_all) means the rollout on that type is real but incomplete — as of August 2026 United was ~29% of its fleet — so it is reported as a signal, never a promise. An absent field means no information, not an absence of Wi-Fi.

for o in offers:
    if o.get("starlink") == "confirmed_all":
        print(f"{o['owner_airline']} {o['price']} — Starlink on every leg")

Full semantics: docs/api-search.md.

🏨 Hotels — new, and live

Your agent can now book hotels, not just flights. Same credential, same card on file.

Update your SDK before booking hotels. Hotel booking needs letsfg 2026.5.101 or later (Python), letsfg 2026.5.74 or later (JavaScript/TypeScript) or letsfg-mcp 2026.5.77 or later. Earlier releases send the reservation-fee fields retired on 2026-09-11 (expected_balance, no expected_cost), and the API refuses every hotel booking they make. Update with pip install -U letsfg, npm install letsfg@latest or npx -y letsfg-mcp@latest. The hosted MCP at https://letsfg.co/developers/api/mcp needs no update.

from letsfg import LetsFG
lfg = LetsFG()  # reads LETSFG_API_KEY — the SDK's hotel methods take a Developer API key

city = lfg.hotel_destinations("Warsaw")[0]
stays = lfg.search_hotels(
    city_id=city["Id"], city_name=city["Name"],
    check_in="2026-11-10", check_out="2026-11-12", adults=2,
)

hotel = stays["hotels"][0]
offer = hotel["offers"][0]
print(hotel["name"], offer["price"], offer["currency"],
      "refundable" if offer["refundable"] else "non-refundable")

booking = lfg.book_hotel_and_wait(
    session_id=offer["session_id"],
    hotel_code=hotel["hotel_code"],
    combination_id_v2=offer["combination_id_v2"],
    expected_price=offer["price"],
    expected_cost=offer["expected_cost"],
    currency=offer["currency"],
    fx_rate=offer["fx_rate"],
    city_id=city["Id"], city_name=city["Name"],
    check_in="2026-11-10", check_out="2026-11-12",
    guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"},     # one entry per guest:
            {"title": "Ms", "first_name": "Anna", "last_name": "Kowalska"}],   # adults=2 -> two names
    email="guest@example.com", phone="512345678",
)
print(booking["status"], booking.get("confirmation"), booking.get("total_price"), booking.get("currency"))

How you pay

Held at booking, charged when the hotel confirms. Booking holds the full price on your card — it is not taken. LetsFG books the room and pays the supplier itself, and the hold is captured only once the hotel has confirmed. If the booking fails for any reason (the rate is gone, the price moved, the supplier declined) the hold is released and nothing is charged. There is no reservation fee, no deposit and no pay link.

price is the all-in total, in the currency you searched in (USD by default). A refundable booking cancelled before its free_cancellation_until is refunded in full. The endpoint refuses a cancellation that would cost money; the hotel's own ladder ships in the booking's terms, so you can always see the cost first.

Things worth knowing before you build

  • A card on file is required for every hotel call, including search. That is unusual and it is deliberate: a hotel search opens a real session at the supplier, and booking blocks a real rate. We would rather refuse up front than let you reach the point of commitment and discover you cannot pay. The same card that authorises flight booking authorises hotels — there is no separate hotel signup.

  • Every rate type is sold, refundable and non-refundable. Each offer carries refundable and free_cancellation_until; show them to the guest before booking a non-refundable rate.

  • Booking is asynchronous. book_hotel returns a booking_job_id, not a booking — the real thing takes minutes. Poll hotel_booking(job_id) until status is succeeded, failed or attention, or call book_hotel_and_wait. attention means a person at LetsFG is confirming the outcome with the supplier: the hold is kept, nothing is charged, and you must not book again.

  • Copy the offer back verbatim. Send expected_price, expected_cost, currency and fx_rate exactly as the offer returned them; anything else is refused as price_mismatch before anything is held.

  • Name every guest. guests needs one entry per person in the room, children included: adults first, then children in child_ages order. Fewer names than the searched party fails the booking before anything reaches the hotel, and the hold is released.

  • Do not re-book while a job is running. Poll it. A retry with the same idempotency_key returns the existing job instead of booking twice.

  • The guest hears from us either way. The guest's e-mail gets the confirmation, or a message if the booking fails or needs checking.

  • price is what the guest pays. expected_cost is the supplier's own figure, there only to be sent back — never quote it.

JavaScript

import { LetsFG } from 'letsfg';
const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });

const [city] = await lfg.hotelDestinations('Warsaw');
const stays = await lfg.searchHotels({
  cityId: city.Id, cityName: city.Name,
  checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
});

const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
console.log(booking.status, booking.confirmation, booking.total_price, booking.currency);

MCP

Five new tools, in the order you call them: resolve_hotel_city → search_hotels → book_hotel → get_hotel_booking → cancel_hotel_booking.


🖥️ Omarchy desktop plugin

Search every airline in the world from the Omarchy bar. Type two airport codes and a date, press Search, click an offer to open it. The panel lives in this repo — manifest.json, BarWidget.qml, Panel.qml and Model.js at the root — and runs on the same engine as the CLI and the MCP server, ordered by the same open-source ranking algorithm in sdk/js/src/ranking.ts.

Install

omarchy plugin add https://github.com/LetsFG/LetsFG.git --enable

Then add LetsFG Flights to a bar section in the Omarchy bar settings. The panel reads your LetsFG token from ~/.letsfg/config.json (what letsfg auth writes) and renews it itself, or connects a card from the panel via letsfg.co/connect — nothing is charged. See OMARCHY-PLUGIN.md.

Remove

omarchy plugin remove io.github.letsfg.flights

That removes the plugin only. Your token is yours — delete ~/.letsfg/config.json yourself if you want it gone.

The plugin bundles no API keys. It authenticates with a token you create and can revoke, it never asks for card details, and it never starts a search on its own — no background poll, no price watch, no refresh timer. Every host it can contact, every file it reads or writes, and the anonymous installation id it sends so we can tell whether anyone is using it, are documented in full in OMARCHY-PLUGIN.md.

Requires the Omarchy Quattro shell. MIT, like the rest of this repo. Not affiliated with, sponsored by, or endorsed by Omarchy or 37signals.


Install

Package

Command

What you get

Remote MCP

https://letsfg.co/developers/api/mcp

No install. Approve the connection at letsfg.co/connect (one tap, no card), search and book

Python SDK + CLI

pip install letsfg

SDK + CLI (token from the connect flow in LETSFG_BEARER_TOKEN)

MCP Server (stdio)

npx letsfg-mcp

Local server for clients without remote MCP support; needs LETSFG_BEARER_TOKEN

JS/TS SDK

npm install -g letsfg

SDK + CLI + open-source ranking engine

Agent Skill

npx skills add LetsFG/LetsFG

Install flight search skill for any AI agent (skills.sh)

Smithery

smithery.ai/servers/letsfg

One-click MCP install

Omarchy plugin

omarchy plugin add https://github.com/LetsFG/LetsFG.git --enable

Flight search in the Omarchy bar (details)


CLI Commands

Command

Description

letsfg auth

Connect a card at letsfg.co/connect and store the token (self-registers, PKCE + loopback redirect, opens a browser). --no-browser prints the URL

letsfg search <origin> <dest> <date>

Search flights (free with a card-backed token)

letsfg register

[Developer API only] Register an account for the paid, prepaid-credit product — not part of the agent flow

letsfg connect-payment

[Developer API only] Print a one-time link to connect a card to the paid account; nothing is charged. letsfg setup-payment is kept as an alias — the Stripe route it once called was retired on 2026-09-08 and answers 410 Gone

letsfg recover --email <email>

Recover lost API key via email

letsfg locations <query>

Resolve city/airport to IATA codes

letsfg unlock <offer_id>

RETIRED 2026-09-08 — the route answers 410 Gone. There is no unlock step on either lane; use letsfg book

letsfg book <offer_id>

Book the flight: holds the fare on the connected card, a LetsFG agent buys the ticket, returns a booking_ref to poll

letsfg me

View profile & usage stats

All commands accept --json for structured output and --api-key to override the env variable.


How it works

CLI / SDK / MCP (free, cloud-backed)

Connect the MCP (once, card added at letsfg.co/connect) → card-backed token → Search (free) → Book (hold → agent → PNR)
  1. Auth — add https://letsfg.co/developers/api/mcp as an MCP server and approve it. The OAuth consent step opens letsfg.co/connect: one tap, no card. The card is asked for at the first booking, in a 0.00 Revolut setup. Nothing is charged to connect. The SDKs read that token from LETSFG_BEARER_TOKEN or ~/.letsfg/config.json.

  2. Search — letsfg search LHR BCN 2026-06-15 calls POST https://letsfg.co/api/search, polls until done (8–10 s to first results), and applies the open-source ranking algorithm locally.

  3. Book — POST /api/agent-book holds the price shown on the card and starts a LetsFG booking agent; POST /api/agent-book/status reports completed with the PNR (4–11 minutes), or failed with the hold released. Nothing extra is added at booking.

Polling: completed is not the end

A search returns in 8–10 s to first results. Poll GET /api/results/<search_id> immediately and then every 2 s — a loop that sleeps first puts a floor under a search that is already faster than the sleep.

When status leaves searching, the connector fan-out is done — but the offer set may still be growing. The split-ticket probe is dispatched after the fan-out and merges its result in late, so the cheapest itinerary on the search is routinely one that does not exist yet at the moment the status turns terminal. The response says so:

Flag

Meaning while true

split_ticket_pending

a split-ticket probe is still running

gf_enrich_pending

the Google Flights enrich has not merged yet

Keep polling while either is true, and bound the wait — a flag that never clears must not hang your agent. Take whatever has landed when the bound expires.

The Python and JS SDKs and the MCP server already do this, with a 90 s ceiling — the same window the server uses to decide a result has settled. So a search that fires a split probe can take meaningfully longer than the 8–10 s to first results fast path, and it is the split offer you are waiting for. Set LETSFG_WAIT_FOR_SPLIT=0 if you would rather have the fast answer.

Most searches never fire the probe, so both flags are usually already false on the first poll and this costs nothing.

PFS — raw API (same as CLI, without the wrapper)

Card connected at letsfg.co/connect -> Bearer token -> POST /api/search -> poll GET /api/results/<id> -> POST /api/agent-book -> poll POST /api/agent-book/status
  1. Get a Bearer token — connect through the MCP OAuth flow; the consent step is letsfg.co/connect (card or Revolut Pay, 0.00, nothing charged). POST /api/agent-access/request answers 402 with add_card_url and the steps. The MPP wallet lane ($0.01 once) verifies at POST /api/agent-access/verify with Authorization: Payment.

  2. Search — POST https://letsfg.co/api/search with Authorization: Bearer <token>. Returns { search_id }. Poll GET /api/results/<search_id> immediately, then every 2 s, until status leaves searching. Then keep polling while split_ticket_pending or gf_enrich_pending is true — the split-ticket offer merges in after the status turns terminal.

  3. Book — POST /api/agent-book → booking_ref; poll POST /api/agent-book/status until completed (PNR) or failed (hold released, nothing charged).

Developer API — server-side search and booking

Register → Connect a Revolut method → Search (200 free per booking) → POST /flights/book → poll to a PNR
  1. Discover — POST /flights/discover with up to 20 destinations, get indicative prices sorted cheapest-first. 1 credit, 2–5 s. Use to rank options before committing to a full search.

  2. Full search — POST /flights/search (blocking) or /flights/search/async (non-blocking + poll). 1 credit, 8–10 s to first results.

  3. Book — each offer includes a direct airline booking_url. No LetsFG fee, no checkout step.

The server-side engine builds cross-airline round-trips by combining one-way fares from different carriers. A Ryanair outbound + Wizz Air return can save 30-50% vs booking a round-trip on either airline alone.

Search a city code and LetsFG automatically searches all airports in that city. LON expands to LHR, LGW, STN, LTN, SEN, LCY. NYC expands to JFK, EWR, LGA. Works for 25+ major cities worldwide.


Architecture

CLI / SDK / MCP / PFS

CLI / SDK / MCP / AI Agent
        │  Card connected at letsfg.co/connect -> card-backed Bearer token
        ▼
POST letsfg.co/api/search  (bot-protected, token required)
        │
        ▼
letsfg.co server-side search engine
        │
        ▼
GET /api/results/<search_id>  (poll every 2 s; keep going while split_ticket_pending)
        │
        ▼
Ranking applied locally (sdk/js/src/ranking.ts, open-source)
        │
        ▼
Results + booking via POST /api/agent-book (hold on card -> LetsFG agent -> PNR)

Developer API

Product / Team / Agent
        │  API key + a connected Revolut method
        ▼
letsfg.co/developers/api/v1
  ├─ /flights/discover      (indicative prices, 20 dest, 2–5 s)
  ├─ /flights/search        (full search, 8–10 s to first results)
  ├─ /flights/search/async  (non-blocking + poll)
  ├─ /flights/parse-query   (Gemini NL parsing, free)
  ├─ /flights/book          (holds the fare on the connected method)
  ├─ /flights/bookings/{id} (poll to a real PNR, 4–11 min)
  └─ /sandbox/flights/*     (free: search + simulated booking, same schema)
        │
        ▼
Real airline PNR - the hold is captured only once it exists

Region

Airlines

Europe

Ryanair, Wizz Air, EasyJet, Norwegian, Vueling, Eurowings, Transavia, Pegasus, Turkish Airlines, Condor, SunExpress, Volotea, Smartwings, Jet2, LOT Polish Airlines, Finnair, SAS, Aegean, Aer Lingus, ITA Airways, TAP Portugal, Icelandair, PLAY

Middle East & Africa

Emirates, Etihad, Qatar Airways, flydubai, Air Arabia, flynas, Salam Air, Air Peace, FlySafair, EgyptAir, Ethiopian Airlines, Kenya Airways, Royal Air Maroc, South African Airways

Asia-Pacific

AirAsia, AirAsia X, IndiGo, SpiceJet, Akasa Air, Air India, Air India Express, Alliance Air, Star Air, EaseMyTrip OTA, VietJet, Cebu Pacific, Scoot, Jetstar, Peach, Spring Airlines, Lucky Air, 9 Air, Nok Air, Batik Air, Jeju Air, T'way Air, ZIPAIR, Skymark, H.I.S. Travel OTA, Singapore Airlines, Cathay Pacific, Malaysian Airlines, Thai Airways, Korean Air, ANA, JAL, Qantas, Virgin Australia, Bangkok Airways, Air New Zealand, Garuda Indonesia, Philippine Airlines, US-Bangla, Biman Bangladesh

Americas

Southwest, JetBlue, Frontier, Spirit, Allegiant, Avelo, Breeze, Sun Country, Flair, Porter, WestJet, Volaris, VivaAerobus, GOL, Azul, LATAM, JetSmart, Flybondi, Arajet, Wingo, Sky Airline, Copa, Avianca

Oceania

Rex, Bonza, Link Airways, Air Vanuatu, Fiji Airways


Star History


letsfg.co · API Docs · Connector Health · PyPI · npm · Smithery · Instagram · TikTok · X

Open source · MIT License · Made with ❤️ by travelers, for travelers

Want updates? Click Watch above, or follow LetsFG on Instagram, @letsfg_ on TikTok, or @LetsFG_ on X.

Available Tools

14 tools
answer_booking_questionAInspect

Answer the question a paused booking is waiting on. get_flight_booking returns awaiting_choice when the booking agent has stopped mid-checkout holding a cart: at the airline's own seat map, at a paid extra it has just priced, or at a fare that moved.

NOTHING PROGRESSES UNTIL YOU ANSWER, and the cart expires. Put the question to the traveller with the options and the time left, then send their answer here.

kind "seat" -> seats: [{ d: "12A" }, ...] from the map you were shown, or skip: true kind "extra" -> confirm: true to take it (a bag, or a moved fare), or confirm: false / skip: true to decline

Always pass the round from awaiting_choice - it says WHICH question you are answering. A return trip pauses twice, and an answer without it could seat the wrong leg.

ParametersJSON Schema
NameRequiredDescriptionDefault
kindYesFrom awaiting_choice.kind
skipNoDecline outright — no seat, no extra.
roundYesFrom awaiting_choice.round. Required.
seatsNokind "seat" only: the chosen seats, e.g. [{ "d": "12A" }].
confirmNokind "extra" only: take it (true) or decline (false).
booking_refYesThe booking_ref book_flight returned

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It discloses that nothing progresses until answered, the cart expires, and passing the wrong round could seat the wrong leg on a return trip. This is strong context, though it does not mention authentication or explicit irreversibility of the action.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average but every section earns its place: purpose, urgency, kind-specific parameter rules, and the round warning. The structure is front-loaded and the all-caps urgency line effectively highlights the critical consequence without being vague.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 6-parameter, no-output-schema action tool, the description covers the full calling context: when to call, what values to source, which parameters apply per kind, and what could go wrong. The agent has everything needed to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds meaning well beyond it: kind-specific rules for seat vs extra, skip/confirm combinations, the example seat shape {'d':'12A'}, and the critical requirement to pass round from awaiting_choice to avoid wrong-leg answers. These are exactly the semantics an agent needs beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Answer the question a paused booking is waiting on.' It ties directly to get_flight_booking's awaiting_choice and makes clear this is the action tool that resolves a paused checkout, distinguishing it from the surrounding flight/hotel search and booking siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly states when to use the tool: when get_flight_booking returns awaiting_choice and after the traveller has been asked. It even instructs the agent to present options and time left before calling. It does not name alternatives or exclusions, but no sibling tool serves this purpose, so the context is sufficient.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

authenticateAInspect

Explain how to connect a card so this server can search and book. Nothing is charged to connect — a 0.00 Revolut setup that saves the card so a booking can be charged later.

Call with no arguments. It returns the current instructions and add_card_url (https://letsfg.co/connect). A PERSON must approve once in a browser — there is no endpoint that mints a token from card details, so do not ask the user for card numbers and do not try to automate this step.

Two ways in: (a) add LetsFG as a connector in an assistant that supports remote MCP servers and approve it, or (b) any OAuth-capable client can register itself — see https://letsfg.co/for-agents, section "Option B". Both land on the same card screen.

RETIRED 2026-09-02: the Stripe lanes (setup_url, setup_session_id, payment_method_id, card_token) and every token they issued. Passing them now fails.

This does NOT create a Developer API billing account. Do not use connect_payment for this.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full behavioral disclosure and does so thoroughly. It explains that nothing is charged, the setup is a 0.00 Revolut flow, a human must approve in-browser, no endpoint mints a token from card details, retired Stripe lanes now fail, and this tool does not create a billing account.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average but every section earns its place: purpose, invocation, human-approval requirement, two connection methods, retired fields, and exclusion from billing. It is well-structured and front-loaded, though slightly verbose in the first sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no annotations and no output schema, the description fully equips an agent to invoke the tool correctly: what it does, what it returns, what the user must do, what not to do, what is retired, and how it relates to connect_payment. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has zero parameters and the description explicitly instructs 'Call with no arguments,' removing any ambiguity. It also describes what the call returns, which is useful given there is no output schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: connect a card so the server can search and book, returning instructions and an add_card_url. It also differentiates from connect_payment explicitly. The opening phrase 'Explain how to connect a card' is slightly indirect about what the tool itself does, but the intent is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: call with no arguments, require a person to approve in a browser, never ask for card numbers, and do not automate the step. It also names connect_payment as a sibling and explicitly warns not to use it for this purpose.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

book_flightAInspect

Book a flight from a search result.

FLOW: authenticate (once) -> search_flights -> book_flight CHARGES: nothing from LetsFG. A completed booking pays the airline prices from airlines and the major booking sites. RESULT: either {"booked": true, "order_id": "..."} or {"booked": false, "booking_url": "..."} — the second means the booking genuinely did not complete and nothing was charged. That is a normal outcome, NOT a transient error: do not retry, give the user the booking_url. SAFETY: use REAL passenger details — names must match passport, email receives the e-ticket.

ParametersJSON Schema
NameRequiredDescriptionDefault
offer_idYesOffer ID from search_flights
search_idYessearch_id from search_flights
passengersYesPassengers with 'id' from search passenger_ids
contact_emailYesBooking contact email
idempotency_keyNoUnique key to prevent double-bookings on retry (e.g., UUID). Strongly recommended.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and excels. It discloses charges ('nothing from LetsFG'), the two result variants (success with order_id, incomplete with booking_url), and warns that incomplete is a normal, non-retryable outcome. It also requires 'REAL passenger details' and explains why (names match passport, email receives e-ticket), providing safety context not inferable from schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact (~130 words) and uses labeled sections (FLOW, CHARGES, RESULT, SAFETY) to front-load critical information. Every sentence earns its place, with no filler or repetition of schema content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

It covers flow, charges, result handling, and safety requirements. The result handling for incomplete bookings is exactly the kind of edge-case detail an agent needs, and the absence of an output schema is compensated by explicit example JSONs. No significant gaps remain for the agent to execute the booking safely and correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds meaningful context: 'passengers with id from search' ties params to search_flights output, and 'names must match passport' clarifies the given_name/family_name fields while 'email receives the e-ticket' explains contact_email/passenger email purpose. This elevates it above baseline, though the description does not systematically walk through each parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear verb+resource: 'Book a flight from a search result.' The FLOW line positions it as the final step after search_flights, distinguishing it from related tools like book_hotel and unlock_flight_offer. The purpose is unambiguous and differentiated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The FLOW explicitly states the sequence authenticate -> search_flights -> book_flight, giving clear when-to-use context. The RESULT section provides a critical 'when not to' instruction: if booked:false, do not retry but pass the booking_url to the user. This is explicit behavioral guidance beyond simple prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

book_hotelAInspect

Book one hotel rate. The offer's full price is HELD on the connected Revolut payment method (authorised, not taken); LetsFG books and pays the supplier; the hold is captured only once the supplier has confirmed. If the booking fails for any reason the hold is released and nothing is charged. There is no reservation fee, no deposit and no pay link.

Returns a booking_job_id, NOT the booking — a booking takes minutes. Poll get_hotel_booking every ~20s until status is succeeded, failed or attention; all three are final.

Copy expected_price (the offer's price), expected_cost, currency and fx_rate from the chosen offer exactly. A USD offer sent without its currency is refused (400 price_mismatch). Guest names, phone and e-mail are checked before anything is held (400 invalid_details). guests needs ONE name per guest in the room, children included — adults first, then children in child_ages order. Do NOT call book_hotel again for a booking whose job is running — poll it; a retry returns the same job (duplicate: true).

ParametersJSON Schema
NameRequiredDescriptionDefault
emailYesThe guest's e-mail: the confirmation, or a note that it did not go through, goes here.
phoneYes
adultsNoAdults in the room, as searched (default 2)
guestsYesONE entry per guest in the room, children included: adults first, then children in the child_ages order used in search_hotels (the party travels with the offer's session). Each is {title, first_name, last_name}. The hotel requires a name for every guest; fewer names than guests is refused before anything is submitted, and the hold is released.
city_idYes
fx_rateNoThe offer's `fx_rate`, verbatim (omit for a PLN offer)
check_inYesyyyy-MM-dd
currencyNoThe offer's `currency`, verbatim (omitted means PLN)
check_outYesyyyy-MM-dd
city_nameYes
hotel_codeYesFrom the chosen hotel
hotel_nameNo
session_idYesThe chosen offer's session_id
expected_costYesThe offer's `expected_cost` (supplier cost, PLN), verbatim
combination_idNoFrom the chosen offer (optional)
expected_priceYesThe offer's `price`, verbatim
idempotency_keyNoOptional. A retry with the same key returns the booking already under way
special_requestsNo
combination_id_v2YesFrom the chosen offer — identifies that exact rate
phone_country_codeNoDefault '48'

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure and delivers thoroughly: the price is authorised but not taken, captured only after supplier confirmation, released on failure, and there is no fee/deposit/pay link. It also discloses async behavior, final statuses, validation-before-hold, and duplicate retry semantics.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but appropriately so for a high-stakes booking tool with 20 parameters. It is front-loaded with the core purpose and each paragraph covers a distinct operational concern: payment handling, async return, price copying, guest validation, and idempotency.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema and no annotations, the description explains the return value, polling workflow, final statuses, failure behavior, payment hold mechanics, validation errors, and retry idempotency. It even names the sibling polling tool, leaving little for the agent to infer.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 75%, but the description adds crucial meaning beyond the schema: currency omitted means PLN, fx_rate omitted for PLN offers, expected_price must be the offer's price verbatim, and guests must include every occupant with adults first then children in child_ages order. This materially improves parameter understanding.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Book one hotel rate', a specific verb and resource that clearly distinguishes it from book_flight. It also states it returns a booking_job_id rather than the booking, separating it from get_hotel_booking.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit post-call guidance: poll get_hotel_booking every ~20s until a final status, and 'Do NOT call book_hotel again for a booking whose job is running.' It also tells the agent to copy offer fields exactly and warns about currency mismatch, making when-to-use and when-not-to-retry explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

cancel_hotel_bookingAInspect

Cancel a hotel booking made by this account and refund the guest. A zero-charge cancellation (a refundable rate before free_cancellation_until) refunds the charge in full; a cancellation that would cost money is refused (409) — the hotel's own ladder is in the booking terms.

Takes over a minute; if it times out do NOT assume it failed — re-check before retrying.

ParametersJSON Schema
NameRequiredDescriptionDefault
confirmationYesFrom the completed booking

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it discloses refund side effects, refusal behavior with a 409 status, long execution time, and timeout semantics. This is exemplary transparency for a mutating operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each earning its place: the action, the refund/refusal policy, and the timeout caveat. The most important operational warning is front-loaded near the end but clearly separated, and there is no redundant filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description fully covers what the tool does, when it succeeds, when it fails, how long it takes, and what to do on timeout. Even without an output schema or annotations, an agent has enough to call it correctly and handle the response appropriately.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The only parameter, 'confirmation,' is fully described in the schema as 'From the completed booking.' The description adds no further parameter-level detail, but the schema coverage is 100%, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: 'Cancel a hotel booking made by this account and refund the guest.' It clearly distinguishes this from booking, searching, or getting bookings, and even clarifies the account-scoped nature of the operation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear context for when cancellation is free versus when it will be refused with a 409, and directs attention to the hotel's booking terms for cost information. It does not explicitly name alternatives or say 'use get_hotel_booking first,' but the guidance is strong enough for an agent to decide and handle the outcome.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

connect_paymentAInspect

[Developer API only — you almost certainly want authenticate instead] Mints a one-time link for connecting a payment method to a PAID prepaid Developer API account. Nothing is charged to connect and card details never touch LetsFG: a PERSON opens the returned connect_url in a browser and saves a card, Revolut Pay or Google Pay there. Do not ask a user for card numbers and do not try to automate that step. Refuses to run unless LETSFG_API_KEY is set, because agents kept calling this and creating billing accounts they did not need. Replaced setup_payment on 2026-09-08, when the Stripe lane was retired.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does so thoroughly: nothing is charged, card details never touch LetsFG, a person must open the returned connect_url, the tool refuses to run without LETSFG_API_KEY, and it replaced setup_payment. This discloses important side effects and failure conditions far beyond minimal expectations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence earns its place: an upfront routing warning, a precise functional statement, a security/behavior note, explicit anti-automation guidance, an environment prerequisite with rationale, and a deprecation note. It is front-loaded with the most important guidance and contains no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter tool with no output schema and no annotations, this description is fully sufficient. An agent knows what the tool does, when to use it instead of authenticate, what environment variable is required, what the returned connect_url is for, and that a human must complete the flow. Nothing needed for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, so the schema is trivially complete. The description adds useful operational context by mentioning the environment variable LETSFG_API_KEY and the returned connect_url, which helps an agent understand what is required and what to expect. Baseline 4 is appropriate for a no-parameter tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: it 'Mints a one-time link for connecting a payment method to a PAID prepaid Developer API account.' It also explicitly distinguishes itself from the sibling 'authenticate' by saying 'you almost certainly want authenticate instead' and notes it replaced 'setup_payment.' This is clear and well-differentiated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use and when-not-to-use guidance: it is Developer API only, users should prefer 'authenticate' unless they truly need to connect payment to a paid account, and agents must not collect card numbers or automate the browser step. It even explains the LETSFG_API_KEY requirement and why it exists, which is strong contextual guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_agent_profileAInspect

[Developer API only] Get agent profile, balance and usage stats. Read-only.

Requires LETSFG_API_KEY. A PFS Bearer token has no profile — it is bound to your payment method, carries no balance, and search and booking are free.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the burden of disclosing behavior. It states read-only, requires an API key, and explains the PFS token limitation—unexpected behavior that is valuable. It doesn't cover rate limits or error cases, but for a simple getter this is adequate.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with the primary purpose, followed by crucial usage caveats. Every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, read-only endpoint with no output schema, the description provides the essential context: what it returns (profile, balance, usage stats), auth requirements, and the PFS token edge case. It lacks explicit error behavior but otherwise is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, and the schema covers 100% of them trivially. The description adds meaningful auth context (LETSFG_API_KEY) that is not in the schema, which is helpful for correct invocation. Baseline 4 for zero-parameter tools is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool fetches agent profile, balance, and usage stats with a specific verb and resource. It also distinguishes from sibling tools by explicitly noting PFS bearer tokens have no profile, making the tool's scope clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit context: it is 'Developer API only', requires LETSFG_API_KEY, and clarifies when not to use it (PFS bearer token users). It doesn't name an alternative tool, but the context is sufficient for an agent to decide when to call it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_flight_bookingAInspect

Poll a flight booking started by book_flight. REQUIRED to learn the outcome: on a PFS Bearer token book_flight returns a booking_ref and state "booking_in_progress", not a PNR - the booking itself takes 4-11 minutes.

Call it every 20-30 s with that booking_ref until state is terminal: completed -> PNR issued, card charged failed -> the hold was released, nothing was charged needs_attention -> a human is looking at it; do NOT rebook

Do not rebook while the state is still moving, and do not treat a slow poll as a failure - the money is HELD, not taken, until the airline confirms. Refs last one hour past the booking start.

ParametersJSON Schema
NameRequiredDescriptionDefault
booking_refYesThe booking_ref book_flight returned

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It discloses that the booking takes 4-11 minutes, that money is HELD not taken until airline confirmation, the exact terminal states and their consequences, and the one-hour ref expiry. This goes beyond a simple 'poll the status' and gives the agent critical operational behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Though longer than typical descriptions, every sentence carries essential information. The purpose is front-loaded, the polling interval and terminal states are clearly structured, and warnings are explicit. No filler or repetition exists; the length is proportional to the tool's operational complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description must fully explain what the tool returns and how to interpret it. It does so by defining all terminal states and their implications (PNR issued, card charged; hold released; needs attention). Combined with polling cadence and ref expiry, an agent has everything needed to invoke and interpret the result without ambiguity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% coverage for the single parameter booking_ref, already defining it as 'The booking_ref book_flight returned'. The description adds meaningful context beyond that: the ref's one-hour lifetime and its role in repeated polling until terminal state. This extra usage-centric semantics justifies a score above the baseline 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Poll a flight booking started by book_flight.' It clearly distinguishes this from book_flight by explaining that book_flight returns a booking_ref and state 'booking_in_progress', not a PNR, and that this tool is required to learn the final outcome. Sibling differentiation is explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides precise when-to-use guidance: call every 20-30 seconds with the booking_ref until a terminal state. It enumerates terminal states and their meanings, and explicitly warns against rebooking while state is moving and against treating a slow poll as failure. It also states refs last one hour, covering lifecycle constraints.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_hotel_bookingAInspect

Collect the result of a booking started with book_hotel. Poll every ~20s.

status is in_progress, succeeded, failed or attention; the last three are final — stop polling.

  • succeeded: confirmation, hotel, room, total_price + currency (what the guest is charged), supplier_paid + supplier_currency, payment_status, refundable, free_cancellation_until, cancellation_ladder and terms.

  • failed: error, written for the guest. The hold was released; nothing was charged.

  • attention: error (+ confirmation if known). The outcome could not be settled automatically; the hold is kept (nothing charged) while a person checks with the supplier. Do NOT book again. The guest is e-mailed in every case. Read-only and safe to repeat.

ParametersJSON Schema
NameRequiredDescriptionDefault
booking_job_idYesFrom book_hotel

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses key behavioral traits beyond annotations: polling interval, final vs non-final statuses, side effects (guest is e-mailed), and safety ('Read-only and safe to repeat'). It also explains consequences for each status (hold released, nothing charged, etc.). Since no annotations are provided, the description carries the full burden and does so well, though it could mention rate limits or error handling for network issues.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded: it states the purpose first, then the polling instruction, then status breakdowns, and ends with side effects and safety. Every sentence earns its place, and the use of bullet-like formatting for statuses improves scannability without being verbose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (multiple statuses with distinct outcomes), the description is complete. It covers all possible statuses, their meanings, finality, side effects, and safety. The absence of an output schema is compensated by the detailed status breakdown. An agent has everything needed to call and interpret this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single parameter booking_job_id, and the description adds context by stating it comes 'From book_hotel'. This clarifies the parameter's origin and format, going slightly beyond the schema's bare description. A 4 is appropriate because the description adds meaningful context, though the schema already covers the basic meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Collect the result of a booking started with book_hotel.' It uses a specific verb ('collect') and resource ('result of a booking'), and explicitly references the sibling tool book_hotel, distinguishing it from other tools like get_flight_booking. The polling instruction adds clarity about its role as a status-retrieval tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit when-to-use guidance: poll every ~20s, stop on final statuses, and do NOT book again on attention. It also differentiates from alternatives by referencing book_hotel as the source of the booking_job_id. This is comprehensive and leaves no ambiguity about when to use this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_resourcesAInspect

Load the LetsFG workflow guide (3-step booking flow, pricing, passenger rules, error handling). Call this ONCE at the start of a conversation to understand how to use the flight tools correctly. Clients that support MCP resources get this automatically — this tool is for clients that do not.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It states the tool loads a guide (read behavior) but does not disclose the return format (e.g., plain text, structured JSON) or any potential side effects. For a simple loader, this is adequate but lacks some detail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with two sentences. The key action and context are front-loaded, and every sentence adds value without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a documentation-loading tool with no parameters, output schema, or complex behavior, the description is mostly complete. It could mention that the guide is returned in the response, but the purpose and usage are well covered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so per guidelines the baseline score is 4. The schema coverage is trivially 100%, and the description adds no parameter information as none are needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states it loads the LetsFG workflow guide, specifying the content areas (booking flow, pricing, rules, error handling). It clearly identifies the verb-resource pair and distinguishes from sibling tools like book_flight or search_flights.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance: call once at conversation start, and clarifies that clients with MCP resource support get this automatically, so the tool is for those without. This offers clear when-to-use and when-not-to-use context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

resolve_hotel_cityAInspect

Convert a place name to the supplier city id that search_hotels needs. Always call this first if you only have a city name. Read-only and safe to repeat.

Use Id from the first result as city_id and Name as city_name.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesPlace name (e.g., 'Warsaw', 'Paris')

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the burden and discloses that the operation is read-only and safe to repeat. It also implies multiple results by referencing 'the first result,' which adds useful behavioral context beyond the schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the main purpose and includes actionable instructions on using the result, with no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter tool with no output schema, the description is thorough: it states what the tool does, when to call it, that it is safe, and how to interpret the response. This makes it complete for agent use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already describes the 'text' parameter as a place name with 100% coverage, so the description adds little new information. It reiterates 'place name' and 'city name' without adding format or additional constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool converts a place name to the supplier city id needed by search_hotels, using a specific verb and resource. It also distinguishes from siblings by positioning it as a prerequisite for search_hotels.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly says 'Always call this first if you only have a city name,' providing a clear when-to-use condition. However, it does not mention alternatives or when not to use it, such as when the city ID is already known.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

resolve_locationAInspect

Convert a city/airport name to IATA codes. Always call before search_flights if you only have a city name. Read-only, safe to call multiple times.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYesCity or airport name (e.g., 'London', 'Berlin')

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden and explicitly states 'Read-only, safe to call multiple times', clearly indicating no side effects and idempotency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, no redundant words. The purpose is stated first, followed by usage guidance. It is concise and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one parameter, no output schema), the description fully covers its purpose and usage context. It also explains its relationship to sibling tools, making it complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with a clear description for the query parameter. The description adds context by specifying the output (IATA codes) and reiterating the parameter's purpose, adding value beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool converts a city/airport name to IATA codes, using a specific verb ('Convert') and resource. It also distinguishes itself from sibling tools by explicitly recommending it as a prerequisite for search_flights.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says 'Always call before search_flights if you only have a city name', providing clear when-to-use guidance. It also notes it is read-only and safe to call multiple times, further guiding usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_flightsAInspect

Search hundreds of airlines for live flight prices — completely FREE, unlimited, read-only.

Returns structured offers with prices, airlines, times, durations, and stopovers. Some offers carry starlink for in-flight Starlink Wi-Fi: "confirmed_all" / "confirmed_some" mean the carrier has FULLY fitted that aircraft type, "likely_all" / "likely_some" mean the rollout on that type is underway but incomplete. State only "confirmed_*" as fact; describe "likely_*" as "the airline is fitting this aircraft type, not guaranteed on your flight". Anything ending in "_some" has at least one leg WITHOUT it. An absent field means no information, NOT an absence of Wi-Fi. Covers airlines across all continents including low-cost carriers.

Search is async: this tool polls for you, including waiting out the late split-ticket merge.

Some offers are SPLIT TICKETS: two separately-issued tickets through a hub, each leg booked from whatever is cheapest for it (usually two different airlines), because no one seller offers the combination as a single ticket. They carry split_ticket: "true", combo_type: "virtual_interlining" and self_transfer: "unprotected". ALWAYS tell the user when an offer is a split ticket and what unprotected means: the tickets are not linked, so if the first flight is late and the connection is missed, the second airline owes nothing — no rebooking, no refund. Never present a split ticket as though it were one through-fare.

Requires LETSFG_BEARER_TOKEN or LETSFG_API_KEY. See letsfg://guide resource for the full authenticate->search->book workflow.

ParametersJSON Schema
NameRequiredDescriptionDefault
adultsNoNumber of adults (default: 1)
originYesIATA code of departure (e.g., 'LON', 'JFK'). Use resolve_location if you only have a name.
childrenNoNumber of children (2-11)
currencyNoISO 4217 code prices are shown in - the traveller's own (EUR, USD, GBP, BRL, PLN, ...)EUR
date_fromYesDeparture date YYYY-MM-DD
cabin_classNoM=economy, W=premium, C=business, F=first
destinationYesIATA code of arrival (e.g., 'BCN', 'LAX')
max_resultsNoMax offers to return
return_fromNoReturn date YYYY-MM-DD (omit for one-way)
departure_time_toNoLatest departure time HH:MM (e.g., '14:00')
departure_time_fromNoEarliest departure time HH:MM (e.g., '06:00')

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so thoroughly: it discloses that the call is read-only, async (polls on the caller's behalf, waits out the split-ticket merge), requires LETSFG_BEARER_TOKEN or LETSFG_API_KEY, and explains the starlink field semantics, split-ticket/unprotected-connection implications, and how absent fields should be interpreted.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but dense and front-loaded: the core purpose and read-only nature come first, then return-value semantics, then the split-ticket warning. Almost every sentence carries operational guidance, though the starlink and split-ticket paragraphs are verbose enough that some tightening is possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 11-parameter search tool with no output schema, the description compensates by explaining return fields and their interpretation. Combined with the auth and workflow pointers, an agent has enough to call and correctly report results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all 11 parameters are already documented, making 3 the baseline. The description adds interpretive value around the return payload (starlink, split_ticket) but little per-parameter detail beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: 'Search hundreds of airlines for live flight prices' and adds the read-only/free framing. An agent can immediately distinguish it from siblings like search_hotels and book_flight without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear context for when to use it (live price search, read-only) and implicitly routes name-based origins to resolve_location. It also notes the auth prerequisite and the authenticate->search->book workflow via the letsfg://guide resource, but doesn't explicitly contrast against other search/booking siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_hotelsAInspect

Search real, bookable hotel inventory. Requires a connected payment method — the SAME one that authorises flight booking. That applies to search too, not just booking, because a search opens a real session at the supplier.

Every rate type is returned, refundable and non-refundable; each offer's refundable and free_cancellation_until say which.

price is what the guest pays, in currency (USD unless you pass currency): the supplier's cost plus markup_rate (6.4% for Revolut Pay or an EEA card, 8.3% for a card issued outside the EEA). Nothing is added at booking. Keep the chosen offer whole — book_hotel needs its session_id, combination_id_v2, price, expected_cost, currency and fx_rate. Takes up to a few minutes.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax hotels to return (default 40)
adultsNoAdult guests (default 2)
city_idYesFrom resolve_hotel_city (`Id`)
check_inYesyyyy-MM-dd
childrenNoChild guests (default 0)
currencyNoISO code every price is quoted in (default USD)
check_outYesyyyy-MM-dd
city_nameYesFrom resolve_hotel_city (`Name`)
child_agesNoAge of each child; the supplier needs these to price
nationalityNoTwo-letter guest nationality. Rates and taxes genuinely differ by it.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden and does so excellently. It discloses the surprising session-opening behavior, the payment prerequisite, refundability fields, exact price composition and markup rates, the fact that nothing is added at booking, the fields book_hotel needs, and the expected latency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence earns its place: search scope, prerequisite, rate-type behavior, pricing formula, downstream contract, and latency. It is front-loaded with the core purpose and keeps related details grouped logically, with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 10 parameters, no output schema, and no annotations, this description is unusually complete. It covers prerequisites, side effects, price semantics, required downstream identifiers, rate-type distinctions, and timing. An agent can invoke it and pass the result to book_hotel with confidence.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds meaning beyond the schema by explaining how price relates to currency and markup_rate, confirming child_ages are needed for supplier pricing, and noting that nationality genuinely changes rates and taxes. This is useful but supplementary rather than essential.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb-resource pair, 'Search real, bookable hotel inventory,' and clearly distinguishes itself from booking, cancellation, and city-resolution tools. It leaves no ambiguity that this tool returns bookable hotel offers rather than managing bookings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear contextual usage guidance: a payment method must be connected, search itself opens a supplier session, and the results feed directly into book_hotel. It does not explicitly enumerate when-not-to-use alternatives, but there is no competing hotel-search sibling, and the downstream/upstream relationship is strongly implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev2026.5.102
    • Changedsearch_flights1 field changed
      • changedInput schema / properties / currency / description
        Previous value: -"Currency code (EUR, USD, GBP)"New value: +"ISO 4217 code prices are shown in - the traveller's own (EUR, USD, GBP, BRL, PLN, ...)"
  2. 8 tool updatesv2026.5.101
    • Addedanswer_booking_question
    • Changedauthenticate3 fields changed
      • removedInput schema / properties / card_token
        Removed value: -{
        -  "description": "Single-use Stripe tok_... minted against the LetsFG publishable key — the headless path",
        -  "type": "string"
        -}
      • removedInput schema / properties / payment_method_id
        Removed value: -{
        -  "description": "Stripe pm_... for a card ALREADY enrolled through this flow (re-issue only)",
        -  "type": "string"
        -}
      • removedInput schema / properties / setup_session_id
        Removed value: -{
        -  "description": "cs_... from a previous authenticate call, after the card was added",
        -  "type": "string"
        -}
    • Changedbook_hotel10 fields changed
      • addedInput schema / properties / adults / description
        Added value: +"Adults in the room, as searched (default 2)"
      • addedInput schema / properties / currency
        Added value: +{
        +  "description": "The offer's `currency`, verbatim (omitted means PLN)",
        +  "type": "string"
        +}
      • changedInput schema / properties / email / description
        Previous value: -"The voucher and pay link go here. A typo loses the booking."New value: +"The guest's e-mail: the confirmation, or a note that it did not go through, goes here."
      • removedInput schema / properties / expected_balance
        Removed value: -{
        -  "description": "The offer's `balance_to_supplier`, verbatim",
        -  "type": "number"
        -}
      • addedInput schema / properties / expected_cost
        Added value: +{
        +  "description": "The offer's `expected_cost` (supplier cost, PLN), verbatim",
        +  "type": "number"
        +}
      • addedInput schema / properties / fx_rate
        Added value: +{
        +  "description": "The offer's `fx_rate`, verbatim (omit for a PLN offer)",
        +  "type": "number"
        +}
      • changedInput schema / properties / guests / description
        Previous value: -"One entry per guest: {title, first_name, last_name}"New value: +"ONE entry per guest in the room, children included: adults first, then children in the child_ages order used in search_hotels (the party travels with the offer's session). Each is {title, first_name, last_name}. The hotel requires a name for every guest; fewer names than guests is refused before anything is submitted, and the hold is released."
      • addedInput schema / properties / idempotency_key
        Added value: +{
        +  "description": "Optional. A retry with the same key returns the booking already under way",
        +  "type": "string"
        +}
      • changedInput schema / properties / session_id / description
        Previous value: -"From search_hotels"New value: +"The chosen offer's session_id"
      • changedInput schema / required
        Previous value: -[
        -  "session_id",
        -  "hotel_code",
        -  "combination_id_v2",
        -  "expected_price",
        -  "expected_balance",
        -  "city_id",
        -  "city_name",
        -  "check_in",
        -  "check_out",
        -  "guests",
        -  "email",
        -  "phone"
        -]New value: +[
        +  "session_id",
        +  "hotel_code",
        +  "combination_id_v2",
        +  "expected_price",
        +  "expected_cost",
        +  "city_id",
        +  "city_name",
        +  "check_in",
        +  "check_out",
        +  "guests",
        +  "email",
        +  "phone"
        +]
    • Addedconnect_payment
    • Addedget_flight_booking
    • Changedsearch_hotels1 field changed
      • addedInput schema / properties / currency
        Added value: +{
        +  "description": "ISO code every price is quoted in (default USD)",
        +  "type": "string"
        +}
    • Removedsetup_payment
    • Removedunlock_flight_offer
  3. 11 tool updatesv2026.5.88
    • Addedauthenticate
    • Changedbook_flight3 fields changed
      • changedInput schema / properties / offer_id / description
        Previous value: -"Unlocked offer ID (off_xxx)"New value: +"Offer ID from search_flights"
      • addedInput schema / properties / search_id
        Added value: +{
        +  "description": "search_id from search_flights",
        +  "type": "string"
        +}
      • changedInput schema / required
        Previous value: -[
        -  "offer_id",
        -  "passengers",
        -  "contact_email"
        -]New value: +[
        +  "search_id",
        +  "offer_id",
        +  "passengers",
        +  "contact_email"
        +]
    • Addedbook_hotel
    • Addedcancel_hotel_booking
    • Addedget_hotel_booking
    • Removedlink_github
    • Addedresolve_hotel_city
    • Changedsearch_flights4 fields changed
      • addedInput schema / properties / departure_time_from
        Added value: +{
        +  "description": "Earliest departure time HH:MM (e.g., '06:00')",
        +  "type": "string"
        +}
      • addedInput schema / properties / departure_time_to
        Added value: +{
        +  "description": "Latest departure time HH:MM (e.g., '14:00')",
        +  "type": "string"
        +}
      • removedInput schema / properties / max_browsers
        Removed value: -{
        -  "description": "Max concurrent browser processes (1-32). Default: auto-detect from system RAM.",
        -  "type": "integer"
        -}
      • removedInput schema / properties / mode
        Removed value: -{
        -  "description": "Omit for full search (200+ connectors). 'fast' = ~25 connectors, 20-40s.",
        -  "enum": [
        -    "fast"
        -  ],
        -  "type": "string"
        -}
    • Addedsearch_hotels
    • Removedstart_checkout
    • Removedsystem_info
  4. 10 tool updatesv2026.5.76
    • Addedbook_flight
    • Addedget_agent_profile
    • Addedlink_github
    • Addedload_resources
    • Addedresolve_location
    • Addedsearch_flights
    • Addedsetup_payment
    • Addedstart_checkout
    • Addedsystem_info
    • Addedunlock_flight_offer

TDQS

A4.2/5.0

Scored across 14 tools

Disambiguation3/5

Most tools target distinct resources, but there is clear overlap between authenticate and connect_payment (both about connecting payment), and between resolve_hotel_city and resolve_location (both resolve place names). The descriptions attempt to disambiguate, but the presence of legacy/retired alternatives creates real risk of misselection.

Naming Consistency4/5

The set is heavily verb_noun consistent: resolve_*, search_*, book_*, get_*_booking, load_resources, cancel_hotel_booking. Minor deviations are only the bare authenticate and the pairing load_resources vs get_agent_profile, which is acceptable.

Tool Count4/5

14 tools is a reasonable count for a flight+hotel booking server. Some padding exists (connect_payment, load_resources, get_agent_profile are ancillary), but nothing feels excessive for the domain.

Completeness3/5

The hotel surface is complete (search, book, poll, cancel), but flights lack cancel_flight and any equivalent to answer_booking_question for flight bookings. There is no flight cancellation tool and the flight booking lifecycle ends at get_flight_booking, leaving a notable gap.

Maintenance

ActivityActive
ResponsivenessResponsive

Related MCP Connectors

Related MCP Servers