letsfg 2026.5.71 → 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/dist/cli.mjs CHANGED
@@ -3,12 +3,213 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-GO3FXQXC.mjs";
7
- import {
8
- BearerTokenError,
9
- getBearerToken,
10
- paymentAuth
11
- } from "./chunk-XNO2W77S.mjs";
6
+ } from "./chunk-F5BBI6XX.mjs";
7
+
8
+ // src/auth.ts
9
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
10
+ import { homedir, platform } from "os";
11
+ import { join, dirname } from "path";
12
+ import { createHash, randomBytes } from "crypto";
13
+ import { createServer } from "http";
14
+ var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
15
+ var DEV_ROOT = `${BASE_URL}/developers/api`;
16
+ var FALLBACK_TTL_MS = 55 * 60 * 1e3;
17
+ var REFRESH_SKEW_MS = 5 * 60 * 1e3;
18
+ var BearerTokenError = class extends Error {
19
+ };
20
+ function configPath() {
21
+ const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
22
+ return join(base, ".letsfg", "config.json");
23
+ }
24
+ function loadConfig() {
25
+ const p = configPath();
26
+ if (!existsSync(p)) return {};
27
+ try {
28
+ return JSON.parse(readFileSync(p, "utf-8"));
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+ function saveConfig(cfg) {
34
+ const p = configPath();
35
+ mkdirSync(dirname(p), { recursive: true });
36
+ writeFileSync(p, JSON.stringify(cfg, null, 2));
37
+ try {
38
+ chmodSync(p, 384);
39
+ } catch {
40
+ }
41
+ }
42
+ function saveToken(token, expiresAt, extra) {
43
+ const cfg = loadConfig();
44
+ cfg.pfs_auth = {
45
+ ...cfg.pfs_auth ?? {},
46
+ ...extra ?? {},
47
+ token,
48
+ expires_at: expiresAt ?? Date.now() + FALLBACK_TTL_MS
49
+ };
50
+ saveConfig(cfg);
51
+ }
52
+ function getBearerToken() {
53
+ const env = process.env.LETSFG_BEARER_TOKEN;
54
+ if (env) return env;
55
+ const auth = loadConfig().pfs_auth;
56
+ if (auth?.token && Date.now() < auth.expires_at - REFRESH_SKEW_MS) {
57
+ return auth.token;
58
+ }
59
+ if (auth?.refresh_token) {
60
+ throw new BearerTokenError(
61
+ "LetsFG token expired. Call ensureBearerToken() to refresh it, or run: letsfg auth"
62
+ );
63
+ }
64
+ throw new BearerTokenError(
65
+ "No valid LetsFG Bearer token.\n Run: letsfg auth (connects a card at letsfg.co/connect - nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
66
+ );
67
+ }
68
+ async function discover() {
69
+ const fallback = {
70
+ authorization_endpoint: `${BASE_URL}/connect`,
71
+ token_endpoint: `${DEV_ROOT}/oauth/token`,
72
+ registration_endpoint: `${DEV_ROOT}/oauth/register`
73
+ };
74
+ try {
75
+ const resp = await fetch(`${DEV_ROOT}/.well-known/oauth-authorization-server`);
76
+ if (!resp.ok) return fallback;
77
+ const d = await resp.json();
78
+ return {
79
+ authorization_endpoint: d.authorization_endpoint || fallback.authorization_endpoint,
80
+ token_endpoint: d.token_endpoint || fallback.token_endpoint,
81
+ registration_endpoint: d.registration_endpoint || fallback.registration_endpoint
82
+ };
83
+ } catch {
84
+ return fallback;
85
+ }
86
+ }
87
+ function pkce() {
88
+ const verifier = randomBytes(32).toString("base64url");
89
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
90
+ return { verifier, challenge };
91
+ }
92
+ async function postForm(url, form) {
93
+ const resp = await fetch(url, {
94
+ method: "POST",
95
+ // RFC 6749 4.1.3 - the token endpoint takes form encoding. The server also
96
+ // accepts JSON, but sending the spec encoding keeps this honest.
97
+ headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Client-Type": "js-sdk" },
98
+ body: new URLSearchParams(form).toString()
99
+ });
100
+ const data = await resp.json().catch(() => ({}));
101
+ return { status: resp.status, data };
102
+ }
103
+ function openBrowser(url) {
104
+ import("child_process").then(({ exec }) => {
105
+ const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
106
+ exec(cmd, () => {
107
+ });
108
+ }).catch(() => {
109
+ });
110
+ }
111
+ function startCallbackServer(expectedState) {
112
+ return new Promise((resolve, reject) => {
113
+ let settle = null;
114
+ let fail = null;
115
+ const codePromise = new Promise((res, rej) => {
116
+ settle = res;
117
+ fail = rej;
118
+ });
119
+ const server = createServer((req, res) => {
120
+ const url = new URL(req.url || "/", "http://127.0.0.1");
121
+ if (!url.pathname.startsWith("/callback")) {
122
+ res.writeHead(404).end();
123
+ return;
124
+ }
125
+ const err = url.searchParams.get("error");
126
+ const code = url.searchParams.get("code");
127
+ const state = url.searchParams.get("state");
128
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
129
+ if (err || !code) {
130
+ res.end("<h1>LetsFG</h1><p>That did not complete. Nothing was charged - run <code>letsfg auth</code> again.</p>");
131
+ fail?.(new BearerTokenError(`Authorisation failed: ${err || "no code returned"}`));
132
+ return;
133
+ }
134
+ if (state !== expectedState) {
135
+ res.end("<h1>LetsFG</h1><p>State mismatch - nothing was charged. Run <code>letsfg auth</code> again.</p>");
136
+ fail?.(new BearerTokenError("State mismatch on the OAuth callback - aborted."));
137
+ return;
138
+ }
139
+ res.end("<h1>LetsFG</h1><p>Connected. You can close this tab and go back to your terminal.</p>");
140
+ settle?.(code);
141
+ });
142
+ server.on("error", reject);
143
+ server.listen(0, "127.0.0.1", () => {
144
+ const addr = server.address();
145
+ const port = typeof addr === "object" && addr ? addr.port : 0;
146
+ if (!port) {
147
+ reject(new BearerTokenError("Could not open a local callback port."));
148
+ return;
149
+ }
150
+ resolve({ port, waitForCode: () => codePromise, close: () => server.close() });
151
+ });
152
+ });
153
+ }
154
+ async function connectAuth(openBrowserFlag = true) {
155
+ const meta = await discover();
156
+ const { verifier, challenge } = pkce();
157
+ const state = randomBytes(16).toString("base64url");
158
+ const { port, waitForCode, close } = await startCallbackServer(state);
159
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
160
+ try {
161
+ const reg = await fetch(meta.registration_endpoint, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({
165
+ client_name: "letsfg-cli",
166
+ redirect_uris: [redirectUri],
167
+ grant_types: ["authorization_code", "refresh_token"],
168
+ response_types: ["code"],
169
+ token_endpoint_auth_method: "none"
170
+ })
171
+ });
172
+ const regData = await reg.json().catch(() => ({}));
173
+ const clientId = typeof regData.client_id === "string" ? regData.client_id : "";
174
+ if (!reg.ok || !clientId) {
175
+ throw new BearerTokenError(
176
+ `Could not register with LetsFG (HTTP ${reg.status}). See ${BASE_URL}/for-agents`
177
+ );
178
+ }
179
+ const authUrl = `${meta.authorization_endpoint}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&state=${encodeURIComponent(state)}&scope=flights`;
180
+ console.log("\n LetsFG needs a card connected before it can search or book.");
181
+ console.log(" Nothing is charged now - you pay the fare only when you book,");
182
+ console.log(" and it is held, not taken, until the airline confirms.\n");
183
+ console.log(" Open this and add a card (or pay 0.00 with Revolut Pay):\n");
184
+ console.log(` ${authUrl}
185
+ `);
186
+ if (openBrowserFlag) openBrowser(authUrl);
187
+ process.stdout.write(" Waiting for you to finish... ");
188
+ const code = await waitForCode();
189
+ const { status, data } = await postForm(meta.token_endpoint, {
190
+ grant_type: "authorization_code",
191
+ code,
192
+ client_id: clientId,
193
+ redirect_uri: redirectUri,
194
+ code_verifier: verifier
195
+ });
196
+ if (status !== 200 || !data.access_token) {
197
+ throw new BearerTokenError(
198
+ `Could not complete authentication (HTTP ${status}). ${String(data.error_description || data.error || "")}`.trim()
199
+ );
200
+ }
201
+ const expiresIn = typeof data.expires_in === "number" ? data.expires_in * 1e3 : FALLBACK_TTL_MS;
202
+ saveToken(String(data.access_token), Date.now() + expiresIn, {
203
+ refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
204
+ client_id: clientId
205
+ });
206
+ console.log("done. Card connected - the token refreshes itself from now on.");
207
+ return String(data.access_token);
208
+ } finally {
209
+ close();
210
+ }
211
+ }
212
+ var paymentAuth = connectAuth;
12
213
 
13
214
  // src/cli.ts
14
215
  function resolveCredentials(apiKeyFlag) {
@@ -128,38 +329,15 @@ async function cmdSearch(args) {
128
329
  `);
129
330
  } else {
130
331
  console.log(`
131
- To unlock: letsfg unlock <offer_id>`);
132
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
332
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
133
333
  `);
134
334
  }
135
335
  }
