← 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}`;
  });
}