letsfg 2026.5.73 → 2026.5.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,283 +1,321 @@
1
- # LetsFG — Your AI agent just learned to book flights. (Node.js)
2
-
3
- **Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
4
-
5
- [![GitHub stars](https://img.shields.io/github/stars/LetsFG/LetsFG?style=social)](https://github.com/LetsFG/LetsFG)
6
- [![npm](https://img.shields.io/npm/v/letsfg)](https://www.npmjs.com/package/letsfg)
7
-
8
- ## Two ways to use LetsFG
9
-
10
- | | **CLI / SDK** (this package) | **Developer API** |
11
- |---|---|---|
12
- | **Search cost** | Free (card-backed token from [letsfg.co/connect](https://letsfg.co/connect), nothing charged) | Prepaid credits |
13
- | **Booking** | `POST /api/agent-book` — fare held on your card, a LetsFG agent buys the ticket, captured only on a real PNR. Every offer. | Direct airline URL (unlock required first) |
14
- | **Speed** | 8–10 s to first results; longer on a split | 2–5 s (discover) · 8–10 s to first results (full) |
15
- | **Setup** | `npm install letsfg`, then connect at [letsfg.co/developers/api/mcp](https://letsfg.co/developers/api/mcp) | [letsfg.co/developers](https://letsfg.co/developers) |
16
-
17
- > **Building a product, or need hotels?** Use the [Developer API](https://letsfg.co/developers) — look-to-book search (200 free after every booking, then $0.01), booking through `POST /flights/book`, no booking fee and no transaction fee.
18
-
19
- ## Install
20
-
21
- ```bash
22
- npm install letsfg
23
- ```
24
-
25
- ## Getting a token
26
-
27
- Connect LetsFG as an MCP server at `https://letsfg.co/developers/api/mcp` and
28
- approve the connection — in Claude, ChatGPT, Cursor, Windsurf, or Claude Code
29
- (`claude mcp add --transport http letsfg https://letsfg.co/developers/api/mcp`).
30
- The consent step opens [letsfg.co/connect](https://letsfg.co/connect), where you
31
- add a card (any card, or Revolut Pay / Google Pay) in a 0.00 Revolut setup.
32
- Nothing is charged, no Revolut account is needed, and the card details go to
33
- Revolut, never to LetsFG. The token you get back is card-backed: it searches
34
- and it books. One card = one account; quotas are per card (10 searches per
35
- 10 min, 30 per hour, 100 per day — polling never counts).
36
-
37
- Pass it as `bearerToken`, or set `LETSFG_BEARER_TOKEN` for the CLI.
38
-
39
- > `letsfg auth` still runs the Stripe card setup that was retired on
40
- > 2026-09-02 and cannot get a token today; every token issued that way was
41
- > revoked (401 `TOKEN_REVOKED`). A connect-flow login for the CLI and SDKs is
42
- > coming — until then, connect through the MCP.
43
-
44
- ## Quick Start (SDK)
45
-
46
- ```typescript
47
- import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
48
-
49
- // PFS — free. The card-backed token from the connect flow (see above).
50
- const bt = new LetsFG({ bearerToken: 'eyJ...' });
51
-
52
- // Search — FREE
53
- const flights = await bt.search('GDN', 'BER', '2026-03-03');
54
- const best = cheapestOffer(flights);
55
- console.log(offerSummary(best));
56
-
57
- // Book — no booking fee, no transaction fee — our margin is already in the price you saw; no unlock step. Starts the booking:
58
- // the fare is HELD on your card and a LetsFG agent buys the ticket (4-11 min).
59
- const result = await bt.book(
60
- best.id,
61
- [{
62
- given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm',
63
- nationality: 'GB', phone_number: '+447700900123', phone_country: 'GB',
64
- address_line1: '1 Analytical Way', address_city: 'London',
65
- address_postal: 'N1 9GU', address_country: 'GB',
66
- }],
67
- 'john@example.com',
68
- '',
69
- '',
70
- flights.search_id,
71
- );
72
- const bookingRef = result.booking_ref as string;
73
-
74
- // Poll until it lands (every 20-30 s): completed | failed | needs_attention
75
- let status: Record<string, unknown>;
76
- do {
77
- await new Promise(r => setTimeout(r, 25_000));
78
- status = await (await fetch('https://letsfg.co/api/agent-book/status', {
79
- method: 'POST',
80
- headers: { Authorization: 'Bearer eyJ...', 'Content-Type': 'application/json' },
81
- body: JSON.stringify({ booking_ref: bookingRef }),
82
- })).json();
83
- } while (status.state === 'booking_in_progress');
84
- console.log(status); // { state: 'completed', pnr: 'ABC123', charged_amount: 93, currency: 'EUR' }
85
- ```
86
-
87
- ### How booking works
88
-
89
- `bt.book()` posts to `POST /api/agent-book` and does exactly what the website
90
- checkout does: the fare plus LetsFG's markup is **held** on the connected card
91
- (not taken), a LetsFG booking agent buys the ticket from the seller, and the
92
- hold is captured only once a real airline PNR exists. If the booking fails the
93
- hold is released and nothing is charged. Every offer can be booked this way —
94
- no unlock step, no booking-link fallback, no separate LetsFG fee.
95
-
96
- The call returns within seconds with a `booking_ref`; the booking itself takes
97
- 4–11 minutes. Poll `POST /api/agent-book/status` with `{"booking_ref": ...}`
98
- every 20–30 s (the SDK has no helper for this yet):
99
-
100
- | `state` | Meaning |
101
- |---|---|
102
- | `booking_in_progress` | the agent is at the seller's checkout — keep waiting |
103
- | `completed` | booked — `pnr`, `charged_amount`, `currency` are in the answer |
104
- | `failed` | not booked — the hold was released, nothing charged; see `failure_reason` |
105
- | `needs_attention` | a human at LetsFG is checking it — do **not** book again |
106
-
107
- One traveller per call, with the details an airline checkout asks for: name,
108
- date of birth, gender, nationality, email, phone with its country, residence
109
- address (passport optional). A missing detail returns `missing_details` with
110
- `missing_fields` and charges nothing. Never start a second booking for the
111
- same trip while one is in progress — that would place a second hold.
112
-
113
- Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
114
- `search()`/`book()` dispatch automatically. That path needs a `searchId` (an offer
115
- is bookable only inside the search that produced it) and has no unlock step:
116
- `unlock()` was retired 2026-09-08, the route answers `410 Gone`, and there is no fee.
117
-
118
- ## Quick Start (CLI)
119
-
120
- ```bash
121
- export LETSFG_BEARER_TOKEN=<your-bearer-token> # card-backed, from the connect flow
122
-
123
- letsfg search GDN BER 2026-03-03 --sort price
124
- letsfg search LON BCN 2026-04-01 --json # Machine-readable
125
- letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m","nationality":"GB","phone_number":"+447700900123","phone_country":"GB","address_line1":"1 Analytical Way","address_city":"London","address_postal":"N1 9GU","address_country":"GB"}' -e john@example.com
126
- # prints the booking_ref — poll POST /api/agent-book/status until completed
127
- ```
128
-
129
- ## API
130
-
131
- ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
132
-
133
- ### `bt.search(origin, destination, dateFrom, options?)`
134
- ### `bt.resolveLocation(query)`
135
- ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
136
- Dispatches on which credential is set: `bearerToken` → PFS booking via
137
- `POST /api/agent-book` (pass `searchId`, one passenger with full details;
138
- returns `booking_ref` — poll `POST /api/agent-book/status`). `apiKey` → paid
139
- Developer API `book` (requires `searchId`, no unlock step, supports multiple
140
- passengers and `idempotencyKey`; returns a `booking_id` to poll).
141
- ### `bt.getBooking(bookingId)` / `bt.answerBooking(...)` / `bt.bookAndWait(...)` — Developer API only
142
- ### `bt.connectPayment()` — Developer API only. Returns `connect_url`; nothing is charged
143
- ### `bt.setupPayment()` / `bt.unlock()` — **retired 2026-09-08, both throw locally**
144
- ### `bt.me()`
145
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
146
-
147
- ### Helpers
148
- - `offerSummary(offer)` — One-line string summary
149
- - `cheapestOffer(result)` — Get cheapest offer from search
150
-
151
- ## Starlink Wi-Fi
152
-
153
- Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
154
- has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
155
- rollout on that type is underway but incomplete. Segments carry `confirmed` or
156
- `likely`.
157
-
158
- Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
159
- promise. Anything ending `_some` has at least one leg without it. An **absent**
160
- field means no information, **not** an absence of Wi-Fi.
161
-
162
- Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
163
-
164
- ## Zero Dependencies
165
-
166
- Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
167
-
168
- ## Also Available As
169
-
170
- - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
171
- - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
172
- - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
173
- - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
174
-
175
- > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
176
-
177
- ## License
178
-
179
- MIT
180
-
181
- ## 🏨 Hotels — new, and live
182
-
183
- Your agent can book hotels as well as flights. Same card-backed token or API key, same card on file.
184
-
185
- ```python
186
- from letsfg import LetsFG
187
- lfg = LetsFG()
188
-
189
- city = lfg.hotel_destinations("Warsaw")[0]
190
- stays = lfg.search_hotels(
191
- city_id=city["Id"], city_name=city["Name"],
192
- check_in="2026-11-10", check_out="2026-11-12", adults=2,
193
- )
194
-
195
- hotel = stays["hotels"][0]
196
- offer = hotel["offers"][0]
197
- print(hotel["name"], offer["price"], stays["currency"])
198
- # Hotel Gromada Warszawa Centrum 669.86 PLN
199
-
200
- booking = lfg.book_hotel_and_wait(
201
- session_id=stays["session_id"],
202
- hotel_code=hotel["hotel_code"],
203
- combination_id_v2=offer["combination_id_v2"],
204
- expected_price=offer["price"],
205
- expected_balance=offer["balance_to_supplier"],
206
- city_id=city["Id"], city_name=city["Name"],
207
- check_in="2026-11-10", check_out="2026-11-12",
208
- guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
209
- email="guest@example.com", phone="512345678",
210
- )
211
- print(booking["confirmation"], booking["pay_link"])
212
- ```
213
-
214
- ### How you pay
215
-
216
- **5% now, the rest to the hotel later.** At booking we charge 5% of the price
217
- to your card as a reservation fee. The remaining balance is paid **directly to
218
- the supplier** through a `pay_link` we return — we never hold it.
219
-
220
- `balance_due_by` is the supplier's own auto-cancellation date, not a date we
221
- invent. Miss it and the room is released.
222
-
223
- The 5% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
224
- else; after it, the hotel's own cancellation ladder applies and can reach 100%.
225
- That ladder ships in the booking's `terms`, so you can always see the cost before
226
- you cancel.
227
-
228
- ### What search costs
229
-
230
- Search is metered separately from booking, on **either** auth path (free PFS
231
- Bearer token or Developer API key — both count against the same agent):
232
- **the first 1,000 `search_hotels` calls since your last hotel booking are
233
- free.** Past that, searches are billed in blocks of 1,000 for **$5**
234
- (~$0.005/search) from your prepaid balance — refused with a 402 if the
235
- balance can't cover the next block, never silently allowed. Book a hotel
236
- and the count resets to zero. Resolving a city name (`hotel_destinations`)
237
- is not metered, only the search call itself.
238
-
239
- ### Things worth knowing before you build
240
-
241
- - **A card on file is required for every hotel call, including search.** That is
242
- unusual and it is deliberate: a hotel search opens a real session at the
243
- supplier, and booking blocks a real rate. We would rather refuse up front than
244
- let you reach the point of commitment and discover you cannot pay. The same
245
- card that authorises flight booking authorises hotels — there is no separate
246
- hotel signup.
247
- - **Only free-cancellation, pay-later rates are sold.** Those are the rates where
248
- the balance can safely be settled with the supplier after booking, which is
249
- what makes 5%-now/rest-later work at all. You will see fewer results than a
250
- metasearch shows you. Every one of them can actually be booked.
251
- - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
252
- booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
253
- `status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
254
- SDK do it. This is not ceremony: it is what makes it impossible to charge a
255
- card and then lose the confirmation to a timeout.
256
- - **The fee is charged before the room is committed.** A declined card therefore
257
- costs nothing to unwind — no reservation exists and nothing is charged.
258
- - **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
259
- rate books the room twice and charges two reservation fees.
260
- - `price` is what the guest pays. There is no wholesale figure in the response to
261
- quote by mistake.
262
-
263
- ### JavaScript
264
-
265
- ```javascript
266
- import { LetsFG } from 'letsfg';
267
- const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
268
-
269
- const [city] = await lfg.hotelDestinations('Warsaw');
270
- const stays = await lfg.searchHotels({
271
- cityId: city.Id, cityName: city.Name,
272
- checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
273
- });
274
-
275
- const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
276
- console.log(booking.confirmation, booking.pay_link);
277
- ```
278
-
279
- ### MCP
280
-
281
- Five new tools, in the order you call them: `resolve_hotel_city` →
282
- `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
283
-
1
+ # LetsFG — Your AI agent just learned to book flights. (Node.js)
2
+
3
+ **Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
4
+
5
+ [![GitHub stars](https://img.shields.io/github/stars/LetsFG/LetsFG?style=social)](https://github.com/LetsFG/LetsFG)
6
+ [![npm](https://img.shields.io/npm/v/letsfg)](https://www.npmjs.com/package/letsfg)
7
+
8
+ ## Two ways to use LetsFG
9
+
10
+ | | **CLI / SDK** (this package) | **Developer API** |
11
+ |---|---|---|
12
+ | **Search cost** | Free (card-backed token from [letsfg.co/connect](https://letsfg.co/connect), nothing charged) | Prepaid credits |
13
+ | **Booking** | `POST /api/agent-book` — fare held on your card, a LetsFG agent buys the ticket, captured only on a real PNR. Every offer. | Direct airline URL (unlock required first) |
14
+ | **Speed** | 8–10 s to first results; longer on a split | 2–5 s (discover) · 8–10 s to first results (full) |
15
+ | **Setup** | `npm install letsfg`, then connect at [letsfg.co/developers/api/mcp](https://letsfg.co/developers/api/mcp) | [letsfg.co/developers](https://letsfg.co/developers) |
16
+
17
+ > **Building a product, or need hotels?** Use the [Developer API](https://letsfg.co/developers) — look-to-book search (200 free after every booking, then $0.01), booking through `POST /flights/book`, no booking fee and no transaction fee.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install letsfg
23
+ ```
24
+
25
+ ## Getting a token
26
+
27
+ Connect LetsFG as an MCP server at `https://letsfg.co/developers/api/mcp` and
28
+ approve the connection — in Claude, ChatGPT, Cursor, Windsurf, or Claude Code
29
+ (`claude mcp add --transport http letsfg https://letsfg.co/developers/api/mcp`).
30
+ The consent step opens [letsfg.co/connect](https://letsfg.co/connect), where you
31
+ add a card (any card, or Revolut Pay / Google Pay) in a 0.00 Revolut setup.
32
+ Nothing is charged, no Revolut account is needed, and the card details go to
33
+ Revolut, never to LetsFG. The token you get back is card-backed: it searches
34
+ and it books. One card = one account; quotas are per card (10 searches per
35
+ 10 min, 30 per hour, 100 per day — polling never counts).
36
+
37
+ Pass it as `bearerToken`, or set `LETSFG_BEARER_TOKEN` for the CLI.
38
+
39
+ > `letsfg auth` still runs the Stripe card setup that was retired on
40
+ > 2026-09-02 and cannot get a token today; every token issued that way was
41
+ > revoked (401 `TOKEN_REVOKED`). A connect-flow login for the CLI and SDKs is
42
+ > coming — until then, connect through the MCP.
43
+
44
+ ## Quick Start (SDK)
45
+
46
+ ```typescript
47
+ import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
48
+
49
+ // PFS — free. The card-backed token from the connect flow (see above).
50
+ const bt = new LetsFG({ bearerToken: 'eyJ...' });
51
+
52
+ // Search — FREE
53
+ const flights = await bt.search('GDN', 'BER', '2026-03-03');
54
+ const best = cheapestOffer(flights);
55
+ console.log(offerSummary(best));
56
+
57
+ // Book — no booking fee, no transaction fee — our margin is already in the price you saw; no unlock step. Starts the booking:
58
+ // the fare is HELD on your card and a LetsFG agent buys the ticket (4-11 min).
59
+ const result = await bt.book(
60
+ best.id,
61
+ [{
62
+ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm',
63
+ nationality: 'GB', phone_number: '+447700900123', phone_country: 'GB',
64
+ address_line1: '1 Analytical Way', address_city: 'London',
65
+ address_postal: 'N1 9GU', address_country: 'GB',
66
+ }],
67
+ 'john@example.com',
68
+ '',
69
+ '',
70
+ flights.search_id,
71
+ );
72
+ const bookingRef = result.booking_ref as string;
73
+
74
+ // Poll until it lands (every 20-30 s): completed | failed | needs_attention
75
+ let status: Record<string, unknown>;
76
+ do {
77
+ await new Promise(r => setTimeout(r, 25_000));
78
+ status = await (await fetch('https://letsfg.co/api/agent-book/status', {
79
+ method: 'POST',
80
+ headers: { Authorization: 'Bearer eyJ...', 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ booking_ref: bookingRef }),
82
+ })).json();
83
+ } while (status.state === 'booking_in_progress');
84
+ console.log(status); // { state: 'completed', pnr: 'ABC123', charged_amount: 93, currency: 'EUR' }
85
+ ```
86
+
87
+ ### How booking works
88
+
89
+ `bt.book()` posts to `POST /api/agent-book` and does exactly what the website
90
+ checkout does: the fare plus LetsFG's markup is **held** on the connected card
91
+ (not taken), a LetsFG booking agent buys the ticket from the seller, and the
92
+ hold is captured only once a real airline PNR exists. If the booking fails the
93
+ hold is released and nothing is charged. Every offer can be booked this way —
94
+ no unlock step, no booking-link fallback, no separate LetsFG fee.
95
+
96
+ The call returns within seconds with a `booking_ref`; the booking itself takes
97
+ 4–11 minutes. Poll `POST /api/agent-book/status` with `{"booking_ref": ...}`
98
+ every 20–30 s (the SDK has no helper for this yet):
99
+
100
+ | `state` | Meaning |
101
+ |---|---|
102
+ | `booking_in_progress` | the agent is at the seller's checkout — keep waiting |
103
+ | `completed` | booked — `pnr`, `charged_amount`, `currency` are in the answer |
104
+ | `failed` | not booked — the hold was released, nothing charged; see `failure_reason` |
105
+ | `needs_attention` | a human at LetsFG is checking it — do **not** book again |
106
+
107
+ One traveller per call, with the details an airline checkout asks for: name,
108
+ date of birth, gender, nationality, email, phone with its country, residence
109
+ address (passport optional). A missing detail returns `missing_details` with
110
+ `missing_fields` and charges nothing. Never start a second booking for the
111
+ same trip while one is in progress — that would place a second hold.
112
+
113
+ Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
114
+ `search()`/`book()` dispatch automatically. That path needs a `searchId` (an offer
115
+ is bookable only inside the search that produced it) and has no unlock step:
116
+ `unlock()` was retired 2026-09-08, the route answers `410 Gone`, and there is no fee.
117
+
118
+ ## Quick Start (CLI)
119
+
120
+ ```bash
121
+ export LETSFG_BEARER_TOKEN=<your-bearer-token> # card-backed, from the connect flow
122
+
123
+ letsfg search GDN BER 2026-03-03 --sort price
124
+ letsfg search LON BCN 2026-04-01 --json # Machine-readable
125
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m","nationality":"GB","phone_number":"+447700900123","phone_country":"GB","address_line1":"1 Analytical Way","address_city":"London","address_postal":"N1 9GU","address_country":"GB"}' -e john@example.com
126
+ # prints the booking_ref — poll POST /api/agent-book/status until completed
127
+ ```
128
+
129
+ ## API
130
+
131
+ ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
132
+
133
+ ### `bt.search(origin, destination, dateFrom, options?)`
134
+ ### `bt.resolveLocation(query)`
135
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
136
+ Dispatches on which credential is set: `bearerToken` → PFS booking via
137
+ `POST /api/agent-book` (pass `searchId`, one passenger with full details;
138
+ returns `booking_ref` — poll `POST /api/agent-book/status`). `apiKey` → paid
139
+ Developer API `book` (requires `searchId`, no unlock step, supports multiple
140
+ passengers and `idempotencyKey`; returns a `booking_id` to poll).
141
+ ### `bt.getBooking(bookingId)` / `bt.answerBooking(...)` / `bt.bookAndWait(...)` — Developer API only
142
+ ### `bt.connectPayment()` — Developer API only. Returns `connect_url`; nothing is charged
143
+ ### `bt.setupPayment()` / `bt.unlock()` — **retired 2026-09-08, both throw locally**
144
+ ### `bt.me()`
145
+ ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
146
+
147
+ ### Helpers
148
+ - `offerSummary(offer)` — One-line string summary
149
+ - `cheapestOffer(result)` — Get cheapest offer from search
150
+
151
+ ## Starlink Wi-Fi
152
+
153
+ Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
154
+ has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
155
+ rollout on that type is underway but incomplete. Segments carry `confirmed` or
156
+ `likely`.
157
+
158
+ Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
159
+ promise. Anything ending `_some` has at least one leg without it. An **absent**
160
+ field means no information, **not** an absence of Wi-Fi.
161
+
162
+ Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
163
+
164
+ ## Zero Dependencies
165
+
166
+ Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
167
+
168
+ ## Also Available As
169
+
170
+ - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
171
+ - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
172
+ - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
173
+ - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
174
+
175
+ > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
176
+
177
+ ## License
178
+
179
+ MIT
180
+
181
+ ## 🏨 Hotels — new, and live
182
+
183
+ Your agent can book hotels as well as flights. Same card-backed token or API key, same connected payment method.
184
+
185
+ ```python
186
+ from letsfg import LetsFG
187
+ lfg = LetsFG()
188
+
189
+ city = lfg.hotel_destinations("Warsaw")[0]
190
+ stays = lfg.search_hotels(
191
+ city_id=city["Id"], city_name=city["Name"],
192
+ check_in="2026-11-10", check_out="2026-11-12", adults=2,
193
+ )
194
+
195
+ hotel = stays["hotels"][0]
196
+ offer = hotel["offers"][0]
197
+ print(hotel["name"], offer["price"], offer["currency"], offer["refundable"])
198
+ # prices are in stays["currency"]: USD unless you pass currency=
199
+
200
+ booking = lfg.book_hotel_and_wait(
201
+ session_id=offer["session_id"],
202
+ hotel_code=hotel["hotel_code"],
203
+ combination_id_v2=offer["combination_id_v2"],
204
+ expected_price=offer["price"], # copy these four from the offer, verbatim
205
+ expected_cost=offer["expected_cost"],
206
+ currency=offer["currency"],
207
+ fx_rate=offer["fx_rate"],
208
+ city_id=city["Id"], city_name=city["Name"],
209
+ check_in="2026-11-10", check_out="2026-11-12",
210
+ # ONE entry per guest in the room, children included: adults first, then children in child_ages order
211
+ guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"},
212
+ {"title": "Mrs", "first_name": "Anna", "last_name": "Kowalska"}],
213
+ email="GUEST_EMAIL", phone="512345678", # the guest's real e-mail and phone
214
+ )
215
+ if booking["status"] == "succeeded":
216
+ print(booking["confirmation"], booking["total_price"], booking["currency"])
217
+ elif booking["status"] == "failed":
218
+ print("Not booked, nothing charged:", booking["error"])
219
+ else: # "attention": a person is checking it with the supplier, the hold is kept. Do not book again.
220
+ print(booking["status"], booking.get("error"))
221
+ ```
222
+
223
+ ### How you pay
224
+
225
+ **The full price is held, and taken only once the hotel confirms.** Booking holds the offer's
226
+ `price` on the Revolut payment method connected to your account — authorised, not charged.
227
+ LetsFG then books the room with the supplier and pays the supplier itself. The hold is captured
228
+ only after the supplier has confirmed the booking; if the booking fails for any reason (the rate
229
+ is gone, the price moved, the supplier declined, a guest detail was rejected), the hold is
230
+ released and nothing is charged.
231
+
232
+ There is **no reservation fee, no deposit and no pay link**, and the guest owes the hotel nothing
233
+ further. (Those belonged to the process retired on 2026-09-11.)
234
+
235
+ `price` is the supplier's cost plus 6.4% (our margin and payment processing) for Revolut Pay or a
236
+ card issued in the EEA, or 8.3% for a card issued outside the EEA — the search response's
237
+ `markup_rate` says which. Nothing is added at booking. Prices are in the currency you search in:
238
+ USD unless you ask for another.
239
+
240
+ Cancelling a refundable rate before its `free_cancellation_until` costs nothing and refunds the
241
+ charge in full. A cancellation that would cost money is refused (409); the hotel's own ladder is in
242
+ the booking's `terms`.
243
+
244
+ ### What search costs
245
+
246
+ Search is metered separately from booking, on **either** auth path (free PFS
247
+ Bearer token or Developer API key — both count against the same agent):
248
+ **the first 1,000 `search_hotels` calls since your last hotel booking are
249
+ free.** Past that, searches are billed in blocks of 1,000 for **$5**
250
+ (~$0.005/search) from your prepaid balance — refused with a 402 if the
251
+ balance can't cover the next block, never silently allowed. Book a hotel
252
+ and the count resets to zero. Resolving a city name (`hotel_destinations`)
253
+ is not metered, only the search call itself.
254
+
255
+ ### Things worth knowing before you build
256
+
257
+ - **A connected payment method is required for every hotel call, including search.** A hotel
258
+ search opens a real session at the supplier and booking blocks a real rate, so we refuse up
259
+ front rather than let you reach the point of commitment and discover you cannot pay. The same
260
+ method authorises flights and hotels — there is no separate hotel signup.
261
+ - **Every rate type is sold**, refundable and non-refundable. Each offer's `refundable` and
262
+ `free_cancellation_until` say which one you are buying.
263
+ - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a booking — the real
264
+ thing takes minutes. Poll `hotel_booking(job_id)` every ~20 s until `status` is `succeeded`,
265
+ `failed` or `attention`, or call `book_hotel_and_wait` and let the SDK do it. All three are final:
266
+ - `succeeded` — `confirmation`, `total_price` + `currency` (what the guest is charged),
267
+ `supplier_paid` + `supplier_currency`, `refundable`, `free_cancellation_until`,
268
+ `cancellation_ladder` and `terms`.
269
+ - `failed` — `error`, written for the guest. The hold has been released; nothing was charged.
270
+ - `attention` — the outcome could not be settled automatically. The hold is kept (nothing is
271
+ charged) while a person checks with the supplier. Do not book again.
272
+ - **Copy the offer verbatim.** Send `expected_price` (the offer's `price`), `expected_cost`,
273
+ `currency` and `fx_rate` exactly as search returned them. A mis-copied price, or a USD offer sent
274
+ without its `currency`, is refused with `400 price_mismatch` before anything is held.
275
+ - **Guest details are checked before anything is held**: Latin-script names, a phone number valid
276
+ for its country code, and an e-mail. A problem returns `400 invalid_details` naming the fields.
277
+ - **One name per guest in the room, children included.** For a family, search with `children` and
278
+ `child_ages`; the party travels with the offer's session. `guests` then lists every guest — adults
279
+ first, then children in `child_ages` order. The hotel requires a name for every guest: fewer names
280
+ than guests is refused before anything is submitted, and the hold is released.
281
+ - **The guest is e-mailed however it ends**: a confirmation with the code and the cancellation
282
+ term, a note that it did not go through and nothing was charged, or a note that it is being
283
+ confirmed with the supplier.
284
+ - **A retry never books twice.** A second `book_hotel` for the same rate and guest returns the job
285
+ already under way (`duplicate: true`); pass `idempotency_key` to make that explicit. Still, never
286
+ re-post a booking whose job is running — poll it.
287
+
288
+ ### JavaScript
289
+
290
+ ```javascript
291
+ import { LetsFG } from 'letsfg';
292
+ const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
293
+
294
+ const [city] = await lfg.hotelDestinations('Warsaw');
295
+ const stays = await lfg.searchHotels({
296
+ cityId: city.Id, cityName: city.Name,
297
+ checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
298
+ });
299
+
300
+ const hotel = stays.hotels[0];
301
+ const offer = hotel.offers[0];
302
+ const booking = await lfg.bookHotelAndWait({
303
+ sessionId: offer.session_id, hotelCode: hotel.hotel_code,
304
+ combinationIdV2: offer.combination_id_v2,
305
+ expectedPrice: offer.price, expectedCost: offer.expected_cost,
306
+ currency: offer.currency, fxRate: offer.fx_rate,
307
+ cityId: city.Id, cityName: city.Name,
308
+ checkIn: '2026-11-10', checkOut: '2026-11-12',
309
+ // ONE entry per guest in the room, children included: adults first, then children in childAges order
310
+ guests: [{ title: 'Mr', first_name: 'Jan', last_name: 'Kowalski' },
311
+ { title: 'Mrs', first_name: 'Anna', last_name: 'Kowalska' }],
312
+ email: 'GUEST_EMAIL', phone: '512345678',
313
+ });
314
+ console.log(booking.status, booking.confirmation, booking.total_price, booking.currency);
315
+ ```
316
+
317
+ ### MCP
318
+
319
+ Five new tools, in the order you call them: `resolve_hotel_city` →
320
+ `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
321
+