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; } 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: 'trav_...' }); * 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; } 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; 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'; } 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 60-90s 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. */ resolveLocation(query: string): Promise>>; /** * Unlock a flight offer — confirms live price, reveals direct airline booking URL. * Cost: 1% of ticket price, min $3. Free with Developer API. */ unlock(offerId: string): Promise; /** * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR. * Always provide idempotencyKey to prevent double-bookings on retry. */ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise; /** * Set up payment method (required before unlock/booking). */ setupPayment(token?: string): Promise>; /** * Get current agent profile and usage stats. */ me(): Promise>; /** * Register a new Developer API agent — no auth needed. */ 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, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };