letsfg 2026.5.69 → 2026.5.71

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
@@ -11,7 +11,7 @@
11
11
  |---|---|---|
12
12
  | **Search cost** | Free (Bearer token via `letsfg auth` — zero-amount card setup) | Prepaid credits |
13
13
  | **Booking** | `POST /api/agent-book` — confirmed order or a booking link, no LetsFG fee | Direct airline URL (unlock required first) |
14
- | **Speed** | 6090 s | 2–5 s (discover) · 6090 s (full) |
14
+ | **Speed** | 810 s to first results; longer on a split | 2–5 s (discover) · 810 s to first results (full) |
15
15
  | **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
16
16
 
17
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.
@@ -1558,8 +1558,12 @@ function cheapestOffer(result) {
1558
1558
  return result.offers.reduce((min, o) => o.price < min.price ? o : min, result.offers[0]);
1559
1559
  }
1560
1560
  var DEFAULT_BASE_URL = "https://letsfg.co";
1561
- var PFS_POLL_INTERVAL_MS = 1e4;
1561
+ var PFS_POLL_INTERVAL_MS = 2e3;
1562
1562
  var PFS_POLL_TIMEOUT_MS = 12e4;
1563
+ var LATE_MERGE_POLL_MS = 3e3;
1564
+ var LATE_MERGE_GRACE_MS = 9e4;
1565
+ var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
1566
+ var NON_TERMINAL = ["pending", "searching"];
1563
1567
  var LetsFG = class {
1564
1568
  bearerToken;
1565
1569
  apiKey;
@@ -1595,7 +1599,7 @@ var LetsFG = class {
1595
1599
  *
1596
1600
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
1597
1601
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
1598
- * Developer API: synchronous 60-90s call.
1602
+ * Developer API: synchronous call.
1599
1603
  *
1600
1604
  * @param origin - IATA code (e.g., "GDN", "LON")
1601
1605
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -1627,31 +1631,49 @@ var LetsFG = class {
1627
1631
  /** PFS path: POST /api/search -> poll /api/results/<id> */
1628
1632
  async searchPFS(body) {
1629
1633
  const { search_id } = await this.postWithBearer("/api/search", body);
1634
+ const poll = () => this.getNoAuth(`/api/results/${search_id}`);
1635
+ const inbound = (r) => Boolean(r.split_ticket_pending || r.gf_enrich_pending);
1630
1636
  const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
1637
+ let terminal = null;
1631
1638
  while (Date.now() < deadline) {
1632
- await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
1633
- const result = await this.getNoAuth(
1634
- `/api/results/${search_id}`
1635
- );
1636
- if (!["pending", "searching"].includes(result.status)) {
1637
- return result;
1639
+ const result = await poll();
1640
+ if (!NON_TERMINAL.includes(result.status)) {
1641
+ terminal = result;
1642
+ break;
1638
1643
  }
1644
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
1645
+ }
1646
+ if (!terminal) {
1647
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
1639
1648
  }
1640
- throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
1649
+ const lateDeadline = Date.now() + LATE_MERGE_GRACE_MS;
1650
+ while (WAIT_FOR_SPLIT && inbound(terminal) && Date.now() < lateDeadline) {
1651
+ await new Promise((r) => setTimeout(r, LATE_MERGE_POLL_MS));
1652
+ const merged = await poll();
1653
+ if (!NON_TERMINAL.includes(merged.status)) terminal = merged;
1654
+ }
1655
+ return terminal;
1641
1656
  }
1642
1657
  /**
1643
1658
  * Resolve a city/airport name to IATA codes.
1659
+ *
1660
+ * Developer API key only. There is no location endpoint on the PFS Bearer
1661
+ * lane — the same dead end `unlock()` documents below. This used to send
1662
+ * PFS callers to `/api/locations?q=`, a route that has never existed on
1663
+ * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse
1664
+ * error off the 404 page instead of an answer. Pass an IATA code directly
1665
+ * on the PFS lane; a city code expands to every airport in that city.
1644
1666
  */
1645
1667
  async resolveLocation(query) {
1646
- this.requireAuth();
1647
- const path = this.usingPFS ? `/api/locations?q=${encodeURIComponent(query)}` : `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
1668
+ this.requireApiKey();
1669
+ const path = `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
1648
1670
  const data = await this.getWithAuth(path);
1649
1671
  return Array.isArray(data) ? data : data.locations || [];
1650
1672
  }
1651
1673
  /**
1652
1674
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1653
- * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1654
- * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1675
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
1676
+ * token, so PFS callers use book() directly.
1655
1677
  */
1656
1678
  async unlock(offerId) {
1657
1679
  this.requireApiKey();
package/dist/cli.js CHANGED
@@ -454,8 +454,12 @@ function offerSummary(offer) {
454
454
  return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
455
455
  }
456
456
  var DEFAULT_BASE_URL = "https://letsfg.co";
457
- var PFS_POLL_INTERVAL_MS = 1e4;
457
+ var PFS_POLL_INTERVAL_MS = 2e3;
458
458
  var PFS_POLL_TIMEOUT_MS = 12e4;
459
+ var LATE_MERGE_POLL_MS = 3e3;
460
+ var LATE_MERGE_GRACE_MS = 9e4;
461
+ var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
462
+ var NON_TERMINAL = ["pending", "searching"];
459
463
  var LetsFG = class {
460
464
  bearerToken;
461
465
  apiKey;
@@ -491,7 +495,7 @@ var LetsFG = class {
491
495
  *
492
496
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
493
497
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
494
- * Developer API: synchronous 60-90s call.
498
+ * Developer API: synchronous call.
495
499
  *
496
500
  * @param origin - IATA code (e.g., "GDN", "LON")
497
501
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -523,31 +527,49 @@ var LetsFG = class {
523
527
  /** PFS path: POST /api/search -> poll /api/results/<id> */
524
528
  async searchPFS(body) {
525
529
  const { search_id } = await this.postWithBearer("/api/search", body);
530
+ const poll = () => this.getNoAuth(`/api/results/${search_id}`);
531
+ const inbound = (r) => Boolean(r.split_ticket_pending || r.gf_enrich_pending);
526
532
  const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
533
+ let terminal = null;
527
534
  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;
535
+ const result = await poll();
536
+ if (!NON_TERMINAL.includes(result.status)) {
537
+ terminal = result;
538
+ break;
534
539
  }
540
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
541
+ }
542
+ if (!terminal) {
543
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
535
544
  }
536
- throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
545
+ const lateDeadline = Date.now() + LATE_MERGE_GRACE_MS;
546
+ while (WAIT_FOR_SPLIT && inbound(terminal) && Date.now() < lateDeadline) {
547
+ await new Promise((r) => setTimeout(r, LATE_MERGE_POLL_MS));
548
+ const merged = await poll();
549
+ if (!NON_TERMINAL.includes(merged.status)) terminal = merged;
550
+ }
551
+ return terminal;
537
552
  }
538
553
  /**
539
554
  * Resolve a city/airport name to IATA codes.
555
+ *
556
+ * Developer API key only. There is no location endpoint on the PFS Bearer
557
+ * lane — the same dead end `unlock()` documents below. This used to send
558
+ * PFS callers to `/api/locations?q=`, a route that has never existed on
559
+ * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse
560
+ * error off the 404 page instead of an answer. Pass an IATA code directly
561
+ * on the PFS lane; a city code expands to every airport in that city.
540
562
  */
541
563
  async resolveLocation(query) {
542
- this.requireAuth();
543
- const path = this.usingPFS ? `/api/locations?q=${encodeURIComponent(query)}` : `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
564
+ this.requireApiKey();
565
+ const path = `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
544
566
  const data = await this.getWithAuth(path);
545
567
  return Array.isArray(data) ? data : data.locations || [];
546
568
  }
547
569
  /**
548
570
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
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.
571
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
572
+ * token, so PFS callers use book() directly.
551
573
  */
552
574
  async unlock(offerId) {
553
575
  this.requireApiKey();
@@ -1184,7 +1206,7 @@ Developer API only (a SEPARATE paid product \u2014 most agents should not use th
1184
1206
  they create a billing account. Use auth above instead):
1185
1207
  register --name ... --email ... Create a paid Developer API account
1186
1208
  setup-payment Attach a card to that paid account
1187
- unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
1209
+ unlock <offer_id> [Developer API only] Unlock offer (legacy)
1188
1210
 
1189
1211
  Options:
1190
1212
  --json, -j Output raw JSON
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-PW775XDB.mjs";
6
+ } from "./chunk-GO3FXQXC.mjs";
7
7
  import {
8
8
  BearerTokenError,
9
9
  getBearerToken,
@@ -348,7 +348,7 @@ Developer API only (a SEPARATE paid product \u2014 most agents should not use th
348
348
  they create a billing account. Use auth above instead):
349
349
  register --name ... --email ... Create a paid Developer API account
350
350
  setup-payment Attach a card to that paid account
351
- unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
351
+ unlock <offer_id> [Developer API only] Unlock offer (legacy)
352
352
 
353
353
  Options:
354
354
  --json, -j Output raw JSON
package/dist/index.d.mts CHANGED
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
285
  * // Developer API (prepaid credits)
286
- * const bt2 = new LetsFG({ apiKey: 'trav_...' });
286
+ * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
287
287
  * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
288
288
  * ```
289
289
  */
@@ -473,7 +473,7 @@ declare class LetsFG {
473
473
  *
474
474
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
475
475
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
476
- * Developer API: synchronous 60-90s call.
476
+ * Developer API: synchronous call.
477
477
  *
478
478
  * @param origin - IATA code (e.g., "GDN", "LON")
479
479
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -485,12 +485,19 @@ declare class LetsFG {
485
485
  private searchPFS;
486
486
  /**
487
487
  * Resolve a city/airport name to IATA codes.
488
+ *
489
+ * Developer API key only. There is no location endpoint on the PFS Bearer
490
+ * lane — the same dead end `unlock()` documents below. This used to send
491
+ * PFS callers to `/api/locations?q=`, a route that has never existed on
492
+ * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse
493
+ * error off the 404 page instead of an answer. Pass an IATA code directly
494
+ * on the PFS lane; a city code expands to every airport in that city.
488
495
  */
489
496
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
490
497
  /**
491
498
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
492
- * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
493
- * endpoint on a PFS Bearer token, so PFS callers use book() directly.
499
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
500
+ * token, so PFS callers use book() directly.
494
501
  */
495
502
  unlock(offerId: string): Promise<UnlockResult>;
496
503
  /**
package/dist/index.d.ts CHANGED
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
285
  * // Developer API (prepaid credits)
286
- * const bt2 = new LetsFG({ apiKey: 'trav_...' });
286
+ * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
287
287
  * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
288
288
  * ```
289
289
  */
@@ -473,7 +473,7 @@ declare class LetsFG {
473
473
  *
474
474
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
475
475
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
476
- * Developer API: synchronous 60-90s call.
476
+ * Developer API: synchronous call.
477
477
  *
478
478
  * @param origin - IATA code (e.g., "GDN", "LON")
479
479
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -485,12 +485,19 @@ declare class LetsFG {
485
485
  private searchPFS;
486
486
  /**
487
487
  * Resolve a city/airport name to IATA codes.
488
+ *
489
+ * Developer API key only. There is no location endpoint on the PFS Bearer
490
+ * lane — the same dead end `unlock()` documents below. This used to send
491
+ * PFS callers to `/api/locations?q=`, a route that has never existed on
492
+ * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse
493
+ * error off the 404 page instead of an answer. Pass an IATA code directly
494
+ * on the PFS lane; a city code expands to every airport in that city.
488
495
  */
489
496
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
490
497
  /**
491
498
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
492
- * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
493
- * endpoint on a PFS Bearer token, so PFS callers use book() directly.
499
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
500
+ * token, so PFS callers use book() directly.
494
501
  */
495
502
  unlock(offerId: string): Promise<UnlockResult>;
496
503
  /**
package/dist/index.js CHANGED
@@ -1606,8 +1606,12 @@ function cheapestOffer(result) {
1606
1606
  return result.offers.reduce((min, o) => o.price < min.price ? o : min, result.offers[0]);
1607
1607
  }
1608
1608
  var DEFAULT_BASE_URL = "https://letsfg.co";
1609
- var PFS_POLL_INTERVAL_MS = 1e4;
1609
+ var PFS_POLL_INTERVAL_MS = 2e3;
1610
1610
  var PFS_POLL_TIMEOUT_MS = 12e4;
1611
+ var LATE_MERGE_POLL_MS = 3e3;
1612
+ var LATE_MERGE_GRACE_MS = 9e4;
1613
+ var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
1614
+ var NON_TERMINAL = ["pending", "searching"];
1611
1615
  var LetsFG = class {
1612
1616
  bearerToken;
1613
1617
  apiKey;
@@ -1643,7 +1647,7 @@ var LetsFG = class {
1643
1647
  *
1644
1648
  * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
1645
1649
  * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
1646
- * Developer API: synchronous 60-90s call.
1650
+ * Developer API: synchronous call.
1647
1651
  *
1648
1652
  * @param origin - IATA code (e.g., "GDN", "LON")
1649
1653
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -1675,31 +1679,49 @@ var LetsFG = class {
1675
1679
  /** PFS path: POST /api/search -> poll /api/results/<id> */
1676
1680
  async searchPFS(body) {
1677
1681
  const { search_id } = await this.postWithBearer("/api/search", body);
1682
+ const poll = () => this.getNoAuth(`/api/results/${search_id}`);
1683
+ const inbound = (r) => Boolean(r.split_ticket_pending || r.gf_enrich_pending);
1678
1684
  const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
1685
+ let terminal = null;
1679
1686
  while (Date.now() < deadline) {
1680
- await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
1681
- const result = await this.getNoAuth(
1682
- `/api/results/${search_id}`
1683
- );
1684
- if (!["pending", "searching"].includes(result.status)) {
1685
- return result;
1687
+ const result = await poll();
1688
+ if (!NON_TERMINAL.includes(result.status)) {
1689
+ terminal = result;
1690
+ break;
1686
1691
  }
1692
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
1693
+ }
1694
+ if (!terminal) {
1695
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
1687
1696
  }
1688
- throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
1697
+ const lateDeadline = Date.now() + LATE_MERGE_GRACE_MS;
1698
+ while (WAIT_FOR_SPLIT && inbound(terminal) && Date.now() < lateDeadline) {
1699
+ await new Promise((r) => setTimeout(r, LATE_MERGE_POLL_MS));
1700
+ const merged = await poll();
1701
+ if (!NON_TERMINAL.includes(merged.status)) terminal = merged;
1702
+ }
1703
+ return terminal;
1689
1704
  }
1690
1705
  /**
1691
1706
  * Resolve a city/airport name to IATA codes.
1707
+ *
1708
+ * Developer API key only. There is no location endpoint on the PFS Bearer
1709
+ * lane — the same dead end `unlock()` documents below. This used to send
1710
+ * PFS callers to `/api/locations?q=`, a route that has never existed on
1711
+ * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse
1712
+ * error off the 404 page instead of an answer. Pass an IATA code directly
1713
+ * on the PFS lane; a city code expands to every airport in that city.
1692
1714
  */
1693
1715
  async resolveLocation(query) {
1694
- this.requireAuth();
1695
- const path = this.usingPFS ? `/api/locations?q=${encodeURIComponent(query)}` : `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
1716
+ this.requireApiKey();
1717
+ const path = `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
1696
1718
  const data = await this.getWithAuth(path);
1697
1719
  return Array.isArray(data) ? data : data.locations || [];
1698
1720
  }
1699
1721
  /**
1700
1722
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1701
- * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1702
- * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1723
+ * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
1724
+ * token, so PFS callers use book() directly.
1703
1725
  */
1704
1726
  async unlock(offerId) {
1705
1727
  this.requireApiKey();
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-PW775XDB.mjs";
25
+ } from "./chunk-GO3FXQXC.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.69",
3
+ "version": "2026.5.71",
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",