Guardian-Agent / artifacts /api-server /src /lib /booking-manipulation.ts
Prince9868's picture
Hackathon submission: Guardian OpenEnv
5743bc2
Raw
History Blame Contribute Delete
8.25 kB
export type BookingManipulation = {
type: "payment_timer" | "fake_viewers" | "seat_pressure" | "price_surge" | "dynamic_pricing";
severity: "critical" | "high" | "medium" | "low";
reality: "REAL" | "FAKE" | "UNCERTAIN";
confidence: number;
evidence: string[];
reasoning: string;
userAdvice: string;
};
type TimerData = {
hasTimer?: boolean;
timerText?: string | null;
timerValue?: string | null;
timerResetsOnRefresh?: boolean | null;
};
type PriceData = {
hasPrice?: boolean;
currentPrice?: number | null;
priceChanges?: number | null;
priceHistory?: Array<{ price?: number | null; timestamp?: number }>;
};
type HistoryData = {
viewerCountChanges?: boolean | null;
previousPrice?: number | null;
priceChanges?: number | null;
};
export function detectPaymentTimer(pageText: string, timerData?: TimerData | null, pageType?: string): BookingManipulation | null {
const source = `${pageText || ""} ${timerData?.timerText || ""}`;
const hasTimer = timerData?.hasTimer === true || /(\d+:\d+)|expires?\s*in|time.*left|hurry.*\d+\s*min/i.test(source);
if (!hasTimer) return null;
const evidence: string[] = [];
let reality: BookingManipulation["reality"] = "UNCERTAIN";
let confidence = 0.5;
if (timerData?.timerResetsOnRefresh === true) {
reality = "FAKE";
confidence = 0.95;
evidence.push("Timer resets to the same value on refresh.");
}
const timerMatch = source.match(/(\d+):(\d+)/);
if (timerMatch) {
const minutes = Number(timerMatch[1]);
if (minutes < 5 && reality !== "FAKE") {
reality = "FAKE";
confidence = 0.8;
evidence.push(`Timer is unusually short at ${timerMatch[0]}.`);
} else if (minutes >= 10 && minutes <= 20 && reality === "UNCERTAIN") {
reality = "REAL";
confidence = 0.7;
evidence.push("Timer length looks consistent with a real payment session timeout.");
}
}
if (pageType && pageType !== "payment" && reality !== "FAKE") {
reality = "FAKE";
confidence = Math.max(confidence, 0.85);
evidence.push("Timer appears before the payment stage, which is typical of manipulative pressure.");
}
return {
type: "payment_timer",
severity: reality === "FAKE" ? "high" : "medium",
reality,
confidence,
evidence,
reasoning:
reality === "FAKE"
? "This timer looks like artificial urgency rather than a genuine payment timeout."
: reality === "REAL"
? "This timer may be tied to a legitimate payment or reservation hold window."
: "Guardian could not fully verify whether this timer is real or artificial.",
userAdvice:
reality === "FAKE"
? "Ignore the timer and refresh once to confirm it resets."
: reality === "REAL"
? "Treat the timer as real and finish payment only if you are ready."
: "Refresh once and compare the timer value before trusting it.",
};
}
export function detectFakeViewers(pageText: string, historyData?: HistoryData | null): BookingManipulation | null {
const match =
pageText.match(/(\d+)\s*people?\s*(viewing|looking\s*at|watching)/i) ||
pageText.match(/(\d+)\s*others?\s*are\s*considering/i) ||
pageText.match(/(\d+)\s*travelers?\s*booked/i);
if (!match) return null;
const viewerCount = Number(match[1] || 0);
const evidence = [`Viewer pressure copy detected: ${match[0]}`];
let reality: BookingManipulation["reality"] = "UNCERTAIN";
let confidence = 0.5;
if (historyData?.viewerCountChanges) {
reality = "FAKE";
confidence = 0.95;
evidence.push("Viewer count changed across refreshes.");
} else if (viewerCount > 100 || viewerCount % 5 === 0) {
reality = "FAKE";
confidence = 0.72;
evidence.push("Viewer count looks suspiciously rounded or unrealistically high.");
}
return {
type: "fake_viewers",
severity: reality === "FAKE" ? "medium" : "low",
reality,
confidence,
evidence,
reasoning:
reality === "FAKE"
? "This viewer count likely exists to create competition pressure rather than reflect real users."
: "Guardian cannot verify whether the viewer count is genuinely live.",
userAdvice: reality === "FAKE" ? "Ignore the viewer count and focus on price and availability." : "Refresh once to see whether the count changes randomly.",
};
}
export function detectSeatPressure(pageText: string, bookingType: string): BookingManipulation | null {
const match =
pageText.match(/only\s*(\d+)\s*(seat|room|table|spot|ticket)s?\s*left/i) ||
pageText.match(/(\d+)\s*(seat|room|table)s?\s*remaining/i) ||
pageText.match(/last\s*(\d+)\s*(available|left)/i) ||
pageText.match(/almost\s*sold\s*out|high\s*demand|selling\s*fast/i);
if (!match) return null;
const count = match[1] ? Number(match[1]) : null;
const evidence = [`Scarcity claim detected: ${match[0]}`];
let reality: BookingManipulation["reality"] = count == null ? "FAKE" : "UNCERTAIN";
let confidence = count == null ? 0.7 : 0.55;
if (count != null) {
reality = "REAL";
confidence = 0.6;
if (bookingType === "bus" && count > 20) {
reality = "FAKE";
confidence = 0.72;
evidence.push("Claimed scarcity does not look meaningful for a bus-sized inventory.");
}
} else {
evidence.push("The claim is vague rather than based on a specific count.");
}
return {
type: "seat_pressure",
severity: reality === "FAKE" ? "medium" : "low",
reality,
confidence,
evidence,
reasoning:
reality === "FAKE"
? "The scarcity claim appears exaggerated or too vague to trust."
: "The scarcity claim is more specific and may reflect real inventory pressure.",
userAdvice: reality === "FAKE" ? "Do not rush solely because of this scarcity banner." : "Cross-check availability if the booking matters and the count is low.",
};
}
export function detectDynamicPricing(currentPrice?: number | null, historyData?: HistoryData | null): BookingManipulation | null {
if (typeof currentPrice !== "number" || typeof historyData?.previousPrice !== "number" || historyData.previousPrice <= 0) {
return null;
}
const percentChange = ((currentPrice - historyData.previousPrice) / historyData.previousPrice) * 100;
if (Math.abs(percentChange) < 2) {
return null;
}
const reality: BookingManipulation["reality"] = percentChange > 0 ? "REAL" : "UNCERTAIN";
const confidence = percentChange > 10 || (historyData.priceChanges || 0) > 2 ? 0.85 : 0.65;
const evidence = [`Price changed by ${percentChange.toFixed(1)}% compared with the previous observed value.`];
return {
type: percentChange > 0 ? "price_surge" : "dynamic_pricing",
severity: percentChange > 10 ? "high" : "medium",
reality,
confidence,
evidence,
reasoning:
percentChange > 0
? "Dynamic pricing appears active, so the price may continue to move."
: "Price movement was detected, but Guardian cannot confirm whether it is a meaningful surge.",
userAdvice:
percentChange > 0
? "Compare across platforms and decide whether the increase is worth accepting."
: "Track one more refresh before treating this as a real trend.",
};
}
export function analyzeBookingManipulation(input: {
pageText: string;
bookingType: string;
pageType?: string;
timer?: TimerData | null;
viewers?: { countChanges?: boolean | null } | null;
seats?: { hasScarcity?: boolean; text?: string | null } | null;
price?: PriceData | null;
}) {
const historyData: HistoryData = {
viewerCountChanges: input.viewers?.countChanges ?? null,
previousPrice:
Array.isArray(input.price?.priceHistory) && input.price.priceHistory.length > 1
? (input.price.priceHistory[input.price.priceHistory.length - 2]?.price ?? null)
: null,
priceChanges: input.price?.priceChanges ?? null,
};
return [
detectPaymentTimer(input.pageText, input.timer, input.pageType),
detectFakeViewers(input.pageText, historyData),
detectSeatPressure(`${input.pageText} ${input.seats?.text || ""}`, input.bookingType),
detectDynamicPricing(input.price?.currentPrice ?? null, historyData),
].filter(Boolean) as BookingManipulation[];
}