136
- async function cmdUnlock(args) {
137
- const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
138
- const apiKey = getFlag(args, "--api-key", "-k");
139
- const baseUrl = getFlag(args, "--base-url");
140
- const offerId = args[0];
141
- if (!offerId) {
142
- console.error("Usage: letsfg unlock <offer_id>");
143
- process.exit(1);
144
- }
145
- const bt = new LetsFG({ apiKey, baseUrl });
146
- const result = await bt.unlock(offerId);
147
- if (jsonOut) {
148
- console.log(JSON.stringify(result, null, 2));
149
- return;
150
- }
151
- if (result.unlock_status === "unlocked") {
152
- console.log(`
153
- \u2713 Offer unlocked!`);
154
- console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
155
- console.log(` Expires at: ${result.offer_expires_at}`);
156
- console.log(`
157
- Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
158
- `);
159
- } else {
160
- console.error(` \u2717 Unlock failed: ${result.message}`);
161
- process.exit(1);
162
- }
336
+ async function cmdUnlock(_args) {
337
+ console.error(
338
+ "\n letsfg unlock was retired on 2026-09-08 and the endpoint answers 410 Gone.\n\n There is no unlock step any more. Book directly:\n letsfg book <offer_id> --search-id <search_id> --passenger '{...}' --email you@example.com\n\n The fare is HELD on your connected payment method and captured only once a real\n airline PNR exists, which is what unlock existed to protect against. If the fare moves\n at checkout you are asked to accept or decline it.\n"
339
+ );
340
+ process.exit(1);
163
341
  }
164
342
  async function cmdBook(args) {
165
343
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -246,17 +424,19 @@ async function cmdLocations(args) {
246
424
  }
247
425
  }
248
426
  async function cmdAuth(args) {
249
- const cardToken = getFlag(args, "--card-token");
250
- const paymentMethodId = getFlag(args, "--payment-method");
251
- const noBrowser = hasFlag(args, "--no-browser");
252
- if (cardToken || paymentMethodId) {
253
- const { verifyPaymentMethod } = await import("./auth-SYZ5AYZ6.mjs");
254
- await verifyPaymentMethod({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
255
- console.log("\n \u2713 Authenticated. Nothing was charged.");
256
- } else {
257
- await paymentAuth(!noBrowser);
427
+ for (const dead of ["--card-token", "--payment-method"]) {
428
+ if (getFlag(args, dead)) {
429
+ console.error(
430
+ `
431
+ ${dead} was part of the Stripe enrolment, retired 2026-09-02.
432
+ Run plain \`letsfg auth\` instead - it opens letsfg.co/connect to add a card.
433
+ `
434
+ );
435
+ process.exit(1);
436
+ }
258
437
  }
259
- console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
438
+ await paymentAuth(!hasFlag(args, "--no-browser"));
439
+ console.log("\n You are all set. Run: letsfg search WAW BCN 2026-07-15\n");
260
440
  }
261
441
  async function cmdRegister(args) {
262
442
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -289,19 +469,26 @@ async function cmdSetupPayment(args) {
289
469
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
290
470
  const apiKey = getFlag(args, "--api-key", "-k");
291
471
  const baseUrl = getFlag(args, "--base-url");
292
- const token = getFlag(args, "--token", "-t") || "tok_visa";
472
+ if (getFlag(args, "--token", "-t")) {
473
+ console.log("\n Note: --token was part of the Stripe enrolment, retired 2026-09-08. Ignoring it.");
474
+ }
293
475
  const bt = new LetsFG({ apiKey, baseUrl });
294
- const result = await bt.setupPayment(token);
476
+ const result = await bt.connectPayment();
295
477
  if (jsonOut) {
296
478
  console.log(JSON.stringify(result, null, 2));
297
479
  return;
298
480
  }
299
- if (result.status === "ready") {
481
+ const url = result.connect_url;
482
+ if (url) {
300
483
  console.log(`
301
- \u2713 Payment ready! You can now unlock offers and book flights.
484
+ Open this in a browser to connect a payment method:
485
+ `);
486
+ console.log(` ${url}
487
+ `);
488
+ console.log(` Nothing is charged. Run \`letsfg me\` afterwards to confirm it landed.
302
489
  `);
303
490
  } else {
304
- console.error(` \u2717 Payment setup failed: ${result.message || result.status}`);
491
+ console.error(` \u2717 Could not mint a connect link: ${result.message || result.status}`);
305
492
  process.exit(1);
306
493
  }
307
494
  }
@@ -322,10 +509,9 @@ async function cmdMe(args) {
322
509
  console.log(` Email: ${p.email}`);
323
510
  console.log(` Tier: ${p.tier}`);
324
511
  const access = p.access_granted || false;
325
- console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted"}`);
512
+ console.log(` Access: ${access ? "\u2713 Granted (search, book)" : "\u2717 Not granted"}`);
326
513
  console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
327
514
  console.log(` Searches: ${u.total_searches || 0}`);
328
- console.log(` Unlocks: ${u.total_unlocks || 0}`);
329
515
  console.log(` Bookings: ${u.total_bookings || 0}`);
330
516
  console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
331
517
  `);
@@ -334,29 +520,29 @@ var HELP = `
334
520
  LetsFG \u2014 Agent-native flight search & booking.
335
521
 
336
522
  Search hundreds of airlines via the LetsFG cloud engine.
337
- Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
338
- charged \u2014 then search and book.
523
+ Authenticate once with letsfg auth \u2014 it opens letsfg.co/connect so you can add
524
+ a card. Nothing is charged; you pay the fare only when you book, and it is held,
525
+ not taken, until the airline confirms.
339
526
 
340
527
  Commands:
341
- auth Put a card on file -> 90-day token. Nothing charged
528
+ auth Connect a card at letsfg.co/connect. Nothing charged
342
529
  search <origin> <dest> <date> Search for flights (free), prints search_id
343
530
  locations <query> Resolve city name to IATA codes
344
- book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
531
+ book <offer_id> --search-id ... Book a flight. No booking or transaction fee, no unlock step
345
532
  me Show agent profile
346
533
 
347
534
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
348
535
  they create a billing account. Use auth above instead):
349
536
  register --name ... --email ... Create a paid Developer API account
350
- setup-payment Attach a card to that paid account
351
- unlock <offer_id> [Developer API only] Unlock offer (legacy)
537
+ connect-payment Print a link to connect a card to that paid account
538
+ setup-payment Alias of connect-payment (the Stripe lane retired 2026-09-08)
539
+ unlock <offer_id> RETIRED 2026-09-08 \u2014 no unlock step, book directly
352
540
 
353
541
  Options:
354
542
  --json, -j Output raw JSON
355
543
  --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
356
544
  --base-url API URL (default: https://letsfg.co)
357
- --card-token (auth only) Stripe tok_... you already hold, for a headless auth
358
- --payment-method (auth only) Stripe pm_... you already hold, for a headless auth
359
- --no-browser (auth only) Don't try to auto-open the card setup page
545
+ --no-browser (auth only) Print the connect URL instead of opening a browser
360
546
 
361
547
  Examples:
362
548
  letsfg auth
@@ -386,6 +572,7 @@ async function main() {
386
572
  case "register":
387
573
  await cmdRegister(args);
388
574
  break;
575
+ case "connect-payment":
389
576
  case "setup-payment":
390
577
  await cmdSetupPayment(args);
391
578
  break;
package/dist/index.d.mts CHANGED
@@ -282,7 +282,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
282
282
  * const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN });
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
- * // Developer API (prepaid credits)
285
+ * // Developer API (look-to-book search: 200 free after every booking)
286
286
  * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
287
287
  * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
288
288
  * ```
@@ -402,7 +402,7 @@ interface SearchOptions {
402
402
  interface LetsFGConfig {
403
403
  /** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */
404
404
  bearerToken?: string;
405
- /** Developer API key (prepaid credits, no per-booking fee). */
405
+ /** Developer API key. Look-to-book search; no booking fee, no transaction fee. */
406
406
  apiKey?: string;
407
407
  baseUrl?: string;
408
408
  timeout?: number;
@@ -495,11 +495,19 @@ declare class LetsFG {
495
495
  */
496
496
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
497
497
  /**
498
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
499
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
500
- * token, so PFS callers use book() directly.
498
+ * RETIRED 2026-09-08. Throws instead of calling the server.
499
+ *
500
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
501
+ * before charging; booking now HOLDS the fare on the connected payment method
502
+ * and captures only once a real airline PNR exists, so a fare that moved
503
+ * cannot become a charge for a ticket you did not get. If it moves at
504
+ * checkout you get a `price_change` question to accept or decline instead.
505
+ *
506
+ * Kept as a method, and throwing locally rather than making the request, so an
507
+ * older caller gets one clear sentence at the line that is actually wrong —
508
+ * not a 410 body to decode, and not a TypeError somewhere else.
501
509
  */
502
- unlock(offerId: string): Promise<UnlockResult>;
510
+ unlock(_offerId: string): Promise<UnlockResult>;
503
511
  /**
504
512
  * Book a flight.
505
513
  *
@@ -509,11 +517,64 @@ declare class LetsFG {
509
517
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
510
518
  * nothing was charged.
511
519
  *
512
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
513
- * creates a real PNR. Requires unlock() first. Always provide
514
- * idempotencyKey to prevent double-bookings on retry.
520
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
521
+ * REQUIRED — an offer is bookable only inside the search that produced it.
522
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
523
+ * buys the ticket and the hold is captured only against a real airline PNR.
524
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
525
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
526
+ * Always provide idempotencyKey: a retry with the same key returns the
527
+ * existing booking instead of opening a second hold on the card.
515
528
  */
516
529
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
530
+ /**
531
+ * Poll a Developer API flight booking.
532
+ *
533
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
534
+ * knows you are still there, which is what keeps a booking paused on a
535
+ * question alive — so do not back off to minutes.
536
+ *
537
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
538
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
539
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
540
+ * book again).
541
+ */
542
+ getBooking(bookingId: string): Promise<Record<string, unknown>>;
543
+ /**
544
+ * Answer the open `question` on a booking.
545
+ *
546
+ * Echo the question's `round`. A stale round is refused with 409 rather than
547
+ * guessed at, so an answer to an old question can never be applied to a new
548
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
549
+ * { confirm: true } or { skip: true } — declining an extra still completes
550
+ * the booking, without it.
551
+ */
552
+ answerBooking(bookingId: string, round: number, answer?: {
553
+ seats?: Array<Record<string, unknown>>;
554
+ confirm?: boolean;
555
+ skip?: boolean;
556
+ }): Promise<Record<string, unknown>>;
557
+ /**
558
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
559
+ *
560
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
561
+ * book() + getBooking() instead if your caller has a request timeout.
562
+ *
563
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
564
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
565
+ * reading of "the traveller asked for this flight".
566
+ */
567
+ bookAndWait(offerId: string, passengers: Passenger[], contactEmail: string, searchId: string, opts?: {
568
+ contactPhone?: string;
569
+ idempotencyKey?: string;
570
+ pollMs?: number;
571
+ timeoutMs?: number;
572
+ onQuestion?: (q: Record<string, unknown>) => {
573
+ seats?: Array<Record<string, unknown>>;
574
+ confirm?: boolean;
575
+ skip?: boolean;
576
+ };
577
+ }): Promise<Record<string, unknown>>;
517
578
  /**
518
579
  * Resolve a place name to the city id that searchHotels() needs.
519
580
  *
@@ -621,13 +682,30 @@ declare class LetsFG {
621
682
  */
622
683
  cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
623
684
  /**
624
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
685
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
686
+ *
687
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
688
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
689
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
690
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
691
+ * user for a card number and do not try to automate this step.
692
+ *
693
+ * Most agents should NOT need a Developer API account at all. To authenticate for
694
+ * search and booking, run `letsfg auth`, which creates no billing account.
695
+ */
696
+ connectPayment(): Promise<Record<string, unknown>>;
697
+ /**
698
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
699
+ *
700
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
701
+ * Revolut rail as the rest of the product: call connectPayment() and open the
702
+ * `connect_url` it returns.
625
703
  *
626
- * Most agents should NOT call this. It is unrelated to authenticating for
627
- * search and booking — for that, run `letsfg auth`, which puts a card on file
628
- * through a zero-amount setup and creates no billing account.
704
+ * Kept as a method, and throwing locally rather than making the request, for the
705
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
706
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
629
707
  */
630
- setupPayment(token?: string): Promise<Record<string, unknown>>;
708
+ setupPayment(_token?: string): Promise<Record<string, unknown>>;
631
709
  /**
632
710
  * Get current agent profile and usage stats.
633
711
  */