letsfg 2026.5.70 → 2026.5.72

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.js CHANGED
@@ -6,13 +6,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
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
9
  var __copyProps = (to, from, except, desc) => {
17
10
  if (from && typeof from === "object" || typeof from === "function") {
18
11
  for (let key of __getOwnPropNames(from))
@@ -30,139 +23,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
23
  mod
31
24
  ));
32
25
 
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
- });
165
-
166
26
  // src/ranking.ts
167
27
  var W = {
168
28
  // Generic solo / default — price-driven
@@ -454,8 +314,12 @@ function offerSummary(offer) {
454
314
  return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
455
315
  }
456
316
  var DEFAULT_BASE_URL = "https://letsfg.co";
457
- var PFS_POLL_INTERVAL_MS = 1e4;
317
+ var PFS_POLL_INTERVAL_MS = 2e3;
458
318
  var PFS_POLL_TIMEOUT_MS = 12e4;
319
+ var LATE_MERGE_POLL_MS = 3e3;
320
+ var LATE_MERGE_GRACE_MS = 9e4;
321
+ var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
322
+ var NON_TERMINAL = ["pending", "searching"];
459
323
  var LetsFG = class {
460
324
  bearerToken;
461
325
  apiKey;
@@ -491,7 +355,7 @@ var LetsFG = class {
491
355
  *
492
356
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
493
357
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
494
- * Developer API: synchronous 60-90s call.
358
+ * Developer API: synchronous call.
495
359
  *
496
360
  * @param origin - IATA code (e.g., "GDN", "LON")
497
361
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -523,17 +387,28 @@ var LetsFG = class {
523
387
  /** PFS path: POST /api/search -> poll /api/results/<id> */
524
388
  async searchPFS(body) {
525
389
  const { search_id } = await this.postWithBearer("/api/search", body);
390
+ const poll = () => this.getNoAuth(`/api/results/${search_id}`);
391
+ const inbound = (r) => Boolean(r.split_ticket_pending || r.gf_enrich_pending);
526
392
  const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
393
+ let terminal = null;
527
394
  while (Date.now() < deadline) {
528
- await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
529
- const result = await this.getNoAuth(
530
- `/api/results/${search_id}`
531
- );
532
- if (!["pending", "searching"].includes(result.status)) {
533
- return result;
395
+ const result = await poll();
396
+ if (!NON_TERMINAL.includes(result.status)) {
397
+ terminal = result;
398
+ break;
534
399
  }
400
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
401
+ }
402
+ if (!terminal) {
403
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
404
+ }
405
+ const lateDeadline = Date.now() + LATE_MERGE_GRACE_MS;
406
+ while (WAIT_FOR_SPLIT && inbound(terminal) && Date.now() < lateDeadline) {
407
+ await new Promise((r) => setTimeout(r, LATE_MERGE_POLL_MS));
408
+ const merged = await poll();
409
+ if (!NON_TERMINAL.includes(merged.status)) terminal = merged;
535
410
  }
536
- throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
411
+ return terminal;
537
412
  }
538
413
  /**
539
414
  * Resolve a city/airport name to IATA codes.
@@ -553,8 +428,8 @@ var LetsFG = class {
553
428
  }
554
429
  /**
555
430
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
556
- * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
557
- * endpoint on a PFS Bearer token, so PFS callers use book() directly.
431
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
432
+ * token, so PFS callers use book() directly.
558
433
  */
559
434
  async unlock(offerId) {
560
435
  this.requireApiKey();
@@ -852,8 +727,213 @@ var LetsFG = class {
852
727
  }
853
728
  };
854
729
 
730
+ // src/auth.ts
731
+ var import_node_fs = require("fs");
732
+ var import_node_os = require("os");
733
+ var import_node_path = require("path");
734
+ var import_node_crypto = require("crypto");
735
+ var import_node_http = require("http");
736
+ var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
737
+ var DEV_ROOT = `${BASE_URL}/developers/api`;
738
+ var FALLBACK_TTL_MS = 55 * 60 * 1e3;
739
+ var REFRESH_SKEW_MS = 5 * 60 * 1e3;
740
+ var BearerTokenError = class extends Error {
741
+ };
742
+ function configPath() {
743
+ const base = (0, import_node_os.platform)() === "win32" ? process.env.APPDATA || (0, import_node_os.homedir)() : (0, import_node_os.homedir)();
744
+ return (0, import_node_path.join)(base, ".letsfg", "config.json");
745
+ }
746
+ function loadConfig() {
747
+ const p = configPath();
748
+ if (!(0, import_node_fs.existsSync)(p)) return {};
749
+ try {
750
+ return JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
751
+ } catch {
752
+ return {};
753
+ }
754
+ }
755
+ function saveConfig(cfg) {
756
+ const p = configPath();
757
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(p), { recursive: true });
758
+ (0, import_node_fs.writeFileSync)(p, JSON.stringify(cfg, null, 2));
759
+ try {
760
+ (0, import_node_fs.chmodSync)(p, 384);
761
+ } catch {
762
+ }
763
+ }
764
+ function saveToken(token, expiresAt, extra) {
765
+ const cfg = loadConfig();
766
+ cfg.pfs_auth = {
767
+ ...cfg.pfs_auth ?? {},
768
+ ...extra ?? {},
769
+ token,
770
+ expires_at: expiresAt ?? Date.now() + FALLBACK_TTL_MS
771
+ };
772
+ saveConfig(cfg);
773
+ }
774
+ function getBearerToken() {
775
+ const env = process.env.LETSFG_BEARER_TOKEN;
776
+ if (env) return env;
777
+ const auth = loadConfig().pfs_auth;
778
+ if (auth?.token && Date.now() < auth.expires_at - REFRESH_SKEW_MS) {
779
+ return auth.token;
780
+ }
781
+ if (auth?.refresh_token) {
782
+ throw new BearerTokenError(
783
+ "LetsFG token expired. Call ensureBearerToken() to refresh it, or run: letsfg auth"
784
+ );
785
+ }
786
+ throw new BearerTokenError(
787
+ "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>"
788
+ );
789
+ }
790
+ async function discover() {
791
+ const fallback = {
792
+ authorization_endpoint: `${BASE_URL}/connect`,
793
+ token_endpoint: `${DEV_ROOT}/oauth/token`,
794
+ registration_endpoint: `${DEV_ROOT}/oauth/register`
795
+ };
796
+ try {
797
+ const resp = await fetch(`${DEV_ROOT}/.well-known/oauth-authorization-server`);
798
+ if (!resp.ok) return fallback;
799
+ const d = await resp.json();
800
+ return {
801
+ authorization_endpoint: d.authorization_endpoint || fallback.authorization_endpoint,
802
+ token_endpoint: d.token_endpoint || fallback.token_endpoint,
803
+ registration_endpoint: d.registration_endpoint || fallback.registration_endpoint
804
+ };
805
+ } catch {
806
+ return fallback;
807
+ }
808
+ }
809
+ function pkce() {
810
+ const verifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
811
+ const challenge = (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
812
+ return { verifier, challenge };
813
+ }
814
+ async function postForm(url, form) {
815
+ const resp = await fetch(url, {
816
+ method: "POST",
817
+ // RFC 6749 4.1.3 - the token endpoint takes form encoding. The server also
818
+ // accepts JSON, but sending the spec encoding keeps this honest.
819
+ headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Client-Type": "js-sdk" },
820
+ body: new URLSearchParams(form).toString()
821
+ });
822
+ const data = await resp.json().catch(() => ({}));
823
+ return { status: resp.status, data };
824
+ }
825
+ function openBrowser(url) {
826
+ import("child_process").then(({ exec }) => {
827
+ const cmd = (0, import_node_os.platform)() === "win32" ? `start "" "${url}"` : (0, import_node_os.platform)() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
828
+ exec(cmd, () => {
829
+ });
830
+ }).catch(() => {
831
+ });
832
+ }
833
+ function startCallbackServer(expectedState) {
834
+ return new Promise((resolve, reject) => {
835
+ let settle = null;
836
+ let fail = null;
837
+ const codePromise = new Promise((res, rej) => {
838
+ settle = res;
839
+ fail = rej;
840
+ });
841
+ const server = (0, import_node_http.createServer)((req, res) => {
842
+ const url = new URL(req.url || "/", "http://127.0.0.1");
843
+ if (!url.pathname.startsWith("/callback")) {
844
+ res.writeHead(404).end();
845
+ return;
846
+ }
847
+ const err = url.searchParams.get("error");
848
+ const code = url.searchParams.get("code");
849
+ const state = url.searchParams.get("state");
850
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
851
+ if (err || !code) {
852
+ res.end("<h1>LetsFG</h1><p>That did not complete. Nothing was charged - run <code>letsfg auth</code> again.</p>");
853
+ fail?.(new BearerTokenError(`Authorisation failed: ${err || "no code returned"}`));
854
+ return;
855
+ }
856
+ if (state !== expectedState) {
857
+ res.end("<h1>LetsFG</h1><p>State mismatch - nothing was charged. Run <code>letsfg auth</code> again.</p>");
858
+ fail?.(new BearerTokenError("State mismatch on the OAuth callback - aborted."));
859
+ return;
860
+ }
861
+ res.end("<h1>LetsFG</h1><p>Connected. You can close this tab and go back to your terminal.</p>");
862
+ settle?.(code);
863
+ });
864
+ server.on("error", reject);
865
+ server.listen(0, "127.0.0.1", () => {
866
+ const addr = server.address();
867
+ const port = typeof addr === "object" && addr ? addr.port : 0;
868
+ if (!port) {
869
+ reject(new BearerTokenError("Could not open a local callback port."));
870
+ return;
871
+ }
872
+ resolve({ port, waitForCode: () => codePromise, close: () => server.close() });
873
+ });
874
+ });
875
+ }
876
+ async function connectAuth(openBrowserFlag = true) {
877
+ const meta = await discover();
878
+ const { verifier, challenge } = pkce();
879
+ const state = (0, import_node_crypto.randomBytes)(16).toString("base64url");
880
+ const { port, waitForCode, close } = await startCallbackServer(state);
881
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
882
+ try {
883
+ const reg = await fetch(meta.registration_endpoint, {
884
+ method: "POST",
885
+ headers: { "Content-Type": "application/json" },
886
+ body: JSON.stringify({
887
+ client_name: "letsfg-cli",
888
+ redirect_uris: [redirectUri],
889
+ grant_types: ["authorization_code", "refresh_token"],
890
+ response_types: ["code"],
891
+ token_endpoint_auth_method: "none"
892
+ })
893
+ });
894
+ const regData = await reg.json().catch(() => ({}));
895
+ const clientId = typeof regData.client_id === "string" ? regData.client_id : "";
896
+ if (!reg.ok || !clientId) {
897
+ throw new BearerTokenError(
898
+ `Could not register with LetsFG (HTTP ${reg.status}). See ${BASE_URL}/for-agents`
899
+ );
900
+ }
901
+ 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`;
902
+ console.log("\n LetsFG needs a card connected before it can search or book.");
903
+ console.log(" Nothing is charged now - you pay the fare only when you book,");
904
+ console.log(" and it is held, not taken, until the airline confirms.\n");
905
+ console.log(" Open this and add a card (or pay 0.00 with Revolut Pay):\n");
906
+ console.log(` ${authUrl}
907
+ `);
908
+ if (openBrowserFlag) openBrowser(authUrl);
909
+ process.stdout.write(" Waiting for you to finish... ");
910
+ const code = await waitForCode();
911
+ const { status, data } = await postForm(meta.token_endpoint, {
912
+ grant_type: "authorization_code",
913
+ code,
914
+ client_id: clientId,
915
+ redirect_uri: redirectUri,
916
+ code_verifier: verifier
917
+ });
918
+ if (status !== 200 || !data.access_token) {
919
+ throw new BearerTokenError(
920
+ `Could not complete authentication (HTTP ${status}). ${String(data.error_description || data.error || "")}`.trim()
921
+ );
922
+ }
923
+ const expiresIn = typeof data.expires_in === "number" ? data.expires_in * 1e3 : FALLBACK_TTL_MS;
924
+ saveToken(String(data.access_token), Date.now() + expiresIn, {
925
+ refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
926
+ client_id: clientId
927
+ });
928
+ console.log("done. Card connected - the token refreshes itself from now on.");
929
+ return String(data.access_token);
930
+ } finally {
931
+ close();
932
+ }
933
+ }
934
+ var paymentAuth = connectAuth;
935
+
855
936
  // src/cli.ts
856
- init_auth();
857
937
  function resolveCredentials(apiKeyFlag) {
858
938
  const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
859
939
  if (apiKey) return { apiKey };
@@ -1089,17 +1169,19 @@ async function cmdLocations(args) {
1089
1169
  }
1090
1170
  }
1091
1171
  async function cmdAuth(args) {
1092
- const cardToken = getFlag(args, "--card-token");
1093
- const paymentMethodId = getFlag(args, "--payment-method");
1094
- const noBrowser = hasFlag(args, "--no-browser");
1095
- if (cardToken || paymentMethodId) {
1096
- const { verifyPaymentMethod: verifyPaymentMethod2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1097
- await verifyPaymentMethod2({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
1098
- console.log("\n \u2713 Authenticated. Nothing was charged.");
1099
- } else {
1100
- await paymentAuth(!noBrowser);
1172
+ for (const dead of ["--card-token", "--payment-method"]) {
1173
+ if (getFlag(args, dead)) {
1174
+ console.error(
1175
+ `
1176
+ ${dead} was part of the Stripe enrolment, retired 2026-09-02.
1177
+ Run plain \`letsfg auth\` instead - it opens letsfg.co/connect to add a card.
1178
+ `
1179
+ );
1180
+ process.exit(1);
1181
+ }
1101
1182
  }
1102
- console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
1183
+ await paymentAuth(!hasFlag(args, "--no-browser"));
1184
+ console.log("\n You are all set. Run: letsfg search WAW BCN 2026-07-15\n");
1103
1185
  }
1104
1186
  async function cmdRegister(args) {
1105
1187
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -1177,11 +1259,12 @@ var HELP = `
1177
1259
  LetsFG \u2014 Agent-native flight search & booking.
1178
1260
 
1179
1261
  Search hundreds of airlines via the LetsFG cloud engine.
1180
- Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
1181
- charged \u2014 then search and book.
1262
+ Authenticate once with letsfg auth \u2014 it opens letsfg.co/connect so you can add
1263
+ a card. Nothing is charged; you pay the fare only when you book, and it is held,
1264
+ not taken, until the airline confirms.
1182
1265
 
1183
1266
  Commands:
1184
- auth Put a card on file -> 90-day token. Nothing charged
1267
+ auth Connect a card at letsfg.co/connect. Nothing charged
1185
1268
  search <origin> <dest> <date> Search for flights (free), prints search_id
1186
1269
  locations <query> Resolve city name to IATA codes
1187
1270
  book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
@@ -1191,15 +1274,13 @@ Developer API only (a SEPARATE paid product \u2014 most agents should not use th
1191
1274
  they create a billing account. Use auth above instead):
1192
1275
  register --name ... --email ... Create a paid Developer API account
1193
1276
  setup-payment Attach a card to that paid account
1194
- unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
1277
+ unlock <offer_id> [Developer API only] Unlock offer (legacy)
1195
1278
 
1196
1279
  Options:
1197
1280
  --json, -j Output raw JSON
1198
1281
  --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
1199
1282
  --base-url API URL (default: https://letsfg.co)
1200
- --card-token (auth only) Stripe tok_... you already hold, for a headless auth
1201
- --payment-method (auth only) Stripe pm_... you already hold, for a headless auth
1202
- --no-browser (auth only) Don't try to auto-open the card setup page
1283
+ --no-browser (auth only) Print the connect URL instead of opening a browser
1203
1284
 
1204
1285
  Examples:
1205
1286
  letsfg auth