letsfg 2026.5.65 → 2026.5.67

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
@@ -9,8 +9,8 @@
9
9
 
10
10
  | | **CLI / SDK** (this package) | **Developer API** |
11
11
  |---|---|---|
12
- | **Search cost** | Free (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
13
- | **Booking** | `POST /api/agent-book` | Direct airline URL |
12
+ | **Search cost** | Free (Bearer token via `letsfg auth` — zero-amount card setup) | Prepaid credits |
13
+ | **Booking** | `POST /api/agent-book` — confirmed order or a booking link, no LetsFG fee | Direct airline URL (unlock required first) |
14
14
  | **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (full) |
15
15
  | **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
16
16
 
@@ -27,60 +27,60 @@ npm install letsfg
27
27
  ```typescript
28
28
  import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
29
29
 
30
- // Register (one-time)
31
- const creds = await LetsFG.register('my-agent', 'agent@example.com');
32
- console.log(creds.api_key); // Save this
33
-
34
- // Use
35
- const bt = new LetsFG({ apiKey: 'trav_...' });
30
+ // PFS — free. Get a Bearer token once with `letsfg auth` (zero-amount card
31
+ // setup, nothing charged), then pass it here.
32
+ const bt = new LetsFG({ bearerToken: 'eyJ...' });
36
33
 
37
34
  // Search — FREE
38
35
  const flights = await bt.search('GDN', 'BER', '2026-03-03');
39
36
  const best = cheapestOffer(flights);
40
37
  console.log(offerSummary(best));
41
38
 
42
- // Unlock
43
- const unlock = await bt.unlock(best.id);
44
-
45
- // Book
46
- const booking = await bt.book(
39
+ // Book — free, ticket price only, no LetsFG fee. No unlock step.
40
+ const result = await bt.book(
47
41
  best.id,
48
- [{
49
- id: flights.passenger_ids[0],
50
- given_name: 'John',
51
- family_name: 'Doe',
52
- born_on: '1990-01-15',
53
- gender: 'm',
54
- title: 'mr',
55
- email: 'john@example.com',
56
- }],
57
- 'john@example.com'
42
+ [{ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm' }],
43
+ 'john@example.com',
44
+ '',
45
+ '',
46
+ flights.search_id,
58
47
  );
59
- console.log(`PNR: ${booking.booking_reference}`);
48
+ if (result.booked) {
49
+ console.log(`Order: ${result.order_id}`);
50
+ } else {
51
+ console.log(`Booking link (nothing charged): ${result.booking_url}`);
52
+ }
60
53
  ```
61
54
 
55
+ Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
56
+ `search()`/`book()` dispatch automatically. That path requires `unlock()`
57
+ (1% fee, min $3) before `book()`.
58
+
62
59
  ## Quick Start (CLI)
63
60
 
64
61
  ```bash
65
- export LETSFG_BEARER_TOKEN=<your-bearer-token>
62
+ export LETSFG_BEARER_TOKEN=<your-bearer-token> # from `letsfg auth`
66
63
 
67
64
  letsfg search GDN BER 2026-03-03 --sort price
68
65
  letsfg search LON BCN 2026-04-01 --json # Machine-readable
69
- letsfg unlock off_xxx
70
- letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
66
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m"}' -e john@example.com
71
67
  ```
72
68
 
73
69
  ## API
74
70
 
75
- ### `new LetsFG({ apiKey, baseUrl?, timeout? })`
71
+ ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
76
72
 
77
73
  ### `bt.search(origin, destination, dateFrom, options?)`
78
74
  ### `bt.resolveLocation(query)`
79
- ### `bt.unlock(offerId)`
80
- ### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
81
- ### `bt.setupPayment(token?)`
75
+ ### `bt.unlock(offerId)` — Developer API only
76
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
77
+ Dispatches on which credential is set: `bearerToken` → free PFS booking via
78
+ `POST /api/agent-book` (pass `searchId`, one passenger). `apiKey` → paid
79
+ Developer API `book` (requires `unlock()` first, supports multiple passengers
80
+ and `idempotencyKey`).
81
+ ### `bt.setupPayment(token?)` — Developer API only
82
82
  ### `bt.me()`
83
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
83
+ ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
84
84
 
85
85
  ### Helpers
86
86
  - `offerSummary(offer)` — One-line string summary
@@ -150,6 +150,17 @@ else; after it, the hotel's own cancellation ladder applies and can reach 100%.
150
150
  That ladder ships in the booking's `terms`, so you can always see the cost before
151
151
  you cancel.
152
152
 
153
+ ### What search costs
154
+
155
+ Search is metered separately from booking, on **either** auth path (free PFS
156
+ Bearer token or Developer API key — both count against the same agent):
157
+ **the first 1,000 `search_hotels` calls since your last hotel booking are
158
+ free.** Past that, searches are billed in blocks of 1,000 for **$5**
159
+ (~$0.005/search) from your prepaid balance — refused with a 402 if the
160
+ balance can't cover the next block, never silently allowed. Book a hotel
161
+ and the count resets to zero. Resolving a city name (`hotel_destinations`)
162
+ is not metered, only the search call itself.
163
+
153
164
  ### Things worth knowing before you build
154
165
 
155
166
  - **A card on file is required for every hotel call, including search.** That is
@@ -0,0 +1,16 @@
1
+ import {
2
+ BearerTokenError,
3
+ getBearerToken,
4
+ paymentAuth,
5
+ requestEnrolment,
6
+ saveToken,
7
+ verifyPaymentMethod
8
+ } from "./chunk-XNO2W77S.mjs";
9
+ export {
10
+ BearerTokenError,
11
+ getBearerToken,
12
+ paymentAuth,
13
+ requestEnrolment,
14
+ saveToken,
15
+ verifyPaymentMethod
16
+ };
@@ -1629,12 +1629,12 @@ var LetsFG = class {
1629
1629
  }
1630
1630
  /**
1631
1631
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1632
- * Cost: 1% of ticket price, min $3. Free with Developer API.
1632
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1633
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1633
1634
  */
1634
1635
  async unlock(offerId) {
1635
- this.requireAuth();
1636
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
1637
- return this.postWithAuth(path, { offer_id: offerId });
1636
+ this.requireApiKey();
1637
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1638
1638
  }
1639
1639
  /**
1640
1640
  * Book a flight.
@@ -0,0 +1,126 @@
1
+ // src/auth.ts
2
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
3
+ import { homedir, platform } from "os";
4
+ import { join, dirname } from "path";
5
+ var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
6
+ var TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
7
+ var BearerTokenError = class extends Error {
8
+ };
9
+ function configPath() {
10
+ const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
11
+ return join(base, ".letsfg", "config.json");
12
+ }
13
+ function loadConfig() {
14
+ const p = configPath();
15
+ if (!existsSync(p)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(p, "utf-8"));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+ function saveConfig(cfg) {
23
+ const p = configPath();
24
+ mkdirSync(dirname(p), { recursive: true });
25
+ writeFileSync(p, JSON.stringify(cfg, null, 2));
26
+ try {
27
+ chmodSync(p, 384);
28
+ } catch {
29
+ }
30
+ }
31
+ function getBearerToken() {
32
+ const env = process.env.LETSFG_BEARER_TOKEN;
33
+ if (env) return env;
34
+ const auth = loadConfig().pfs_auth;
35
+ if (auth?.token && Date.now() < auth.expires_at - 36e5) {
36
+ return auth.token;
37
+ }
38
+ throw new BearerTokenError(
39
+ "No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
40
+ );
41
+ }
42
+ function saveToken(token, expiresAt) {
43
+ const cfg = loadConfig();
44
+ cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
45
+ saveConfig(cfg);
46
+ }
47
+ async function postJson(path, payload) {
48
+ const resp = await fetch(`${BASE_URL}${path}`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
51
+ body: JSON.stringify(payload)
52
+ });
53
+ const data = await resp.json().catch(() => ({}));
54
+ return { status: resp.status, data };
55
+ }
56
+ function parseExpiry(raw) {
57
+ if (typeof raw === "number") return raw * 1e3;
58
+ if (typeof raw === "string") {
59
+ const ms = Date.parse(raw);
60
+ if (!Number.isNaN(ms)) return ms;
61
+ }
62
+ return Date.now() + TOKEN_TTL_MS;
63
+ }
64
+ async function requestEnrolment() {
65
+ const { status, data } = await postJson("/api/agent-access/request", {});
66
+ if (status !== 200 && status !== 402) {
67
+ throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
68
+ }
69
+ return data;
70
+ }
71
+ async function verifyPaymentMethod(opts) {
72
+ const payload = {};
73
+ if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
74
+ else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
75
+ else if (opts.cardToken) payload.card_token = opts.cardToken;
76
+ else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
77
+ const { status, data } = await postJson("/api/agent-access/verify", payload);
78
+ if (status !== 200 || !data.token) {
79
+ throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
80
+ }
81
+ const token = data.token;
82
+ saveToken(token, parseExpiry(data.expires_at));
83
+ return token;
84
+ }
85
+ function openBrowser(url) {
86
+ import("child_process").then(({ exec }) => {
87
+ const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
88
+ exec(cmd, () => {
89
+ });
90
+ }).catch(() => {
91
+ });
92
+ }
93
+ async function paymentAuth(openBrowserFlag = true) {
94
+ console.log("\n Connecting to LetsFG...");
95
+ const data = await requestEnrolment();
96
+ const setupUrl = data.setup_url;
97
+ const sessionId = data.setup_session_id;
98
+ if (!setupUrl || !sessionId) {
99
+ throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
100
+ }
101
+ console.log("\n LetsFG needs a payment method on file before it can search or book.");
102
+ console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
103
+ console.log(" Step 1 \u2014 add a card here:\n");
104
+ console.log(` ${setupUrl}
105
+ `);
106
+ if (openBrowserFlag) openBrowser(setupUrl);
107
+ console.log(" Step 2 \u2014 press Enter once you've finished.");
108
+ const readline = await import("readline/promises");
109
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
110
+ await rl.question("");
111
+ rl.close();
112
+ process.stdout.write(" Verifying... ");
113
+ const token = await verifyPaymentMethod({ setupSessionId: sessionId });
114
+ const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
115
+ console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
116
+ return token;
117
+ }
118
+
119
+ export {
120
+ BearerTokenError,
121
+ getBearerToken,
122
+ saveToken,
123
+ requestEnrolment,
124
+ verifyPaymentMethod,
125
+ paymentAuth
126
+ };
package/dist/cli.js CHANGED
@@ -1,5 +1,167 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/auth.ts
34
+ var auth_exports = {};
35
+ __export(auth_exports, {
36
+ BearerTokenError: () => BearerTokenError,
37
+ getBearerToken: () => getBearerToken,
38
+ paymentAuth: () => paymentAuth,
39
+ requestEnrolment: () => requestEnrolment,
40
+ saveToken: () => saveToken,
41
+ verifyPaymentMethod: () => verifyPaymentMethod
42
+ });
43
+ function configPath() {
44
+ const base = (0, import_node_os.platform)() === "win32" ? process.env.APPDATA || (0, import_node_os.homedir)() : (0, import_node_os.homedir)();
45
+ return (0, import_node_path.join)(base, ".letsfg", "config.json");
46
+ }
47
+ function loadConfig() {
48
+ const p = configPath();
49
+ if (!(0, import_node_fs.existsSync)(p)) return {};
50
+ try {
51
+ return JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+ function saveConfig(cfg) {
57
+ const p = configPath();
58
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(p), { recursive: true });
59
+ (0, import_node_fs.writeFileSync)(p, JSON.stringify(cfg, null, 2));
60
+ try {
61
+ (0, import_node_fs.chmodSync)(p, 384);
62
+ } catch {
63
+ }
64
+ }
65
+ function getBearerToken() {
66
+ const env = process.env.LETSFG_BEARER_TOKEN;
67
+ if (env) return env;
68
+ const auth = loadConfig().pfs_auth;
69
+ if (auth?.token && Date.now() < auth.expires_at - 36e5) {
70
+ return auth.token;
71
+ }
72
+ throw new BearerTokenError(
73
+ "No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
74
+ );
75
+ }
76
+ function saveToken(token, expiresAt) {
77
+ const cfg = loadConfig();
78
+ cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
79
+ saveConfig(cfg);
80
+ }
81
+ async function postJson(path, payload) {
82
+ const resp = await fetch(`${BASE_URL}${path}`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
85
+ body: JSON.stringify(payload)
86
+ });
87
+ const data = await resp.json().catch(() => ({}));
88
+ return { status: resp.status, data };
89
+ }
90
+ function parseExpiry(raw) {
91
+ if (typeof raw === "number") return raw * 1e3;
92
+ if (typeof raw === "string") {
93
+ const ms = Date.parse(raw);
94
+ if (!Number.isNaN(ms)) return ms;
95
+ }
96
+ return Date.now() + TOKEN_TTL_MS;
97
+ }
98
+ async function requestEnrolment() {
99
+ const { status, data } = await postJson("/api/agent-access/request", {});
100
+ if (status !== 200 && status !== 402) {
101
+ throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
102
+ }
103
+ return data;
104
+ }
105
+ async function verifyPaymentMethod(opts) {
106
+ const payload = {};
107
+ if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
108
+ else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
109
+ else if (opts.cardToken) payload.card_token = opts.cardToken;
110
+ else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
111
+ const { status, data } = await postJson("/api/agent-access/verify", payload);
112
+ if (status !== 200 || !data.token) {
113
+ throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
114
+ }
115
+ const token = data.token;
116
+ saveToken(token, parseExpiry(data.expires_at));
117
+ return token;
118
+ }
119
+ function openBrowser(url) {
120
+ import("child_process").then(({ exec }) => {
121
+ const cmd = (0, import_node_os.platform)() === "win32" ? `start "" "${url}"` : (0, import_node_os.platform)() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
122
+ exec(cmd, () => {
123
+ });
124
+ }).catch(() => {
125
+ });
126
+ }
127
+ async function paymentAuth(openBrowserFlag = true) {
128
+ console.log("\n Connecting to LetsFG...");
129
+ const data = await requestEnrolment();
130
+ const setupUrl = data.setup_url;
131
+ const sessionId = data.setup_session_id;
132
+ if (!setupUrl || !sessionId) {
133
+ throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
134
+ }
135
+ console.log("\n LetsFG needs a payment method on file before it can search or book.");
136
+ console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
137
+ console.log(" Step 1 \u2014 add a card here:\n");
138
+ console.log(` ${setupUrl}
139
+ `);
140
+ if (openBrowserFlag) openBrowser(setupUrl);
141
+ console.log(" Step 2 \u2014 press Enter once you've finished.");
142
+ const readline = await import("readline/promises");
143
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
144
+ await rl.question("");
145
+ rl.close();
146
+ process.stdout.write(" Verifying... ");
147
+ const token = await verifyPaymentMethod({ setupSessionId: sessionId });
148
+ const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
149
+ console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
150
+ return token;
151
+ }
152
+ var import_node_fs, import_node_os, import_node_path, BASE_URL, TOKEN_TTL_MS, BearerTokenError;
153
+ var init_auth = __esm({
154
+ "src/auth.ts"() {
155
+ "use strict";
156
+ import_node_fs = require("fs");
157
+ import_node_os = require("os");
158
+ import_node_path = require("path");
159
+ BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
160
+ TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
161
+ BearerTokenError = class extends Error {
162
+ };
163
+ }
164
+ });
3
165
 
4
166
  // src/ranking.ts
5
167
  var W = {
@@ -384,12 +546,12 @@ var LetsFG = class {
384
546
  }
385
547
  /**
386
548
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
387
- * Cost: 1% of ticket price, min $3. Free with Developer API.
549
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
550
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
388
551
  */
389
552
  async unlock(offerId) {
390
- this.requireAuth();
391
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
392
- return this.postWithAuth(path, { offer_id: offerId });
553
+ this.requireApiKey();
554
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
393
555
  }
394
556
  /**
395
557
  * Book a flight.
@@ -684,6 +846,16 @@ var LetsFG = class {
684
846
  };
685
847
 
686
848
  // src/cli.ts
849
+ init_auth();
850
+ function resolveCredentials(apiKeyFlag) {
851
+ const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
852
+ if (apiKey) return { apiKey };
853
+ try {
854
+ return { bearerToken: getBearerToken() };
855
+ } catch {
856
+ return {};
857
+ }
858
+ }
687
859
  function getFlag(args, flag, alias) {
688
860
  for (let i = 0; i < args.length; i++) {
689
861
  if (args[i] === flag || alias && args[i] === alias) {
@@ -738,7 +910,8 @@ async function cmdSearch(args) {
738
910
  console.error("Usage: letsfg search <origin> <destination> <date> [options]");
739
911
  process.exit(1);
740
912
  }
741
- const bt = new LetsFG({ apiKey, baseUrl });
913
+ const creds = resolveCredentials(apiKey);
914
+ const bt = new LetsFG({ ...creds, baseUrl });
742
915
  const result = await bt.search(origin, destination, date, {
743
916
  returnDate,
744
917
  adults,
@@ -752,6 +925,7 @@ async function cmdSearch(args) {
752
925
  });
753
926
  if (jsonOut) {
754
927
  console.log(JSON.stringify({
928
+ search_id: result.search_id,
755
929
  passenger_ids: result.passenger_ids,
756
930
  total_results: result.total_results,
757
931
  offers: result.offers.map((o) => ({
@@ -775,16 +949,25 @@ async function cmdSearch(args) {
775
949
  }
776
950
  console.log(`
777
951
  ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
952
+ if (result.search_id) {
953
+ console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
954
+ }
778
955
  console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
779
956
  `);
780
957
  result.offers.forEach((o, i) => {
781
958
  console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
782
959
  console.log(` ID: ${o.id}`);
783
960
  });
784
- console.log(`
961
+ if (creds.bearerToken) {
962
+ console.log(`
963
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
964
+ `);
965
+ } else {
966
+ console.log(`
785
967
  To unlock: letsfg unlock <offer_id>`);
786
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
968
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
787
969
  `);
970
+ }
788
971
  }
789
972
  async function cmdUnlock(args) {
790
973
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -818,33 +1001,55 @@ async function cmdBook(args) {
818
1001
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
819
1002
  const apiKey = getFlag(args, "--api-key", "-k");
820
1003
  const baseUrl = getFlag(args, "--base-url");
1004
+ const searchId = getFlag(args, "--search-id");
821
1005
  const email = getFlag(args, "--email", "-e") || "";
822
1006
  const phone = getFlag(args, "--phone") || "";
823
1007
  const passengerStrs = getAllFlags(args, "--passenger", "-p");
824
1008
  const offerId = args[0];
825
1009
  if (!offerId || !passengerStrs.length || !email) {
826
- console.error(`Usage: letsfg book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
1010
+ console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
827
1011
  process.exit(1);
828
1012
  }
829
1013
  const passengers = passengerStrs.map((s) => JSON.parse(s));
830
- const bt = new LetsFG({ apiKey, baseUrl });
831
- const result = await bt.book(offerId, passengers, email, phone);
1014
+ const creds = resolveCredentials(apiKey);
1015
+ if (creds.bearerToken && !searchId) {
1016
+ console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
1017
+ process.exit(1);
1018
+ }
1019
+ const bt = new LetsFG({ ...creds, baseUrl });
1020
+ const result = await bt.book(offerId, passengers, email, phone, "", searchId);
832
1021
  if (jsonOut) {
833
1022
  console.log(JSON.stringify(result, null, 2));
834
1023
  return;
835
1024
  }
836
- if (result.status === "confirmed") {
1025
+ if ("booked" in result) {
1026
+ if (result.booked) {
1027
+ console.log(`
1028
+ \u2713 Booking confirmed!`);
1029
+ console.log(` Order ID: ${result.order_id}`);
1030
+ console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
1031
+ `);
1032
+ } else {
1033
+ console.log(`
1034
+ Could not complete a confirmed booking. Nothing was charged.`);
1035
+ console.log(` Booking link: ${result.booking_url ?? "(none)"}
1036
+ `);
1037
+ }
1038
+ return;
1039
+ }
1040
+ const br = result;
1041
+ if (br.status === "confirmed") {
837
1042
  console.log(`
838
1043
  \u2713 Booking confirmed!`);
839
- console.log(` PNR: ${result.booking_reference}`);
840
- console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
841
- console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
842
- console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
843
- console.log(` Order: ${result.order_id}
1044
+ console.log(` PNR: ${br.booking_reference}`);
1045
+ console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
1046
+ console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
1047
+ console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
1048
+ console.log(` Order: ${br.order_id}
844
1049
  `);
845
1050
  } else {
846
1051
  console.error(` \u2717 Booking failed`);
847
- console.error(JSON.stringify(result.details, null, 2));
1052
+ console.error(JSON.stringify(br.details, null, 2));
848
1053
  process.exit(1);
849
1054
  }
850
1055
  }
@@ -876,6 +1081,19 @@ async function cmdLocations(args) {
876
1081
  console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
877
1082
  }
878
1083
  }
1084
+ async function cmdAuth(args) {
1085
+ const cardToken = getFlag(args, "--card-token");
1086
+ const paymentMethodId = getFlag(args, "--payment-method");
1087
+ const noBrowser = hasFlag(args, "--no-browser");
1088
+ if (cardToken || paymentMethodId) {
1089
+ const { verifyPaymentMethod: verifyPaymentMethod2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1090
+ await verifyPaymentMethod2({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
1091
+ console.log("\n \u2713 Authenticated. Nothing was charged.");
1092
+ } else {
1093
+ await paymentAuth(!noBrowser);
1094
+ }
1095
+ console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
1096
+ }
879
1097
  async function cmdRegister(args) {
880
1098
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
881
1099
  const baseUrl = getFlag(args, "--base-url");
@@ -956,11 +1174,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
956
1174
  charged \u2014 then search and book.
957
1175
 
958
1176
  Commands:
959
- auth Put a card on file -> 90-day token. Nothing charged
960
- search <origin> <dest> <date> Search for flights (free)
961
- locations <query> Resolve city name to IATA codes
962
- book <offer_id> --passenger ... Book a flight. No LetsFG fee
963
- me Show agent profile
1177
+ auth Put a card on file -> 90-day token. Nothing charged
1178
+ search <origin> <dest> <date> Search for flights (free), prints search_id
1179
+ locations <query> Resolve city name to IATA codes
1180
+ book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
1181
+ me Show agent profile
964
1182
 
965
1183
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
966
1184
  they create a billing account. Use auth above instead):
@@ -969,20 +1187,26 @@ they create a billing account. Use auth above instead):
969
1187
  unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
970
1188
 
971
1189
  Options:
972
- --json, -j Output raw JSON
973
- --api-key, -k API key (or set LETSFG_API_KEY)
974
- --base-url API URL (default: https://letsfg.co/developers)
1190
+ --json, -j Output raw JSON
1191
+ --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
1192
+ --base-url API URL (default: https://letsfg.co)
1193
+ --card-token (auth only) Stripe tok_... you already hold, for a headless auth
1194
+ --payment-method (auth only) Stripe pm_... you already hold, for a headless auth
1195
+ --no-browser (auth only) Don't try to auto-open the card setup page
975
1196
 
976
1197
  Examples:
977
1198
  letsfg auth
978
1199
  letsfg search GDN BER 2026-03-03 --sort price
979
- letsfg book off_xxx -p '{"given_name":"Ada",...}' -e ada@example.com
1200
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
980
1201
  `;
981
1202
  async function main() {
982
1203
  const args = process.argv.slice(2);
983
1204
  const command = args.shift();
984
1205
  try {
985
1206
  switch (command) {
1207
+ case "auth":
1208
+ await cmdAuth(args);
1209
+ break;
986
1210
  case "search":
987
1211
  await cmdSearch(args);
988
1212
  break;
@@ -1016,7 +1240,7 @@ async function main() {
1016
1240
  process.exit(1);
1017
1241
  }
1018
1242
  } catch (e) {
1019
- if (e instanceof LetsFGError) {
1243
+ if (e instanceof LetsFGError || e instanceof BearerTokenError) {
1020
1244
  console.error(`Error: ${e.message}`);
1021
1245
  process.exit(1);
1022
1246
  }
package/dist/cli.mjs CHANGED
@@ -3,9 +3,23 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-DEFFV47W.mjs";
6
+ } from "./chunk-2SNO3AGS.mjs";
7
+ import {
8
+ BearerTokenError,
9
+ getBearerToken,
10
+ paymentAuth
11
+ } from "./chunk-XNO2W77S.mjs";
7
12
 
8
13
  // src/cli.ts
14
+ function resolveCredentials(apiKeyFlag) {
15
+ const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
16
+ if (apiKey) return { apiKey };
17
+ try {
18
+ return { bearerToken: getBearerToken() };
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
9
23
  function getFlag(args, flag, alias) {
10
24
  for (let i = 0; i < args.length; i++) {
11
25
  if (args[i] === flag || alias && args[i] === alias) {
@@ -60,7 +74,8 @@ async function cmdSearch(args) {
60
74
  console.error("Usage: letsfg search <origin> <destination> <date> [options]");
61
75
  process.exit(1);
62
76
  }
63
- const bt = new LetsFG({ apiKey, baseUrl });
77
+ const creds = resolveCredentials(apiKey);
78
+ const bt = new LetsFG({ ...creds, baseUrl });
64
79
  const result = await bt.search(origin, destination, date, {
65
80
  returnDate,
66
81
  adults,
@@ -74,6 +89,7 @@ async function cmdSearch(args) {
74
89
  });
75
90
  if (jsonOut) {
76
91
  console.log(JSON.stringify({
92
+ search_id: result.search_id,
77
93
  passenger_ids: result.passenger_ids,
78
94
  total_results: result.total_results,
79
95
  offers: result.offers.map((o) => ({
@@ -97,16 +113,25 @@ async function cmdSearch(args) {
97
113
  }
98
114
  console.log(`
99
115
  ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
116
+ if (result.search_id) {
117
+ console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
118
+ }
100
119
  console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
101
120
  `);
102
121
  result.offers.forEach((o, i) => {
103
122
  console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
104
123
  console.log(` ID: ${o.id}`);
105
124
  });
106
- console.log(`
125
+ if (creds.bearerToken) {
126
+ console.log(`
127
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
128
+ `);
129
+ } else {
130
+ console.log(`
107
131
  To unlock: letsfg unlock <offer_id>`);
108
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
132
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
109
133
  `);
134
+ }
110
135
  }
111
136
  async function cmdUnlock(args) {
112
137
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -140,33 +165,55 @@ async function cmdBook(args) {
140
165
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
141
166
  const apiKey = getFlag(args, "--api-key", "-k");
142
167
  const baseUrl = getFlag(args, "--base-url");
168
+ const searchId = getFlag(args, "--search-id");
143
169
  const email = getFlag(args, "--email", "-e") || "";
144
170
  const phone = getFlag(args, "--phone") || "";
145
171
  const passengerStrs = getAllFlags(args, "--passenger", "-p");
146
172
  const offerId = args[0];
147
173
  if (!offerId || !passengerStrs.length || !email) {
148
- console.error(`Usage: letsfg book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
174
+ console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
149
175
  process.exit(1);
150
176
  }
151
177
  const passengers = passengerStrs.map((s) => JSON.parse(s));
152
- const bt = new LetsFG({ apiKey, baseUrl });
153
- const result = await bt.book(offerId, passengers, email, phone);
178
+ const creds = resolveCredentials(apiKey);
179
+ if (creds.bearerToken && !searchId) {
180
+ console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
181
+ process.exit(1);
182
+ }
183
+ const bt = new LetsFG({ ...creds, baseUrl });
184
+ const result = await bt.book(offerId, passengers, email, phone, "", searchId);
154
185
  if (jsonOut) {
155
186
  console.log(JSON.stringify(result, null, 2));
156
187
  return;
157
188
  }
158
- if (result.status === "confirmed") {
189
+ if ("booked" in result) {
190
+ if (result.booked) {
191
+ console.log(`
192
+ \u2713 Booking confirmed!`);
193
+ console.log(` Order ID: ${result.order_id}`);
194
+ console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
195
+ `);
196
+ } else {
197
+ console.log(`
198
+ Could not complete a confirmed booking. Nothing was charged.`);
199
+ console.log(` Booking link: ${result.booking_url ?? "(none)"}
200
+ `);
201
+ }
202
+ return;
203
+ }
204
+ const br = result;
205
+ if (br.status === "confirmed") {
159
206
  console.log(`
160
207
  \u2713 Booking confirmed!`);
161
- console.log(` PNR: ${result.booking_reference}`);
162
- console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
163
- console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
164
- console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
165
- console.log(` Order: ${result.order_id}
208
+ console.log(` PNR: ${br.booking_reference}`);
209
+ console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
210
+ console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
211
+ console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
212
+ console.log(` Order: ${br.order_id}
166
213
  `);
167
214
  } else {
168
215
  console.error(` \u2717 Booking failed`);
169
- console.error(JSON.stringify(result.details, null, 2));
216
+ console.error(JSON.stringify(br.details, null, 2));
170
217
  process.exit(1);
171
218
  }
172
219
  }
@@ -198,6 +245,19 @@ async function cmdLocations(args) {
198
245
  console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
199
246
  }
200
247
  }
248
+ 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);
258
+ }
259
+ console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
260
+ }
201
261
  async function cmdRegister(args) {
202
262
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
203
263
  const baseUrl = getFlag(args, "--base-url");
@@ -278,11 +338,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
278
338
  charged \u2014 then search and book.
279
339
 
280
340
  Commands:
281
- auth Put a card on file -> 90-day token. Nothing charged
282
- search <origin> <dest> <date> Search for flights (free)
283
- locations <query> Resolve city name to IATA codes
284
- book <offer_id> --passenger ... Book a flight. No LetsFG fee
285
- me Show agent profile
341
+ auth Put a card on file -> 90-day token. Nothing charged
342
+ search <origin> <dest> <date> Search for flights (free), prints search_id
343
+ locations <query> Resolve city name to IATA codes
344
+ book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
345
+ me Show agent profile
286
346
 
287
347
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
288
348
  they create a billing account. Use auth above instead):
@@ -291,20 +351,26 @@ they create a billing account. Use auth above instead):
291
351
  unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
292
352
 
293
353
  Options:
294
- --json, -j Output raw JSON
295
- --api-key, -k API key (or set LETSFG_API_KEY)
296
- --base-url API URL (default: https://letsfg.co/developers)
354
+ --json, -j Output raw JSON
355
+ --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
356
+ --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
297
360
 
298
361
  Examples:
299
362
  letsfg auth
300
363
  letsfg search GDN BER 2026-03-03 --sort price
301
- letsfg book off_xxx -p '{"given_name":"Ada",...}' -e ada@example.com
364
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
302
365
  `;
303
366
  async function main() {
304
367
  const args = process.argv.slice(2);
305
368
  const command = args.shift();
306
369
  try {
307
370
  switch (command) {
371
+ case "auth":
372
+ await cmdAuth(args);
373
+ break;
308
374
  case "search":
309
375
  await cmdSearch(args);
310
376
  break;
@@ -338,7 +404,7 @@ async function main() {
338
404
  process.exit(1);
339
405
  }
340
406
  } catch (e) {
341
- if (e instanceof LetsFGError) {
407
+ if (e instanceof LetsFGError || e instanceof BearerTokenError) {
342
408
  console.error(`Error: ${e.message}`);
343
409
  process.exit(1);
344
410
  }
package/dist/index.d.mts CHANGED
@@ -472,7 +472,8 @@ declare class LetsFG {
472
472
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
473
473
  /**
474
474
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
475
- * Cost: 1% of ticket price, min $3. Free with Developer API.
475
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
476
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
476
477
  */
477
478
  unlock(offerId: string): Promise<UnlockResult>;
478
479
  /**
package/dist/index.d.ts CHANGED
@@ -472,7 +472,8 @@ declare class LetsFG {
472
472
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
473
473
  /**
474
474
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
475
- * Cost: 1% of ticket price, min $3. Free with Developer API.
475
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
476
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
476
477
  */
477
478
  unlock(offerId: string): Promise<UnlockResult>;
478
479
  /**
package/dist/index.js CHANGED
@@ -1677,12 +1677,12 @@ var LetsFG = class {
1677
1677
  }
1678
1678
  /**
1679
1679
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1680
- * Cost: 1% of ticket price, min $3. Free with Developer API.
1680
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1681
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1681
1682
  */
1682
1683
  async unlock(offerId) {
1683
- this.requireAuth();
1684
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
1685
- return this.postWithAuth(path, { offer_id: offerId });
1684
+ this.requireApiKey();
1685
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1686
1686
  }
1687
1687
  /**
1688
1688
  * Book a flight.
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-DEFFV47W.mjs";
25
+ } from "./chunk-2SNO3AGS.mjs";
26
26
  export {
27
27
  AuthenticationError,
28
28
  BoostedTravel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letsfg",
3
- "version": "2026.5.65",
3
+ "version": "2026.5.67",
4
4
  "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
- "test": "tsx --test src/index.test.ts",
18
+ "test": "tsx --test src/index.test.ts src/auth.test.ts",
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
21
  "keywords": [