letsfg 2026.5.72 → 2026.5.73

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,281 +1,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
- > **Want direct airline URLs without any per-booking fee?** Use the [Developer API](https://letsfg.co/developers) — prepaid credits, results in seconds, no checkout step.
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 — ticket price only, no LetsFG fee, 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 requires `unlock()`
115
- (1% fee, min $3) before `book()`.
116
-
117
- ## Quick Start (CLI)
118
-
119
- ```bash
120
- export LETSFG_BEARER_TOKEN=<your-bearer-token> # card-backed, from the connect flow
121
-
122
- letsfg search GDN BER 2026-03-03 --sort price
123
- letsfg search LON BCN 2026-04-01 --json # Machine-readable
124
- 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
125
- # prints the booking_ref — poll POST /api/agent-book/status until completed
126
- ```
127
-
128
- ## API
129
-
130
- ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
131
-
132
- ### `bt.search(origin, destination, dateFrom, options?)`
133
- ### `bt.resolveLocation(query)`
134
- ### `bt.unlock(offerId)` — Developer API only
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 `unlock()` first, supports multiple passengers
140
- and `idempotencyKey`).
141
- ### `bt.setupPayment(token?)` — Developer API only
142
- ### `bt.me()`
143
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
144
-
145
- ### Helpers
146
- - `offerSummary(offer)` — One-line string summary
147
- - `cheapestOffer(result)` — Get cheapest offer from search
148
-
149
- ## Starlink Wi-Fi
150
-
151
- Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
152
- has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
153
- rollout on that type is underway but incomplete. Segments carry `confirmed` or
154
- `likely`.
155
-
156
- Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
157
- promise. Anything ending `_some` has at least one leg without it. An **absent**
158
- field means no information, **not** an absence of Wi-Fi.
159
-
160
- Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
161
-
162
- ## Zero Dependencies
163
-
164
- Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
165
-
166
- ## Also Available As
167
-
168
- - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
169
- - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
170
- - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
171
- - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
172
-
173
- > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
174
-
175
- ## License
176
-
177
- MIT
178
-
179
- ## 🏨 Hotels — new, and live
180
-
181
- Your agent can book hotels as well as flights. Same card-backed token or API key, same card on file.
182
-
183
- ```python
184
- from letsfg import LetsFG
185
- lfg = LetsFG()
186
-
187
- city = lfg.hotel_destinations("Warsaw")[0]
188
- stays = lfg.search_hotels(
189
- city_id=city["Id"], city_name=city["Name"],
190
- check_in="2026-11-10", check_out="2026-11-12", adults=2,
191
- )
192
-
193
- hotel = stays["hotels"][0]
194
- offer = hotel["offers"][0]
195
- print(hotel["name"], offer["price"], stays["currency"])
196
- # Hotel Gromada Warszawa Centrum 669.86 PLN
197
-
198
- booking = lfg.book_hotel_and_wait(
199
- session_id=stays["session_id"],
200
- hotel_code=hotel["hotel_code"],
201
- combination_id_v2=offer["combination_id_v2"],
202
- expected_price=offer["price"],
203
- expected_balance=offer["balance_to_supplier"],
204
- city_id=city["Id"], city_name=city["Name"],
205
- check_in="2026-11-10", check_out="2026-11-12",
206
- guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
207
- email="guest@example.com", phone="512345678",
208
- )
209
- print(booking["confirmation"], booking["pay_link"])
210
- ```
211
-
212
- ### How you pay
213
-
214
- **5% now, the rest to the hotel later.** At booking we charge 5% of the price
215
- to your card as a reservation fee. The remaining balance is paid **directly to
216
- the supplier** through a `pay_link` we return — we never hold it.
217
-
218
- `balance_due_by` is the supplier's own auto-cancellation date, not a date we
219
- invent. Miss it and the room is released.
220
-
221
- The 5% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
222
- else; after it, the hotel's own cancellation ladder applies and can reach 100%.
223
- That ladder ships in the booking's `terms`, so you can always see the cost before
224
- you cancel.
225
-
226
- ### What search costs
227
-
228
- Search is metered separately from booking, on **either** auth path (free PFS
229
- Bearer token or Developer API key — both count against the same agent):
230
- **the first 1,000 `search_hotels` calls since your last hotel booking are
231
- free.** Past that, searches are billed in blocks of 1,000 for **$5**
232
- (~$0.005/search) from your prepaid balance — refused with a 402 if the
233
- balance can't cover the next block, never silently allowed. Book a hotel
234
- and the count resets to zero. Resolving a city name (`hotel_destinations`)
235
- is not metered, only the search call itself.
236
-
237
- ### Things worth knowing before you build
238
-
239
- - **A card on file is required for every hotel call, including search.** That is
240
- unusual and it is deliberate: a hotel search opens a real session at the
241
- supplier, and booking blocks a real rate. We would rather refuse up front than
242
- let you reach the point of commitment and discover you cannot pay. The same
243
- card that authorises flight booking authorises hotels — there is no separate
244
- hotel signup.
245
- - **Only free-cancellation, pay-later rates are sold.** Those are the rates where
246
- the balance can safely be settled with the supplier after booking, which is
247
- what makes 5%-now/rest-later work at all. You will see fewer results than a
248
- metasearch shows you. Every one of them can actually be booked.
249
- - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
250
- booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
251
- `status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
252
- SDK do it. This is not ceremony: it is what makes it impossible to charge a
253
- card and then lose the confirmation to a timeout.
254
- - **The fee is charged before the room is committed.** A declined card therefore
255
- costs nothing to unwind — no reservation exists and nothing is charged.
256
- - **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
257
- rate books the room twice and charges two reservation fees.
258
- - `price` is what the guest pays. There is no wholesale figure in the response to
259
- quote by mistake.
260
-
261
- ### JavaScript
262
-
263
- ```javascript
264
- import { LetsFG } from 'letsfg';
265
- const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
266
-
267
- const [city] = await lfg.hotelDestinations('Warsaw');
268
- const stays = await lfg.searchHotels({
269
- cityId: city.Id, cityName: city.Name,
270
- checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
271
- });
272
-
273
- const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
274
- console.log(booking.confirmation, booking.pay_link);
275
- ```
276
-
277
- ### MCP
278
-
279
- Five new tools, in the order you call them: `resolve_hotel_city` →
280
- `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
281
-
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
+