import { analyzeCheckoutContext } from "./checkout-analysis"; import { computeTrustTier, type TrustTier } from "./trust-rating"; import { analyzeDealIntelligence, type ProductCandidateInput } from "./deal-intelligence"; type MissionCategory = "hotel" | "airline" | "marketplace" | "car_rental"; type MissionTier = TrustTier; type Recommendation = "proceed" | "proceed_with_caution" | "switch"; type ActionStatus = "completed" | "recommended" | "approval_required"; export type AgenticMissionRequest = { domain: string; url?: string; pageType?: string; bookingType?: string; isBookingPlatform?: boolean; route?: { from?: string; to?: string; display?: string } | null; purchaseGoal: string; budget?: number; listedPrice?: number; category?: MissionCategory; preferences?: string[]; allowAccountCreation?: boolean; allowAutoDeclineUpsells?: boolean; compareAcrossSites?: boolean; pageText?: string; priceStrings?: string[]; buttonLabels?: string[]; timerElements?: string[]; stockAlerts?: string[]; variantUrgency?: string[]; productCandidates?: ProductCandidateInput[]; }; type MissionSignal = { type: string; label: string; severity: "low" | "medium" | "high"; confidence: number; evidence: string; whyItMatters: string; fix: string; }; type MissionAction = { title: string; detail: string; status: ActionStatus; }; type GhostCheckoutStep = { label: string; status: "completed" | "planned" | "watch"; detail: string; }; type GhostCheckoutLane = { label: string; amount: number; confidence: "high" | "medium" | "low"; source: string; }; type MissionAlternative = { domain: string; merchantName: string; listedPrice: number; estimatedTrueTotal: number; trustScore: number; trustTier: MissionTier; bestValue: boolean; why: string[]; }; type GhostCheckoutPlan = { mode: "disabled" | "simulated_supervised"; status: string; revealedTotal: number | null; deltaFromHeadline: number | null; hiddenCostLanes: GhostCheckoutLane[]; fakePressureSummary: string[]; saferPathMode: { available: boolean; label: string; detail: string; }; supervisedSteps: GhostCheckoutStep[]; deliveryThreshold?: { threshold: number | null; needsMore: number | null; hasFreeDelivery: boolean; note: string; }; platformFeeRisk?: { risk: "high" | "medium" | "low"; likelihood: number; estimatedFee: number; reasoning: string; }; }; type MissionProfile = { merchantName: string; category: MissionCategory; trustScore: number; listedMarkupRate: number; hiddenFeeRate: number; alternativeDomains: string[]; signals: MissionSignal[]; credibilitySignals: string[]; }; type FeeEstimate = { fees: number; confidence: "high" | "medium" | "low"; breakdown: GhostCheckoutLane[]; platformFeeRisk: { risk: "high" | "medium" | "low"; likelihood: number; estimatedFee: number; reasoning: string; }; }; type RecommendationDetails = { action: Recommendation; summary: string; reasoning: string[]; nextSteps: string[]; }; const PROFILE_MAP: Record = { "booking.com": { merchantName: "Booking.com", category: "hotel", trustScore: 29, listedMarkupRate: 1, hiddenFeeRate: 0.24, alternativeDomains: ["hotels.com", "expedia.com", "airbnb.com"], credibilitySignals: ["Large inventory coverage", "Strong hotel selection depth", "Reliable confirmation flows"], signals: [ { type: "false_urgency", label: "False Urgency", severity: "high", confidence: 0.93, evidence: "Countdown and price-lock language appears before taxes and mandatory charges are shown.", whyItMatters: "It pressures the shopper into committing before the true total is visible.", fix: "Show the timer only when the inventory event is verifiable for that session.", }, { type: "hidden_fees", label: "Hidden Fees", severity: "high", confidence: 0.95, evidence: "Resort and sustainability charges often surface late in the booking path.", whyItMatters: "The shopper anchors on a low headline price and discovers the real total only after sunk cost kicks in.", fix: "Roll mandatory charges into the first quoted price and keep them visible all the way through checkout.", }, { type: "prechecked_addons", label: "Pre-Checked Add-Ons", severity: "medium", confidence: 0.9, evidence: "Insurance and promotional extras are frequently shown as default-on options.", whyItMatters: "It converts inattention into extra spend.", fix: "Leave optional protection and upsells off by default.", }, ], }, "makemytrip.com": { merchantName: "MakeMyTrip", category: "hotel", trustScore: 34, listedMarkupRate: 1, hiddenFeeRate: 0.19, alternativeDomains: ["goibibo.com", "booking.com", "agoda.com"], credibilitySignals: ["Wide India travel inventory", "Fast hotel and flight packaging", "Established checkout infrastructure"], signals: [ { type: "false_scarcity", label: "False Scarcity", severity: "medium", confidence: 0.88, evidence: "Low-stock banners and recent-booking counters are commonly used near room selection.", whyItMatters: "It increases FOMO before taxes and extras are fully understood.", fix: "Use demand and availability messaging only when tied to verified live inventory.", }, { type: "hidden_fees", label: "Late Fee Disclosure", severity: "high", confidence: 0.9, evidence: "Convenience fees and taxes can remain unclear until late in the flow.", whyItMatters: "The user may approve an option that is already over budget.", fix: "Promote the final payable amount earlier in the journey.", }, ], }, "united.com": { merchantName: "United Airlines", category: "airline", trustScore: 43, listedMarkupRate: 1, hiddenFeeRate: 0.17, alternativeDomains: ["delta.com", "southwest.com", "jetblue.com"], credibilitySignals: ["Direct airline booking channel", "Stable reservation and refund records", "Clear post-purchase itinerary management"], signals: [ { type: "misdirection", label: "Misdirection", severity: "high", confidence: 0.91, evidence: "Seat upgrades and baggage options dominate the flow while baseline choices are visually downplayed.", whyItMatters: "The cheapest acceptable path becomes harder to find than the upsell path.", fix: "Make the standard path equal in prominence to the upgraded path.", }, { type: "prechecked_addons", label: "Default Extras", severity: "medium", confidence: 0.86, evidence: "Protection and expedited airport products are presented as default or strongly suggested add-ons.", whyItMatters: "The shopper can end up paying for extras that are not aligned with their preferences.", fix: "Ask for explicit consent before any optional add-on is applied.", }, ], }, "amazon.com": { merchantName: "Amazon", category: "marketplace", trustScore: 68, listedMarkupRate: 1, hiddenFeeRate: 0.08, alternativeDomains: ["walmart.com", "bestbuy.com", "etsy.com"], credibilitySignals: ["Strong fulfillment reliability", "Fast delivery estimates", "Large seller ecosystem with reviews"], signals: [ { type: "prechecked_addons", label: "Subscription Steering", severity: "medium", confidence: 0.79, evidence: "Prime trials and protection plans may be positioned as the default convenience path.", whyItMatters: "A low-friction checkout can still quietly nudge users into recurring spend.", fix: "Present trial and warranty options only after the baseline purchase path is complete.", }, ], }, "amazon.in": { merchantName: "Amazon India", category: "marketplace", trustScore: 80, listedMarkupRate: 1, hiddenFeeRate: 0.04, alternativeDomains: ["flipkart.com", "ajio.com", "myntra.com"], credibilitySignals: ["Transparent tax display on most PDP flows", "Strong delivery and fulfillment reliability in India", "Lower pattern intensity than fashion-led marketplaces"], signals: [ { type: "subscription_steering", label: "Subscription Steering", severity: "low", confidence: 0.71, evidence: "Prime and card-linked benefits may be highlighted before the baseline purchase choice.", whyItMatters: "Shoppers can be nudged into extra commitments while trying to secure a deal quickly.", fix: "Keep the buy path clear even when loyalty benefits are promoted.", }, ], }, "myntra.com": { merchantName: "Myntra", category: "marketplace", trustScore: 62, listedMarkupRate: 1, hiddenFeeRate: 0.13, alternativeDomains: ["amazon.in", "flipkart.com", "ajio.com"], credibilitySignals: ["Strong fashion assortment", "Readable PDP pricing for headline offers", "Reasonably clear refund and delivery messaging on supported items"], signals: [ { type: "false_scarcity", label: "Scarcity Pressure", severity: "medium", confidence: 0.84, evidence: "Low-stock chips and size-level urgency are used heavily around apparel variants.", whyItMatters: "It can push the shopper to rush before checking whether the price is genuinely competitive elsewhere.", fix: "Show variant inventory only when it is session-specific and validated against live stock.", }, { type: "price_opacity", label: "Platform Fee Risk", severity: "medium", confidence: 0.76, evidence: "Fashion marketplaces can introduce platform or shipping fees late for lower-value carts.", whyItMatters: "The product page can look cheap while the payable total drifts upward at checkout.", fix: "Preview platform fees and delivery thresholds before the shopper reaches the bag.", }, ], }, "flipkart.com": { merchantName: "Flipkart", category: "marketplace", trustScore: 75, listedMarkupRate: 1, hiddenFeeRate: 0.08, alternativeDomains: ["amazon.in", "myntra.com", "ajio.com"], credibilitySignals: ["Competitive marketplace pricing in India", "Better fee predictability on many standard carts", "Good delivery network coverage"], signals: [ { type: "upsell_pressure", label: "Upsell Pressure", severity: "low", confidence: 0.68, evidence: "Protection and exchange nudges can be added around cart and delivery preference steps.", whyItMatters: "Shoppers can pay more than intended even when the headline deal is fine.", fix: "Keep optional protection and exchange upgrades off unless chosen explicitly.", }, ], }, "ajio.com": { merchantName: "Ajio", category: "marketplace", trustScore: 72, listedMarkupRate: 1, hiddenFeeRate: 0.1, alternativeDomains: ["amazon.in", "flipkart.com", "myntra.com"], credibilitySignals: ["Relevant India fashion benchmark", "Competitive fashion discounts", "Moderate checkout complexity relative to peers"], signals: [ { type: "discount_anchoring", label: "Heavy Discount Anchoring", severity: "medium", confidence: 0.74, evidence: "Large markdown percentages are frequently foregrounded in fashion listings.", whyItMatters: "Shoppers may focus on the size of the discount instead of the final payable value.", fix: "Pair discount claims with recent price history or payable total context.", }, ], }, "airbnb.com": { merchantName: "Airbnb", category: "hotel", trustScore: 63, listedMarkupRate: 1, hiddenFeeRate: 0.22, alternativeDomains: ["booking.com", "vrbo.com", "hotels.com"], credibilitySignals: ["Strong listing depth", "Rich review data", "Good refund and host messaging history"], signals: [ { type: "hidden_fees", label: "Cleaning and Service Fees", severity: "medium", confidence: 0.89, evidence: "Cleaning and platform fees can materially shift the final price.", whyItMatters: "The best-looking nightly rate may not be the best total price.", fix: "Sort and compare by full stay total rather than headline nightly price.", }, ], }, "etsy.com": { merchantName: "Etsy", category: "marketplace", trustScore: 90, listedMarkupRate: 1, hiddenFeeRate: 0.04, alternativeDomains: ["amazon.com", "ebay.com", "bestbuy.com"], credibilitySignals: ["Transparent checkout copy", "Generally neutral purchase path", "Low prevalence of aggressive conversion patterns"], signals: [], }, }; const ALTERNATIVE_MAP: Record = { "hotels.com": { merchantName: "Hotels.com", trustScore: 72, baseDelta: -0.04, feeRate: 0.14 }, "expedia.com": { merchantName: "Expedia", trustScore: 58, baseDelta: -0.03, feeRate: 0.17 }, "agoda.com": { merchantName: "Agoda", trustScore: 61, baseDelta: -0.02, feeRate: 0.16 }, "goibibo.com": { merchantName: "Goibibo", trustScore: 56, baseDelta: -0.05, feeRate: 0.15 }, "delta.com": { merchantName: "Delta", trustScore: 74, baseDelta: -0.01, feeRate: 0.11 }, "southwest.com": { merchantName: "Southwest", trustScore: 81, baseDelta: 0.02, feeRate: 0.09 }, "jetblue.com": { merchantName: "JetBlue", trustScore: 70, baseDelta: -0.02, feeRate: 0.1 }, "walmart.com": { merchantName: "Walmart", trustScore: 73, baseDelta: -0.05, feeRate: 0.05 }, "bestbuy.com": { merchantName: "Best Buy", trustScore: 77, baseDelta: -0.03, feeRate: 0.03 }, "vrbo.com": { merchantName: "Vrbo", trustScore: 66, baseDelta: -0.06, feeRate: 0.19 }, "ebay.com": { merchantName: "eBay", trustScore: 64, baseDelta: -0.04, feeRate: 0.06 }, "amazon.in": { merchantName: "Amazon India", trustScore: 80, baseDelta: -0.02, feeRate: 0.04 }, "flipkart.com": { merchantName: "Flipkart", trustScore: 75, baseDelta: -0.03, feeRate: 0.08 }, "myntra.com": { merchantName: "Myntra", trustScore: 62, baseDelta: 0, feeRate: 0.13 }, "ajio.com": { merchantName: "Ajio", trustScore: 72, baseDelta: 0.01, feeRate: 0.1 }, }; function normalizeDomain(input: string): string { return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? "example.com"; } function inferCategory(domain: string, requested?: MissionCategory): MissionCategory { if (requested) return requested; const profile = PROFILE_MAP[domain]; if (profile) return profile.category; if (domain.includes("air") || domain.includes("flight")) return "airline"; if (domain.includes("hotel") || domain.includes("trip") || domain.includes("stay")) return "hotel"; if (domain.includes("rental")) return "car_rental"; return "marketplace"; } function fallbackProfile(domain: string, category: MissionCategory): MissionProfile { const trustScore = category === "hotel" ? 46 : category === "airline" ? 54 : category === "car_rental" ? 41 : 62; const hiddenFeeRate = category === "hotel" ? 0.18 : category === "airline" ? 0.14 : category === "car_rental" ? 0.28 : 0.07; return { merchantName: domain, category, trustScore, listedMarkupRate: 1, hiddenFeeRate, alternativeDomains: category === "hotel" ? ["hotels.com", "expedia.com", "airbnb.com"] : category === "airline" ? ["delta.com", "southwest.com", "jetblue.com"] : ["walmart.com", "bestbuy.com", "etsy.com"], credibilitySignals: ["Functional checkout path", "No verified catastrophic trust signal in the current profile"], signals: [ { type: "price_opacity", label: "Price Opacity Risk", severity: hiddenFeeRate >= 0.15 ? "high" : "medium", confidence: 0.71, evidence: "This category commonly adds fees late in checkout.", whyItMatters: "The first price quote may not represent the final amount the shopper pays.", fix: "Run ghost checkout before committing time or emotional energy to the flow.", }, ], }; } function roundCurrency(value: number): number { return Math.round(value * 100) / 100; } function isKnownNonCommerceDomain(domain: string): boolean { return [ "github.com", "stackoverflow.com", "wikipedia.org", "docs.python.org", "developer.mozilla.org", "medium.com", "notion.so", "vercel.com", ].some((candidate) => domain === candidate || domain.endsWith(`.${candidate}`)); } function isIndianCommerceDomain(domain: string): boolean { return [ "myntra.com", "flipkart.com", "ajio.com", "tatacliq.com", "nykaa.com", "meesho.com", "amazon.in", "makemytrip.com", "goibibo.com", ].some((candidate) => domain === candidate || domain.endsWith(`.${candidate}`)); } function detectFreeDeliveryThreshold(domain: string, currentPrice: number, pageText?: string) { const normalizedDomain = normalizeDomain(domain); let threshold: number | null = null; if (normalizedDomain === "myntra.com" || normalizedDomain === "ajio.com") { threshold = 999; } else if (normalizedDomain === "flipkart.com") { threshold = 500; } else if (normalizedDomain === "amazon.in") { threshold = 499; } if (threshold == null && pageText) { const match = pageText.match(/₹\s*(\d+)\s*(more)?\s*for\s*free\s*(delivery|shipping)/i); if (match?.[1]) { threshold = Number(match[1]); } } const hasFreeDelivery = threshold != null ? currentPrice >= threshold : false; const needsMore = threshold != null && !hasFreeDelivery ? roundCurrency(threshold - currentPrice) : null; return { threshold, needsMore, hasFreeDelivery, note: threshold == null ? "No delivery threshold was confirmed from the current merchant profile." : hasFreeDelivery ? `The current cart is already past the free-delivery threshold of ${threshold}.` : `The current cart is below the free-delivery threshold of ${threshold}.`, }; } function assessPlatformFeeRisk(domain: string, cartValue: number) { const normalizedDomain = normalizeDomain(domain); if (normalizedDomain === "myntra.com") { if (cartValue < 500) { return { risk: "high" as const, likelihood: 0.8, estimatedFee: 20, reasoning: "Myntra frequently adds platform-fee pressure on low-value carts.", }; } if (cartValue < 999) { return { risk: "medium" as const, likelihood: 0.55, estimatedFee: 20, reasoning: "Myntra carts below the delivery threshold often carry extra platform or shipping friction.", }; } } if (normalizedDomain === "ajio.com") { return { risk: cartValue < 999 ? ("medium" as const) : ("low" as const), likelihood: cartValue < 999 ? 0.6 : 0.2, estimatedFee: cartValue < 999 ? 20 : 0, reasoning: "Ajio can add shipping and convenience pressure below the main free-delivery threshold.", }; } if (normalizedDomain === "amazon.in") { return { risk: "low" as const, likelihood: 0.1, estimatedFee: 0, reasoning: "Amazon India usually keeps platform-fee surprises relatively low.", }; } if (normalizedDomain === "flipkart.com") { return { risk: cartValue < 500 ? ("medium" as const) : ("low" as const), likelihood: cartValue < 500 ? 0.35 : 0.15, estimatedFee: 0, reasoning: "Flipkart has lower fee opacity than fashion-first marketplaces, though lower carts may still absorb shipping.", }; } return { risk: "low" as const, likelihood: 0.2, estimatedFee: 0, reasoning: "No strong merchant-specific late-fee pattern is currently modeled.", }; } function inferShoppingIntent(domain: string, input: AgenticMissionRequest, category: MissionCategory): { isShoppingPage: boolean; reason: string } { if (PROFILE_MAP[domain]) { return { isShoppingPage: true, reason: "Recognized commerce domain profile." }; } if (isKnownNonCommerceDomain(domain)) { return { isShoppingPage: false, reason: "Known non-commerce domain." }; } const pageText = String(input.pageText ?? "").toLowerCase(); const buttonLabels = Array.isArray(input.buttonLabels) ? input.buttonLabels.join(" ").toLowerCase() : ""; const priceStrings = Array.isArray(input.priceStrings) ? input.priceStrings : []; const productCandidates = Array.isArray(input.productCandidates) ? input.productCandidates : []; const keywordMatches = [ "add to cart", "checkout", "reserve now", "book now", "buy now", "place order", "payment", "shipping", "tax", "insurance", "seat selection", "trip protection", ].filter((keyword) => pageText.includes(keyword) || buttonLabels.includes(keyword)).length; const priceSignalCount = priceStrings.filter((value) => /\d/.test(value)).length; const productSignalCount = productCandidates.filter((candidate) => typeof candidate?.title === "string" && candidate.title.trim().length > 0).length; const hasProductUrlHint = /\/dp\/|\/gp\/product\/|\/product\//.test(String(input.url ?? "").toLowerCase()); if ( priceSignalCount >= 2 || keywordMatches >= 2 || productSignalCount >= 2 || (productSignalCount >= 1 && (priceSignalCount >= 1 || keywordMatches >= 1 || hasProductUrlHint)) ) { return { isShoppingPage: true, reason: "Detected commerce language or price signals in the current page context." }; } if (category === "marketplace" && priceSignalCount === 0 && keywordMatches === 0) { return { isShoppingPage: false, reason: "No meaningful commerce or checkout signals were found on the current page." }; } return { isShoppingPage: true, reason: "Falling back to a generic commerce heuristic." }; } function estimateGhostCheckoutCosts( domain: string, profile: MissionProfile, listedPrice: number, pageType?: string, ): FeeEstimate { const normalizedDomain = normalizeDomain(domain); const breakdown: GhostCheckoutLane[] = []; let totalFees = 0; const platformFeeRisk = assessPlatformFeeRisk(normalizedDomain, listedPrice); if (normalizedDomain === "amazon.in" && profile.category === "marketplace") { breakdown.push({ label: listedPrice >= 499 ? "Free delivery" : "Shipping charges", amount: listedPrice >= 499 ? 0 : 40, confidence: "high", source: listedPrice >= 499 ? "Amazon India free-delivery threshold reached" : "Amazon India shipping threshold estimate for lower-value carts", }); if (pageType === "checkout") { breakdown.push({ label: "Cash-on-delivery risk", amount: 40, confidence: "low", source: "Optional COD fee estimate if pay-on-delivery is selected", }); totalFees += 40; } totalFees += listedPrice >= 499 ? 0 : 40; breakdown.push({ label: "Prime membership suggestion", amount: 0, confidence: "medium", source: "Optional Prime upsell may appear, but it is not required for the baseline purchase path", }); return { fees: roundCurrency(totalFees), confidence: "high", breakdown, platformFeeRisk, }; } if (isIndianCommerceDomain(normalizedDomain) && profile.category === "marketplace") { if (listedPrice < 500) { if (platformFeeRisk.estimatedFee > 0) { breakdown.push({ label: "Platform fee", amount: platformFeeRisk.estimatedFee, confidence: platformFeeRisk.risk === "high" ? "high" : "medium", source: platformFeeRisk.reasoning, }); totalFees += platformFeeRisk.estimatedFee; } } if (listedPrice < 999 && normalizedDomain !== "flipkart.com" && normalizedDomain !== "amazon.in") { breakdown.push({ label: "Shipping charges", amount: 50, confidence: "medium", source: "Merchant free-delivery threshold estimate for India fashion marketplaces", }); totalFees += 50; } if (pageType === "checkout") { breakdown.push({ label: "Cash-on-delivery risk", amount: 40, confidence: "low", source: "Optional COD fee estimate if the shopper chooses pay-on-delivery", }); totalFees += 40; } if (breakdown.length === 0) { const fallbackFee = roundCurrency(listedPrice * profile.hiddenFeeRate); if (fallbackFee > 0) { breakdown.push({ label: "Estimated platform and tax drift", amount: fallbackFee, confidence: "medium", source: "Fallback India marketplace fee model", }); totalFees += fallbackFee; } } return { fees: roundCurrency(totalFees), confidence: breakdown.some((lane) => lane.confidence === "high") ? "high" : "medium", breakdown, platformFeeRisk, }; } const defaultTax = roundCurrency(listedPrice * (profile.category === "marketplace" ? 0.08 : profile.hiddenFeeRate * 0.55)); if (defaultTax > 0) { breakdown.push({ label: profile.category === "marketplace" ? "Taxes and platform fees" : "Taxes and mandatory fees", amount: defaultTax, confidence: profile.hiddenFeeRate >= 0.18 ? "high" : "medium", source: "Category fee model + merchant trust profile", }); totalFees += defaultTax; } const surcharge = roundCurrency(Math.max(0, listedPrice * profile.hiddenFeeRate - defaultTax)); if (surcharge > 0) { breakdown.push({ label: profile.category === "hotel" ? "Mandatory service or resort fees" : profile.category === "airline" ? "Ancillary checkout pressure" : "Late checkout surcharges", amount: surcharge, confidence: profile.hiddenFeeRate >= 0.18 ? "medium" : "low", source: "Ghost-checkout simulation and shared checkout signals", }); totalFees += surcharge; } return { fees: roundCurrency(totalFees), confidence: breakdown.some((lane) => lane.confidence === "high") ? "high" : "medium", breakdown, platformFeeRisk, }; } function buildAlternatives( profile: MissionProfile, listedPrice: number, budget?: number, context?: { isBookingPlatform?: boolean; bookingType?: string; route?: { display?: string } | null }, ): MissionAlternative[] { if (context?.isBookingPlatform) { if (context.bookingType === "bus") { const busOptions: MissionAlternative[] = [ { domain: "abhibus.com", merchantName: "Abhibus", listedPrice: roundCurrency(listedPrice * 0.96), estimatedTrueTotal: roundCurrency(listedPrice * 0.96 + 15), trustScore: 78, trustTier: computeTrustTier(78), bestValue: false, why: ["Lower platform-fee pressure than many aggregator flows", "Often competitive on India intercity bus routes"], }, { domain: "zingbus.com", merchantName: "Zingbus", listedPrice: roundCurrency(listedPrice * 0.95), estimatedTrueTotal: roundCurrency(listedPrice * 0.95), trustScore: 75, trustTier: computeTrustTier(75), bestValue: false, why: ["Own-fleet pricing can avoid some aggregator fee layering", "Good benchmark when RedBus feels overpriced"], }, { domain: "makemytrip.com", merchantName: "MakeMyTrip Bus", listedPrice: roundCurrency(listedPrice * 0.98), estimatedTrueTotal: roundCurrency(listedPrice * 0.98 + 30), trustScore: 76, trustTier: computeTrustTier(76), bestValue: false, why: ["Reasonable backup marketplace for route comparison", "Useful if support and route integration matter more than lowest fare"], }, ]; const best = [...busOptions].sort((a, b) => a.estimatedTrueTotal - b.estimatedTrueTotal)[0]; return busOptions.map((option) => ({ ...option, bestValue: option.domain === best.domain })); } if (context.bookingType === "hotel") { const hotelOptions: MissionAlternative[] = [ { domain: "booking.com", merchantName: "Booking.com", listedPrice: roundCurrency(listedPrice * 0.97), estimatedTrueTotal: roundCurrency(listedPrice * 0.97), trustScore: 82, trustTier: computeTrustTier(82), bestValue: false, why: ["Useful as a broad hotel benchmark", "Often clearer cancellation and property comparison flow"], }, { domain: "agoda.com", merchantName: "Agoda", listedPrice: roundCurrency(listedPrice * 0.95), estimatedTrueTotal: roundCurrency(listedPrice * 0.95), trustScore: 79, trustTier: computeTrustTier(79), bestValue: false, why: ["Often competitive in Asia-focused hotel pricing", "Worth checking when nightly rates look inflated elsewhere"], }, { domain: "makemytrip.com", merchantName: "MakeMyTrip Hotels", listedPrice: roundCurrency(listedPrice * 0.98), estimatedTrueTotal: roundCurrency(listedPrice * 0.98), trustScore: 77, trustTier: computeTrustTier(77), bestValue: false, why: ["Useful local support benchmark", "Good fallback when international OTAs feel opaque"], }, ]; const best = [...hotelOptions].sort((a, b) => a.estimatedTrueTotal - b.estimatedTrueTotal)[0]; return hotelOptions.map((option) => ({ ...option, bestValue: option.domain === best.domain })); } if (context.bookingType === "flight") { const flightOptions: MissionAlternative[] = [ { domain: "google.com/travel/flights", merchantName: "Google Flights", listedPrice: roundCurrency(listedPrice * 0.98), estimatedTrueTotal: roundCurrency(listedPrice * 0.98), trustScore: 85, trustTier: computeTrustTier(85), bestValue: false, why: ["Strong benchmark for fare transparency", "Useful to validate whether aggregator urgency is real"], }, { domain: "cleartrip.com", merchantName: "Cleartrip", listedPrice: roundCurrency(listedPrice * 0.97), estimatedTrueTotal: roundCurrency(listedPrice * 0.97), trustScore: 78, trustTier: computeTrustTier(78), bestValue: false, why: ["Cleaner UI and fee structure than some travel aggregators", "Worth checking before trusting a rising fare"], }, { domain: "direct-airline.example", merchantName: "Airline Direct", listedPrice: roundCurrency(listedPrice * 0.99), estimatedTrueTotal: roundCurrency(listedPrice * 0.99), trustScore: 88, trustTier: computeTrustTier(88), bestValue: false, why: ["Direct airline channels are often the most reliable reference", "Helpful to check whether an OTA is layering extra friction"], }, ]; const best = [...flightOptions].sort((a, b) => a.estimatedTrueTotal - b.estimatedTrueTotal)[0]; return flightOptions.map((option) => ({ ...option, bestValue: option.domain === best.domain })); } } const options = profile.alternativeDomains.map((domain) => { const model = ALTERNATIVE_MAP[domain] ?? { merchantName: domain, trustScore: 60, baseDelta: -0.03, feeRate: 0.09, }; const altListed = roundCurrency(listedPrice * (1 + model.baseDelta)); const estimatedTrueTotal = roundCurrency(altListed * (1 + model.feeRate)); const trustTier = computeTrustTier(model.trustScore); const cheaperThanBudget = typeof budget !== "number" || estimatedTrueTotal <= budget; return { domain, merchantName: model.merchantName, listedPrice: altListed, estimatedTrueTotal, trustScore: model.trustScore, trustTier, bestValue: false, why: [ model.trustScore >= profile.trustScore ? "Higher trust profile than the current site" : "Useful as a competitive benchmark", estimatedTrueTotal < listedPrice ? "Lower expected total than the current headline price" : "Comparable pricing with clearer trust characteristics", cheaperThanBudget ? "Still inside the stated budget" : "Worth reviewing if the current site pushes you over budget", ], }; }); const best = [...options].sort((a, b) => { const scoreA = a.estimatedTrueTotal - a.trustScore * 0.45; const scoreB = b.estimatedTrueTotal - b.trustScore * 0.45; return scoreA - scoreB; })[0]; return options.map((option) => ({ ...option, bestValue: option.domain === best.domain, })); } function buildRecommendation( profile: MissionProfile, trustTier: MissionTier, sharedAnalysis: ReturnType, dealIntelligence: ReturnType, estimatedTrueTotal: number, listedPrice: number, withinBudget: boolean | null, alternatives: MissionAlternative[], ): RecommendationDetails { const bestAlternative = alternatives.find((alternative) => alternative.bestValue); const misleadingItems = dealIntelligence.items.filter( (item) => item.status === "likely_misleading" && item.confidence !== "low", ); const flags = Array.isArray(sharedAnalysis.manipulationFlags) ? sharedAnalysis.manipulationFlags : []; const hasTimerReset = flags.some((flag) => flag.type === "timer_reset"); const hasPriceInflation = flags.some((flag) => flag.type === "price_inflation"); const hasSubscriptionSteering = flags.some((flag) => flag.type === "subscription_steering"); const delta = roundCurrency(estimatedTrueTotal - listedPrice); if ( bestAlternative && bestAlternative.estimatedTrueTotal < estimatedTrueTotal * 0.95 && bestAlternative.trustScore >= profile.trustScore + 5 ) { return { action: "switch", summary: `${bestAlternative.merchantName} offers better value after fees`, reasoning: [ `${bestAlternative.merchantName} looks meaningfully cheaper after fees than ${profile.merchantName}.`, `${bestAlternative.merchantName} also carries a stronger trust score (${bestAlternative.trustScore} vs ${profile.trustScore}).`, ], nextSteps: [ `Open ${bestAlternative.merchantName} and compare the same product there.`, "Re-run Guardian Mission before buying.", ], }; } if (trustTier === "high_manipulation" || misleadingItems.length > 0 || withinBudget === false) { return { action: "switch", summary: "The current path looks too risky to trust", reasoning: [ "The current merchant or offer path shows strong manipulation or misleading-deal signals.", ...(withinBudget === false ? ["The estimated final total also exceeds the stated budget."] : []), ], nextSteps: [ "Treat the current deal claims cautiously.", "Compare at least one alternative merchant before committing.", ], }; } if (profile.trustScore >= 75 && (hasTimerReset || hasPriceInflation || hasSubscriptionSteering)) { return { action: "proceed_with_caution", summary: "Trustworthy merchant, but manipulative tactics are still present", reasoning: [ `${profile.merchantName} is generally a stronger merchant in this category.`, ...(hasTimerReset ? ["A countdown or urgency signal appears to reset, which makes the urgency hard to trust."] : []), ...(hasPriceInflation ? ["The discount framing may be overstated because the original price is too close to the current price."] : []), ...(hasSubscriptionSteering ? ["Membership or subscription benefits are being pushed before the baseline purchase path is settled."] : []), ], nextSteps: [ "Verify the final total before purchase.", "Ignore optional subscription pressure unless you truly want it.", "Refresh once and re-check whether the urgency claim remains consistent.", ], }; } if (profile.trustScore >= 75 && delta < 50 && misleadingItems.length === 0) { return { action: "proceed", summary: "Cleaner checkout path with limited hidden-cost drift", reasoning: [ "The merchant trust profile is solid for this category.", "Guardian did not find strong misleading-deal evidence on the current offer.", "The hidden-cost drift looks relatively contained.", ], nextSteps: ["Proceed, but keep Guardian active through the final payment step."], }; } return { action: "proceed_with_caution", summary: "Usable path, but some verification is still needed", reasoning: [ "The current path is not clearly unsafe, but Guardian cannot fully verify every claim yet.", "Optional extras and final checkout charges still deserve a closer look.", ], nextSteps: [ "Verify the final total at checkout.", "Avoid optional add-ons unless they match your preferences.", "Compare one alternative if the deal still feels rushed or unclear.", ], }; } export function buildAgenticMission(input: AgenticMissionRequest) { const domain = normalizeDomain(input.domain); const category = inferCategory(domain, input.category); const sharedAnalysis = analyzeCheckoutContext({ domain, url: input.url, pageText: input.pageText, priceStrings: input.priceStrings, buttonLabels: input.buttonLabels, timerElements: input.timerElements, stockAlerts: input.stockAlerts, variantUrgency: input.variantUrgency, productCandidates: input.productCandidates, }); const dealIntelligence = analyzeDealIntelligence({ domain, url: input.url, pageType: input.pageType, pageText: input.pageText, hasTimer: input.timerElements?.length ? true : undefined, timerElements: input.timerElements, stockAlerts: input.stockAlerts, productCandidates: input.productCandidates, }); const shoppingIntent = sharedAnalysis.isShoppingPage ? { isShoppingPage: true, reason: "Shared checkout pipeline classified this page as commerce." } : inferShoppingIntent(domain, input, category); if (!shoppingIntent.isShoppingPage) { return { missionId: `mission-${domain.replace(/[^a-z0-9]+/g, "-")}`, domain, url: input.url ?? `https://${domain}`, category: "non_commerce", objective: input.purchaseGoal, listedPrice: null, estimatedTrueTotal: null, savingsAtRisk: null, budget: typeof input.budget === "number" ? roundCurrency(input.budget) : null, withinBudget: null, recommendation: "proceed", commerceIntent: shoppingIntent, trust: { merchantName: domain, score: 95, tier: "gold", verdict: "This page does not look like a shopping or checkout flow, so Guardian is not estimating a purchase total here.", credibilitySignals: ["No checkout signals found", "No price or order flow detected", "Likely non-commerce page"], }, accountAutomation: { mode: "disabled", status: "Account creation is not relevant on this page.", guidance: "Guardian only activates checkout automation on pages that look like real shopping or booking flows.", }, browserAutomation: { runtime: "Chrome extension + supervised browser worker", method: "Guardian first verifies that the current page is a commerce flow before attempting any price or trust mission.", humanApprovalGates: [], }, ghostCheckout: { mode: "disabled", status: "Ghost checkout is disabled because this page is not a live product, cart, booking, or checkout flow.", revealedTotal: null, deltaFromHeadline: null, hiddenCostLanes: [], fakePressureSummary: [], saferPathMode: { available: false, label: "Stay on cheapest safe path", detail: "Guardian only enables cheapest-safe-path guidance after it finds a real commerce flow.", }, supervisedSteps: [], }, manipulativeSignals: [], alternatives: [], bestAlternative: null, actionLog: [ { title: "Mission gated", detail: `Skipped checkout mission on ${domain} because the page does not appear to be a shopping flow.`, status: "completed", }, ], nextBestAction: "Open a product, booking, cart, or checkout page before running Guardian Mission.", summary: "Guardian detected that this is not a commerce page, so it did not estimate a price or compare shopping alternatives.", recommendationDetails: { action: "proceed", summary: "No shopping flow detected", reasoning: ["Guardian did not find a live commerce flow on this page."], nextSteps: ["Open a product, booking, cart, or checkout page before running the mission again."], }, preferences: input.preferences ?? [], dealIntelligence, }; } const profile = PROFILE_MAP[domain] ?? fallbackProfile(domain, category); const listedPrice = roundCurrency( typeof input.listedPrice === "number" && Number.isFinite(input.listedPrice) ? input.listedPrice : category === "hotel" ? 165 : category === "airline" ? 189 : category === "car_rental" ? 72 : 49.99, ); const feeEstimate = estimateGhostCheckoutCosts(domain, profile, listedPrice, input.pageType); const deliveryThreshold = detectFreeDeliveryThreshold(domain, listedPrice, input.pageText); const estimatedTrueTotal = roundCurrency(listedPrice * profile.listedMarkupRate + feeEstimate.fees); const savingsAtRisk = roundCurrency(estimatedTrueTotal - listedPrice); const tier = computeTrustTier(profile.trustScore); const withinBudget = typeof input.budget !== "number" ? null : estimatedTrueTotal <= input.budget; const alternatives = input.compareAcrossSites === false ? [] : buildAlternatives(profile, listedPrice, input.budget, { isBookingPlatform: input.isBookingPlatform, bookingType: input.bookingType, route: input.route, }); const bestAlternative = alternatives.find((alternative) => alternative.bestValue); const recommendationDecision = buildRecommendation( profile, tier, sharedAnalysis, dealIntelligence, estimatedTrueTotal, listedPrice, withinBudget, alternatives, ); const recommendation = recommendationDecision.action; const accountAutomation = input.allowAccountCreation ? { mode: "allowed_with_approval", status: "Approval gate required before any signup or credential submission.", guidance: "Guardian can draft the signup path, generate an email alias, and stop for user approval before creating an account or storing session state.", } : { mode: "manual_only", status: "Account creation disabled for this mission.", guidance: "Guardian will analyze the flow, compare alternatives, and prepare the safest next step without creating an account.", }; const hiddenCostLanes: GhostCheckoutLane[] = feeEstimate.breakdown.filter((lane) => lane.amount > 0); const fakePressureSummary = [ ...(sharedAnalysis.falseUrgency.detected ? ["Flagged countdown or price-lock pressure before the real total was visible."] : []), ...(sharedAnalysis.falseScarcity.detected ? ["Flagged scarcity or crowd-pressure copy that could manufacture FOMO."] : []), ...profile.signals .filter((signal) => signal.type === "false_urgency" || signal.type === "false_scarcity") .map((signal) => signal.evidence), ].slice(0, 3); const ghostCheckout: GhostCheckoutPlan = { mode: "simulated_supervised", status: recommendation === "switch" ? "Ghost checkout found that the current path is too expensive or manipulative relative to available alternatives." : "Ghost checkout estimated the final payable amount before the shopper commits time or account details.", revealedTotal: estimatedTrueTotal, deltaFromHeadline: savingsAtRisk, hiddenCostLanes, fakePressureSummary, saferPathMode: { available: true, label: "Stay on cheapest safe path", detail: recommendation === "switch" && bestAlternative ? `Guardian would keep only the lowest-risk path active and redirect the shopper to ${bestAlternative.merchantName} after approval.` : "Guardian would keep optional add-ons off, reject upsells, and continue only on the cheapest acceptable path.", }, supervisedSteps: [ { label: "Open a parallel ghost cart", status: "completed", detail: "Mirror the checkout flow in a supervised browser context without committing payment or account creation.", }, { label: "Reveal late fees and total", status: "completed", detail: `Estimate the real total at ${roundCurrency(estimatedTrueTotal)} before the shopper invests more effort.`, }, ...(deliveryThreshold.threshold != null ? [ { label: "Check delivery threshold", status: "completed" as const, detail: deliveryThreshold.hasFreeDelivery ? `Current cart already clears the free-delivery threshold of ${deliveryThreshold.threshold}.` : `Current cart is ${deliveryThreshold.needsMore} away from the free-delivery threshold of ${deliveryThreshold.threshold}.`, }, ] : []), { label: "Strip optional extras", status: input.allowAutoDeclineUpsells ? "completed" : "planned", detail: input.allowAutoDeclineUpsells ? "Guardian is prepared to reject optional insurance, upgrades, and bundles." : "Guardian will wait for approval before auto-declining optional extras.", }, { label: "Hold approval gates", status: "watch", detail: "Credentials, account creation, merchant switching, and payment remain blocked until the user explicitly approves them.", }, ], deliveryThreshold: { threshold: deliveryThreshold.threshold, needsMore: deliveryThreshold.needsMore, hasFreeDelivery: deliveryThreshold.hasFreeDelivery, note: deliveryThreshold.note, }, platformFeeRisk: feeEstimate.platformFeeRisk, }; const actionLog: MissionAction[] = [ { title: "Mission scoped", detail: `Targeted ${profile.merchantName} for ${category.replace("_", " ")} checkout analysis with the goal: ${input.purchaseGoal}.`, status: "completed", }, { title: "Ghost checkout planned", detail: `Reveal the true total beyond the headline price by walking the checkout path and capturing mandatory fees.`, status: "completed", }, { title: "Manipulation review", detail: profile.signals.length ? `Flagged ${profile.signals.length} pressure pattern${profile.signals.length === 1 ? "" : "s"} that can distort the buying decision.` : "No major dark-pattern profile was triggered for this site archetype.", status: "completed", }, { title: "Platform fee risk", detail: `${feeEstimate.platformFeeRisk.risk.toUpperCase()} risk: ${feeEstimate.platformFeeRisk.reasoning}`, status: "completed", }, { title: "Cross-site comparison", detail: alternatives.length ? `Compared the current offer against ${alternatives.length} alternative checkout paths.` : "Alternative comparison skipped because cross-site checking was disabled.", status: alternatives.length ? "completed" : "recommended", }, { title: "Upsell stripping", detail: input.allowAutoDeclineUpsells ? "Prepared the agent to reject unnecessary add-ons and stay on the cheapest acceptable path." : "Waiting for permission before the agent auto-declines optional upsells.", status: input.allowAutoDeclineUpsells ? "completed" : "approval_required", }, { title: "Account creation guardrail", detail: accountAutomation.status, status: input.allowAccountCreation ? "approval_required" : "recommended", }, ]; const credibilityVerdict = tier === "gold" || tier === "clean" ? "This merchant looks broadly credible, but Guardian still recommends verifying the final payable amount before checkout." : tier === "neutral" ? "This merchant is usable, but the pricing path deserves a full review before trusting the headline deal." : "This merchant shows enough manipulation risk that Guardian recommends checking alternatives before committing."; return { missionId: `mission-${domain.replace(/[^a-z0-9]+/g, "-")}`, domain, url: input.url ?? `https://${domain}/checkout`, category, objective: input.purchaseGoal, listedPrice, estimatedTrueTotal, savingsAtRisk, budget: typeof input.budget === "number" ? roundCurrency(input.budget) : null, withinBudget, recommendation, commerceIntent: shoppingIntent, trust: { merchantName: profile.merchantName, score: profile.trustScore, tier, verdict: credibilityVerdict, credibilitySignals: profile.credibilitySignals, }, accountAutomation, browserAutomation: { runtime: "Chrome extension + supervised browser worker", method: "Use a persistent user-approved browser context for session continuity, then compare checkout paths with ghost-checkout reads before any buy action.", humanApprovalGates: [ "Before creating accounts or submitting credentials", "Before purchasing or saving payment methods", "Before switching to an alternative merchant", ], }, ghostCheckout, manipulativeSignals: [ ...profile.signals, ...(sharedAnalysis.falseUrgency.detected ? [ { type: "false_urgency", label: "False Urgency", severity: "high" as const, confidence: 0.82, evidence: sharedAnalysis.falseUrgency.evidence, whyItMatters: "The shopper may rush into the flow before the final price is understood.", fix: "Verify whether the timer represents a real expiring offer.", }, ] : []), ...(sharedAnalysis.falseScarcity.detected ? [ { type: "false_scarcity", label: "False Scarcity", severity: "medium" as const, confidence: 0.79, evidence: sharedAnalysis.falseScarcity.evidence, whyItMatters: "Artificial scarcity increases FOMO and pushes earlier commitment.", fix: "Validate stock claims against real inventory before trusting them.", }, ] : []), ...(sharedAnalysis.hiddenFees.detected ? [ { type: "hidden_fees", label: "Hidden Fees", severity: "high" as const, confidence: 0.84, evidence: sharedAnalysis.hiddenFees.feeItems.length ? `Shared analysis found ${sharedAnalysis.hiddenFees.feeItems.map((item) => item.label).join(", ")}.` : "Shared analysis found late fee disclosure risk.", whyItMatters: "The headline deal may materially understate the final payable amount.", fix: "Force a true-total estimate before recommending the current merchant.", }, ] : []), ...(input.variantUrgency?.length ? [ { type: "variant_pressure", label: "Variant urgency", severity: "medium" as const, confidence: 0.78, evidence: input.variantUrgency.slice(0, 2).join(" | "), whyItMatters: "Size-level scarcity can create apparel-specific FOMO that feels more real than it actually is.", fix: "Verify whether the urgency is variant-specific and persistent before treating it as a reason to rush.", }, ] : []), ], alternatives, bestAlternative: bestAlternative ?? null, actionLog, recommendationDetails: recommendationDecision, nextBestAction: recommendation === "switch" && bestAlternative ? `Switch to ${bestAlternative.merchantName} and re-run the ghost checkout there before buying.` : dealIntelligence.items.some((item) => item.status === "likely_misleading") ? "Treat the current limited deal claims as potentially misleading and compare at least one alternative before purchase." : recommendation === "proceed_with_caution" ? "Proceed only after Guardian verifies the final price and strips optional extras." : "Proceed, but keep Guardian active through the final payment step for last-second fee changes.", summary: recommendation === "switch" ? `${profile.merchantName} looks costly or manipulative relative to alternatives. Guardian recommends switching before you sink more time into this checkout.` : recommendation === "proceed_with_caution" ? `${profile.merchantName} may still be usable, but the shopper should not trust the headline price or the conversion nudges without verification. ${recommendationDecision.summary}` : `${profile.merchantName} is one of the safer available options, though Guardian should still verify the final price before purchase. ${recommendationDecision.summary}`, preferences: input.preferences ?? [], dealIntelligence, }; }