← back to Govarbitrage
src/agents/scout.ts
50 lines
import { prisma } from "@/lib/db";
import { runAgent, type AgentRunResult } from "./framework";
// The Scout watches the clock: open listings closing within 48h that carry a
// strong Overall Opportunity score (>= minScore). Each hit is an INFO finding;
// anything closing within 6h escalates to CRITICAL.
const HOUR = 3_600_000;
export async function runScout(minScore = 75): Promise<AgentRunResult> {
return runAgent("scout", async (ctx) => {
const now = Date.now();
const listings = await prisma.listing.findMany({
where: {
listingStatus: "ACTIVE",
closingAt: { gt: new Date(now), lte: new Date(now + 48 * HOUR) },
scores: { some: { profile: "OVERALL_OPPORTUNITY", value: { gte: minScore } } },
},
include: {
scores: { where: { profile: "OVERALL_OPPORTUNITY" } },
costBreakdown: true,
},
orderBy: { closingAt: "asc" },
});
let critical = 0;
for (const l of listings) {
ctx.count();
const score = l.scores[0]?.value ?? 0;
const hoursLeft = (new Date(l.closingAt!).getTime() - now) / HOUR;
const isCritical = hoursLeft <= 6;
if (isCritical) critical++;
const net = l.costBreakdown ? Math.round(Number(l.costBreakdown.expectedNetProfit)) : null;
await ctx.finding({
kind: "HOT_CLOSING",
severity: isCritical ? "CRITICAL" : "INFO",
listingId: l.id,
listingTitle: l.title,
title: `Closing in ${hoursLeft < 1 ? "<1h" : Math.round(hoursLeft) + "h"} @ score ${Math.round(score)}: "${l.title.slice(0, 70)}"`,
detail:
`Overall ${Math.round(score)}/100, closes ${l.closingAt?.toISOString()}` +
(net != null ? `, est. net $${net}` : "") +
`. ${isCritical ? "Under 6 hours — act now or drop it." : "Within 48h window."}`,
});
}
return `${listings.length} hot deals closing within 48h (${critical} within 6h) at score >= ${minScore}`;
});
}