type TripPurpose = 'honeymoon' | 'special_occasion' | 'business' | 'ski' | 'beach' | 'city_break' | 'family_holiday' | 'graduation' | 'concert_festival' | 'sports_event' | 'spring_break'; declare const TRIP_PURPOSES: readonly TripPurpose[]; interface TripPurposeOptions { tripPurpose?: TripPurpose | null; tripPurposes?: ReadonlyArray | null; } declare function normalizeTripPurposes({ tripPurpose, tripPurposes }: TripPurposeOptions): TripPurpose[]; declare function getPrimaryTripPurpose(options: TripPurposeOptions): TripPurpose | undefined; /** * LetsFG Personalized Flight Ranking Engine * * Open-source implementation of the scoring algorithm that powers letsfg.co. * Scores each offer across 9 dimensions with personalization weights that shift * based on trip context and purpose. Pure TypeScript — no external dependencies, * safe to run in both Node and browser. * * Usage: * import { rankOffers, type RankingContext } from 'letsfg/ranking' * const ranked = rankOffers(offers, { tripContext: 'family', requireBag: true }) * // ranked[0].offer is the best pick, ranked[0].heroFacts explains why * * Companion modules in the same package: * - trip-purpose.ts — TripPurpose type + normalization helpers * - offer-details.ts — extractOfferDetailSignals, getOfferDetailBadges, etc. */ interface RankOfferSegment { layover_minutes?: number; airline?: string; flight_number?: string; aircraft?: string; origin?: string; destination?: string; } interface RankOffer { id: string; price: number; /** Display total in the user's chosen currency (ticket + fee + ancillaries). * When provided, this is used for all price scoring and penalty calculations * instead of `price`. Callers should populate this so the ranking reflects * exactly what the user will pay. */ displayPrice?: number; google_flights_price?: number; currency: string; airline: string; origin?: string; destination?: string; departure_time: string; arrival_time: string; duration_minutes: number; stops: number; segments?: RankOfferSegment[]; inbound?: { departure_time?: string; arrival_time?: string; origin?: string; destination?: string; stops?: number; /** Inbound-leg duration in minutes. Required for round-trip scoring to * see the return leg in `scoreDuration`. When absent the inbound is * treated as zero-duration (best case for the offer). */ duration_minutes?: number; segments?: RankOfferSegment[]; }; ancillaries?: { cabin_bag?: { included?: boolean; price?: number; currency?: string; description?: string; }; checked_bag?: { included?: boolean; price?: number; currency?: string; description?: string; }; seat_selection?: { included?: boolean; price?: number; currency?: string; description?: string; }; }; conditions?: { refund_before_departure?: 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown'; change_before_departure?: 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown'; [key: string]: string | undefined; }; /** Quality tag set by upstream validateOfferBatch when the offer's intrinsic * data is untrustworthy (date drift from search criteria, asymmetric leg * durations, etc.). The ranker uses this as the highest-priority hero gate: * a suspect offer can never be hero unless every offer in the pool is suspect. * Rank-not-filter — suspect offers still appear as runner-ups. */ quality?: 'suspect' | string; } interface RankingContext { tripContext?: 'solo' | 'couple' | 'family' | 'group' | 'business_traveler'; tripPurpose?: TripPurpose; tripPurposes?: ReadonlyArray; travelerCount?: number; depTimePref?: 'early_morning' | 'morning' | 'afternoon' | 'evening' | 'night' | 'red_eye'; retTimePref?: 'early_morning' | 'morning' | 'afternoon' | 'evening' | 'night' | 'red_eye'; arrivalTimePref?: 'morning' | 'afternoon' | 'evening'; requireBag?: boolean; requireSeat?: boolean; requireMeals?: boolean; requireCancellation?: boolean; preferredAirline?: string; preferQuickFlight?: boolean; /** User said "direct" or "nonstop" — strongly boost stops weight but don't filter. * If no directs exist, 1-stop will naturally float to the top over 3-stop. */ preferDirect?: boolean; /** User explicitly asked for cheapest / lowest price — price dominates all other factors. */ preferCheapest?: boolean; /** User asked for "1 stop" / "max 2 stops" / "at most 1 connection" — a soft cap * on the number of stops. Offers exceeding the cap are gated out of the HERO slot * (rank-not-filter: they still appear as runner-ups, just below within-cap offers). * When no offer satisfies the cap it is relaxed like the other gates. preferDirect * (an implicit cap of 0) is handled by the dedicated 'direct' path; this covers * explicit caps >= 1, where the user tolerates a connection but not a 3-leg odyssey. */ maxStops?: number; /** User explicitly wants — or is happy with — a LONG layover (a stopover to break * the journey or see a city). Inverts the default short-layover preference in * scoreLayover so longer connections score higher, and boosts the layover weight. * Default (undefined/false) leaves the normal short-layover scoring intact. */ preferLongLayover?: boolean; /** Hard lower bound on departure time, in minutes from midnight. * Flights departing before this time receive a heavy score penalty (effectively filtered out). */ departAfterMins?: number; /** Hard upper bound on departure time, in minutes from midnight. */ departBeforeMins?: number; } interface ScoreBreakdown { price: number; stops: number; duration: number; depTime: number; arrivalTime: number; baggage: number; savings: number; comfortHours: number; layover: number; } interface RankedOffer { offer: T; score: number; rank: number; breakdown: ScoreBreakdown; heroFacts: string[]; tradeoffs: string[]; /** Hero only: names of user-stated criteria gates that had to be relaxed to * find a hero (no offer satisfied them all). Surfaced so the UI can banner * the mismatch — e.g. "No direct flights on this route" or "No refundable * fare available". Empty/undefined means the hero satisfied every stated * criterion. Relax order: refund -> bag -> time -> direct. */ relaxedGates?: string[]; } /** Remove near-duplicate offers: when multiple connectors return the same physical * flight (e.g. Ryanair FR1234 from both the direct connector and Kiwi/Skyscanner), * keep only the cheapest. Two offers are considered identical only when they share * the same calendar date, route, airline, outbound timing buckets, inbound timing * buckets (for round-trips), stop counts, and core fare conditions. Including the * inbound leg prevents collapsing distinct return options for the same outbound. */ declare function deduplicateOffers(offers: T[]): T[]; /** * Rank an array of flight offers by personalized score. * Returns a new array sorted best-first. The original array is not mutated. * * @param offers Array of flight offers (any type extending RankOffer) * @param ctx User intent context from the NL query parser */ declare function rankOffers(offers: T[], ctx: RankingContext, options?: { skipDedup?: boolean; }): RankedOffer[]; /** * From an already-ranked list, picks the top N offers that are genuinely * different from each other — so runner-ups are real propositions, not just * the same flight at +$3 from a different booking source. * * Two offers are considered "the same" for this purpose if both their * departure time slot (3-hour window) AND their stop count are identical. * Diversity requires differing in at least one of those dimensions. * * Falls back to next-best-ranked when the pool lacks enough diverse options. */ declare function selectDiverseTop(ranked: RankedOffer[], n: number): RankedOffer[]; /** * Returns a short human-readable label describing the ranking profile * that was applied (e.g. "City break", "Family holiday"). Returns null * if the default generic profile is used. */ declare function getProfileLabel(ctx: RankingContext): string | null; type OfferConditionState = 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown'; type OfferAmenityState = 'included' | 'available' | 'unavailable' | 'unknown'; type OfferAmenityConfidence = 'verified' | 'inferred' | 'unknown'; type OfferAmenitySource = 'ancillary' | 'condition' | 'unknown'; interface OfferDetailAncillary { included?: boolean; price?: number; currency?: string; description?: string; } interface OfferDetailConditions { refund_before_departure?: OfferConditionState; change_before_departure?: OfferConditionState; [key: string]: string | undefined; } interface OfferDetailSegmentLike { airline?: string; flight_number?: string; aircraft?: string; origin?: string; destination?: string; /** Per-leg Starlink verdict; see StarlinkSegmentVerdict in index.ts. */ starlink?: 'confirmed' | 'likely'; } interface OfferDetailLegLike { segments?: OfferDetailSegmentLike[]; } interface OfferDetailLike { airline?: string; flight_number?: string; segments?: OfferDetailSegmentLike[]; inbound?: OfferDetailLegLike; ancillaries?: { cabin_bag?: OfferDetailAncillary; checked_bag?: OfferDetailAncillary; seat_selection?: OfferDetailAncillary; }; conditions?: OfferDetailConditions; } type OfferServiceSignal = 'included' | 'available' | null; interface OfferAmenityAssessment { state: OfferAmenityState; confidence: OfferAmenityConfidence; source: OfferAmenitySource; evidence?: string; } interface OfferDetailSignals { refundability: OfferConditionState | null; changeability: OfferConditionState | null; meals: OfferServiceSignal; refreshments: OfferServiceSignal; insurance: OfferServiceSignal; lounge: OfferServiceSignal; wifi: OfferServiceSignal; power: OfferServiceSignal; entertainment: OfferServiceSignal; amenities: { meals: OfferAmenityAssessment; refreshments: OfferAmenityAssessment; insurance: OfferAmenityAssessment; lounge: OfferAmenityAssessment; wifi: OfferAmenityAssessment; power: OfferAmenityAssessment; entertainment: OfferAmenityAssessment; }; } interface OfferDetailBadge { key: string; label: string; tone: 'positive' | 'neutral' | 'negative'; } declare function extractOfferDetailSignals(offer: OfferDetailLike): OfferDetailSignals; declare function getOfferDetailBadges(offer: OfferDetailLike): OfferDetailBadge[]; declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[]; /** * LetsFG — Agent-native flight search & booking SDK for Node.js/TypeScript. * * Server-side engine covers hundreds of airlines. Free search via PFS Bearer token * or prepaid Developer API. Zero external JS dependencies. Uses native fetch (Node 18+). * * @example * ```ts * import { LetsFG } from 'letsfg'; * * // PFS (free, Bearer token from `letsfg auth`) * const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN }); * const flights = await bt.search('GDN', 'BER', '2026-03-03'); * * // Developer API (prepaid credits) * const bt2 = new LetsFG({ apiKey: 'letsfg_...' }); * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15'); * ``` */ interface FlightSegment { airline: string; airline_name: string; flight_no: string; origin: string; destination: string; origin_city: string; destination_city: string; departure: string; arrival: string; duration_seconds: number; cabin_class: string; aircraft: string; /** Starlink Wi-Fi on this leg. 'confirmed' = the carrier has fitted every * aircraft of this type. 'likely' = installation on this type is underway * but incomplete, so this airframe may not have it. Undefined means no * information — NOT an absence of Wi-Fi. */ starlink?: StarlinkSegmentVerdict; } type StarlinkSegmentVerdict = 'confirmed' | 'likely'; /** Itinerary-level Starlink verdict. '*_some' means at least one leg has none; * 'likely_*' means the rollout on that subfleet is underway but incomplete. * Only 'confirmed_*' should be shown to an end user as a fact. */ type StarlinkOfferVerdict = 'confirmed_all' | 'confirmed_some' | 'likely_all' | 'likely_some'; interface FlightRoute { segments: FlightSegment[]; total_duration_seconds: number; stopovers: number; } interface FlightOffer { id: string; price: number; currency: string; price_formatted: string; outbound: FlightRoute; inbound: FlightRoute | null; airlines: string[]; owner_airline: string; bags_price: Record; /** Starlink Wi-Fi across the whole itinerary; per-leg detail is on each * segment's `starlink`. Undefined when no leg has any information. */ starlink?: StarlinkOfferVerdict; availability_seats: number | null; conditions: Record; is_locked: boolean; fetched_at: string; booking_url: string; } interface FlightSearchResult { search_id: string; offer_request_id: string; passenger_ids: string[]; origin: string; destination: string; currency: string; offers: FlightOffer[]; total_results: number; search_params: Record; pricing_note: string; } interface UnlockResult { offer_id: string; unlock_status: string; payment_charged: boolean; payment_amount_cents: number; payment_currency: string; payment_intent_id: string; confirmed_price: number | null; confirmed_currency: string; offer_expires_at: string; message: string; } interface Passenger { id: string; given_name: string; family_name: string; born_on: string; gender?: string; title?: string; email?: string; phone_number?: string; } interface BookingResult { booking_id: string; status: string; booking_type: string; offer_id: string; flight_price: number; service_fee: number; service_fee_percentage: number; total_charged: number; currency: string; order_id: string; booking_reference: string; unlock_payment_id: string; fee_payment_id: string; created_at: string; details: Record; } interface SearchOptions { returnDate?: string; adults?: number; children?: number; infants?: number; cabinClass?: 'M' | 'W' | 'C' | 'F'; maxStopovers?: number; currency?: string; limit?: number; sort?: 'price' | 'duration'; departureTimeFrom?: string; departureTimeTo?: string; } interface LetsFGConfig { /** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */ bearerToken?: string; /** Developer API key (prepaid credits, no per-booking fee). */ apiKey?: string; baseUrl?: string; timeout?: number; } declare const ErrorCode: { readonly SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT"; readonly RATE_LIMITED: "RATE_LIMITED"; readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE"; readonly NETWORK_ERROR: "NETWORK_ERROR"; readonly INVALID_IATA: "INVALID_IATA"; readonly INVALID_DATE: "INVALID_DATE"; readonly INVALID_PASSENGERS: "INVALID_PASSENGERS"; readonly UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE"; readonly MISSING_PARAMETER: "MISSING_PARAMETER"; readonly INVALID_PARAMETER: "INVALID_PARAMETER"; readonly AUTH_INVALID: "AUTH_INVALID"; readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED"; readonly PAYMENT_DECLINED: "PAYMENT_DECLINED"; readonly OFFER_EXPIRED: "OFFER_EXPIRED"; readonly OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED"; readonly FARE_CHANGED: "FARE_CHANGED"; readonly ALREADY_BOOKED: "ALREADY_BOOKED"; readonly BOOKING_FAILED: "BOOKING_FAILED"; }; type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; declare const ErrorCategory: { readonly TRANSIENT: "transient"; readonly VALIDATION: "validation"; readonly BUSINESS: "business"; }; type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory]; declare class LetsFGError extends Error { statusCode: number; response: Record; errorCode: string; errorCategory: ErrorCategoryType; isRetryable: boolean; constructor(message: string, statusCode?: number, response?: Record, errorCode?: string); } declare class AuthenticationError extends LetsFGError { constructor(message: string, response?: Record); } declare class PaymentRequiredError extends LetsFGError { constructor(message: string, response?: Record); } declare class OfferExpiredError extends LetsFGError { constructor(message: string, response?: Record); } declare class ValidationError extends LetsFGError { constructor(message: string, statusCode?: number, response?: Record, errorCode?: string); } /** One-line offer summary */ declare function offerSummary(offer: FlightOffer): string; /** Get cheapest offer from search results */ declare function cheapestOffer(result: FlightSearchResult): FlightOffer | null; declare class LetsFG { private bearerToken; private apiKey; private baseUrl; private timeout; constructor(config?: LetsFGConfig); private requireAuth; private requireApiKey; /** True when using PFS Bearer token (free search path) */ private get usingPFS(); /** * Search for flights — FREE. * * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config. * PFS: async polling (POST /api/search -> poll /api/results/ every 10s). * Developer API: synchronous call. * * @param origin - IATA code (e.g., "GDN", "LON") * @param destination - IATA code (e.g., "BER", "BCN") * @param dateFrom - Departure date "YYYY-MM-DD" * @param options - Optional search parameters */ search(origin: string, destination: string, dateFrom: string, options?: SearchOptions): Promise; /** PFS path: POST /api/search -> poll /api/results/ */ private searchPFS; /** * Resolve a city/airport name to IATA codes. * * Developer API key only. There is no location endpoint on the PFS Bearer * lane — the same dead end `unlock()` documents below. This used to send * PFS callers to `/api/locations?q=`, a route that has never existed on * letsfg.co (verified 2026-08-16: 404, text/html), so they got a JSON parse * error off the 404 page instead of an answer. Pass an IATA code directly * on the PFS lane; a city code expands to every airport in that city. */ resolveLocation(query: string): Promise>>; /** * Unlock a flight offer — confirms live price, reveals direct airline booking URL. * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer * token, so PFS callers use book() directly. */ unlock(offerId: string): Promise; /** * Book a flight. * * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond * the ticket price. Pass searchId (from search()'s result). Returns either * { ok, booked: true, order_id } or, when the booking genuinely could not * complete, { ok, booked: false, booking_url } — hand the link to the user, * nothing was charged. * * Developer API (X-API-Key): charges ticket price + service fee via Stripe, * creates a real PNR. Requires unlock() first. Always provide * idempotencyKey to prevent double-bookings on retry. */ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise>; /** * Resolve a place name to the city id that searchHotels() needs. * * Use `Id` from the first result as `cityId` and `Name` as `cityName`. */ hotelDestinations(text: string): Promise>>; /** * Search real, bookable hotel inventory. * * Slow by nature — the supplier streams a whole city and every rate is priced * — so this gets its own generous timeout rather than the client default. * * Each offer carries `price` (what the guest pays), `reservation_fee_now` * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and * `free_cancellation_until`. There is no wholesale figure to quote by mistake. * * Keep `session_id` and the chosen offer's `combination_id_v2`: together they * identify the exact rate, and booking needs both. */ searchHotels(params: { cityId: number; cityName: string; checkIn: string; checkOut: string; adults?: number; children?: number; childAges?: number[]; /** Two-letter code. Rates and taxes genuinely differ by nationality. */ nationality?: string; limit?: number; withImages?: boolean; }): Promise>; /** * Start a booking. Returns a job immediately — it does NOT book inline. * * A booking takes minutes: the rate is re-blocked at the supplier, every * price and date rail is checked, the 5% reservation fee is charged to your * card, and only then is the room committed. No proxy holds a connection that * long, so this returns at once and you poll hotelBooking() for the outcome. * Use bookHotelAndWait() if you would rather block. * * Because the fee is taken BEFORE the commit, a declined card costs nothing * to unwind: no reservation exists and nothing is charged. * * Send `expectedPrice` and `expectedBalance` back exactly as search returned * them — the booking is refused if the supplier has moved beyond tolerance, * so a guest is never charged a price they did not agree to. * * Do NOT call this again for the same rate while a job is running: that books * the room twice and charges two reservation fees. */ bookHotel(params: { sessionId: string; hotelCode: number; combinationIdV2: string; expectedPrice: number; expectedBalance: number; cityId: number; cityName: string; checkIn: string; checkOut: string; guests: Array<{ title: string; first_name: string; last_name: string; }>; /** The voucher and the pay link go here. A typo loses the booking. */ email: string; phone: string; adults?: number; combinationId?: number; hotelName?: string; phoneCountryCode?: string; specialRequests?: string[]; }): Promise>; /** * Collect the result of a booking started with bookHotel(). * * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`, * `balance_due_by` and `terms` (including the full cancellation ladder). */ hotelBooking(bookingJobId: string): Promise>; /** * bookHotel(), then poll until the booking settles. Convenience only. * * Giving up after `maxWaitMs` does NOT cancel anything — the booking may * still complete. The returned object carries `booking_job_id` so you can * keep polling, and the confirmation is emailed to the guest regardless. */ bookHotelAndWait(params: Parameters[0] & { pollIntervalMs?: number; maxWaitMs?: number; }): Promise>; /** * Release a reservation at the supplier. * * Free until `balance_due_by`; after that the hotel's own cancellation ladder * applies and can reach 100%. The ladder ships in the booking's `terms`, so * you can see the cost before calling this. The 5% reservation fee is NOT * refunded. * * This drives a browser at the supplier and takes over a minute. If it times * out, do not assume it failed — re-check before retrying. */ cancelHotel(confirmation: string): Promise>; /** * [Developer API only] Attach a card to a PAID prepaid Developer API account. * * Most agents should NOT call this. It is unrelated to authenticating for * search and booking — for that, run `letsfg auth`, which puts a card on file * through a zero-amount setup and creates no billing account. */ setupPayment(token?: string): Promise>; /** * Get current agent profile and usage stats. */ me(): Promise>; /** * [Developer API only] Create a PAID Developer API account with its own billing. * * Most agents should NOT call this — it creates a billing account you probably * do not want. To search and book flights, run `letsfg auth` instead. */ static register(agentName: string, email: string, baseUrl?: string, ownerName?: string, description?: string): Promise>; private postWithBearer; private getNoAuth; private getWithAuth; private postWithAuth; private post; private get; private requestWithHeaders; } declare const BoostedTravel: typeof LetsFG; declare const BoostedTravelError: typeof LetsFGError; type BoostedTravelConfig = LetsFGConfig; export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, type StarlinkOfferVerdict, type StarlinkSegmentVerdict, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };