← back to Govarbitrage
Phase 3: local-Ollama AI identification, research pipeline, 25-listing seed (full research/costs/scores)
23bcc51f56b864725af8ad4eb08ed7259ade5eca · 2026-07-09 22:32:31 -0700 · Steve Abrams
Files touched
A prisma/seed.tsA src/engines/demand.tsA src/lib/ai.tsA src/pipeline/research.ts
Diff
commit 23bcc51f56b864725af8ad4eb08ed7259ade5eca
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 9 22:32:31 2026 -0700
Phase 3: local-Ollama AI identification, research pipeline, 25-listing seed (full research/costs/scores)
---
prisma/seed.ts | 810 +++++++++++++++++++++++++++++++++++++++++++++++
src/engines/demand.ts | 73 +++++
src/lib/ai.ts | 122 +++++++
src/pipeline/research.ts | 273 ++++++++++++++++
4 files changed, 1278 insertions(+)
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 0000000..66c4f43
--- /dev/null
+++ b/prisma/seed.ts
@@ -0,0 +1,810 @@
+import { PrismaClient, type AuctionSource, type Condition } from "@prisma/client";
+import { runResearch } from "../src/pipeline/research";
+
+const prisma = new PrismaClient();
+
+const day = 86_400_000;
+const soon = (days: number) => new Date(Date.now() + days * day);
+const ago = (days: number) => new Date(Date.now() - days * day);
+const img = (seed: string) => [
+ `https://picsum.photos/seed/${seed}a/600/400`,
+ `https://picsum.photos/seed/${seed}b/600/400`,
+];
+
+interface Seed {
+ source: AuctionSource;
+ sourceAuctionId: string;
+ sourceUrl: string;
+ title: string;
+ description: string;
+ category: string;
+ manufacturer?: string;
+ model?: string;
+ condition: Condition;
+ quantity: number;
+ weightLbs: number;
+ dimensions?: string;
+ city: string;
+ state: string;
+ zip: string;
+ currentBid: number;
+ bidCount: number;
+ closesInDays: number;
+ terms: string;
+ seed: string;
+ anchor: { newRetail: number; demandScore: number };
+ comparables: {
+ kind: "SOLD" | "ACTIVE" | "RETAIL";
+ title: string;
+ price: number;
+ url?: string;
+ source?: string;
+ soldAt?: Date;
+ }[];
+}
+
+const LISTINGS: Seed[] = [
+ {
+ source: "GOVDEALS",
+ sourceAuctionId: "GD-448120",
+ sourceUrl: "https://www.govdeals.com/index.cfm?fa=Main.Item&itemid=448120",
+ title: "A-dec 511 Dental Patient Chair with Delivery System",
+ description:
+ "Surplus dental operatory chair, A-dec 511, powered recline, foot control, delivery unit. Removed from county health clinic. Functional, cosmetic wear.",
+ category: "Medical / Dental Equipment",
+ manufacturer: "A-dec",
+ model: "511",
+ condition: "USED_GOOD",
+ quantity: 1,
+ weightLbs: 320,
+ dimensions: '60 x 30 x 44 in',
+ city: "Sacramento",
+ state: "CA",
+ zip: "95814",
+ currentBid: 500,
+ bidCount: 3,
+ closesInDays: 2,
+ terms: "Pickup only. Payment within 5 business days. Buyer responsible for removal.",
+ seed: "adec511",
+ anchor: { newRetail: 20000, demandScore: 74 },
+ comparables: [
+ { kind: "RETAIL", title: "A-dec 511 new operatory package", price: 21500, source: "adec.com", url: "https://www.a-dec.com" },
+ { kind: "SOLD", title: "A-dec 511 chair + delivery (used)", price: 6800, source: "ebay", soldAt: ago(21) },
+ { kind: "SOLD", title: "A-dec 500 series used operatory", price: 5900, source: "dentalplanet", soldAt: ago(40) },
+ { kind: "ACTIVE", title: "A-dec 511 refurb listed", price: 9200, source: "ebay" },
+ ],
+ },
+ {
+ source: "PUBLIC_SURPLUS",
+ sourceAuctionId: "PS-99120",
+ sourceUrl: "https://www.publicsurplus.com/sms/auction/view?auc=99120",
+ title: "Lot of 12 Apple MacBook Pro 14 M-series Laptops",
+ description:
+ "12x MacBook Pro 14-inch, university IT refresh. Wiped, functional, minor scuffs. Chargers included for 9 of 12.",
+ category: "Computers / Laptops",
+ manufacturer: "Apple",
+ model: "MacBook Pro 14",
+ condition: "USED_GOOD",
+ quantity: 12,
+ weightLbs: 45,
+ dimensions: "pallet",
+ city: "Madison",
+ state: "WI",
+ zip: "53706",
+ currentBid: 3200,
+ bidCount: 11,
+ closesInDays: 1,
+ terms: "Shipping available at buyer expense. Payment via PayPal.",
+ seed: "mbp14",
+ anchor: { newRetail: 1999, demandScore: 88 },
+ comparables: [
+ { kind: "SOLD", title: "MacBook Pro 14 M-series used", price: 1150, source: "ebay", soldAt: ago(9) },
+ { kind: "ACTIVE", title: "MacBook Pro 14 refurb", price: 1399, source: "ebay" },
+ { kind: "RETAIL", title: "MacBook Pro 14 new", price: 1999, source: "apple.com" },
+ ],
+ },
+ {
+ source: "GSA_AUCTIONS",
+ sourceAuctionId: "GSA-7781",
+ sourceUrl: "https://gsaauctions.gov/auctions/7781",
+ title: "Herman Miller Aeron Chairs (Qty 25)",
+ description: "Federal office closure. 25 Aeron size B chairs, mixed condition, most fully functional.",
+ category: "Office Furniture",
+ manufacturer: "Herman Miller",
+ model: "Aeron B",
+ condition: "USED_GOOD",
+ quantity: 25,
+ weightLbs: 750,
+ city: "Washington",
+ state: "DC",
+ zip: "20405",
+ currentBid: 1500,
+ bidCount: 7,
+ closesInDays: 4,
+ terms: "Pickup only. Government surplus, no buyer premium (GSA).",
+ seed: "aeron",
+ anchor: { newRetail: 1395, demandScore: 68 },
+ comparables: [
+ { kind: "SOLD", title: "Aeron B used", price: 480, source: "ebay", soldAt: ago(12) },
+ { kind: "ACTIVE", title: "Aeron B refurb", price: 695, source: "facebook" },
+ { kind: "RETAIL", title: "Aeron new", price: 1395, source: "hermanmiller.com" },
+ ],
+ },
+ {
+ source: "STATE_SURPLUS",
+ sourceAuctionId: "TX-33112",
+ sourceUrl: "https://www.tfc.texas.gov/surplus/33112",
+ title: "Toyota 8FGCU25 Forklift 5000lb LP",
+ description: "State facilities forklift, 5000 lb capacity, LP, ~6800 hours. Runs and lifts.",
+ category: "Industrial / Material Handling",
+ manufacturer: "Toyota",
+ model: "8FGCU25",
+ condition: "USED_FAIR",
+ quantity: 1,
+ weightLbs: 8200,
+ city: "Austin",
+ state: "TX",
+ zip: "78701",
+ currentBid: 4200,
+ bidCount: 9,
+ closesInDays: 3,
+ terms: "Pickup only. Heavy equipment, buyer arranges transport.",
+ seed: "forklift",
+ anchor: { newRetail: 32000, demandScore: 66 },
+ comparables: [
+ { kind: "SOLD", title: "Toyota 8FGCU25 used", price: 14500, source: "machinerytrader", soldAt: ago(30) },
+ { kind: "ACTIVE", title: "Toyota 5k LP forklift", price: 16900, source: "ebay" },
+ ],
+ },
+ {
+ source: "UNIVERSITY_SURPLUS",
+ sourceAuctionId: "UW-5521",
+ sourceUrl: "https://surplus.uw.edu/5521",
+ title: "Nikon Eclipse Ci Laboratory Microscope",
+ description: "University lab surplus. Nikon Eclipse Ci upright microscope, objectives included. Functional.",
+ category: "Laboratory Equipment",
+ manufacturer: "Nikon",
+ model: "Eclipse Ci",
+ condition: "LIKE_NEW",
+ quantity: 1,
+ weightLbs: 35,
+ city: "Seattle",
+ state: "WA",
+ zip: "98195",
+ currentBid: 900,
+ bidCount: 4,
+ closesInDays: 5,
+ terms: "Shipping available. Pickup preferred.",
+ seed: "nikonci",
+ anchor: { newRetail: 9500, demandScore: 68 },
+ comparables: [
+ { kind: "SOLD", title: "Nikon Eclipse Ci used", price: 4200, source: "ebay", soldAt: ago(25) },
+ { kind: "ACTIVE", title: "Nikon Eclipse Ci-L", price: 5600, source: "labx" },
+ { kind: "RETAIL", title: "Nikon Eclipse Ci new config", price: 9500, source: "nikon" },
+ ],
+ },
+ {
+ source: "COUNTY",
+ sourceAuctionId: "CTY-2201",
+ sourceUrl: "https://county.example.gov/auctions/2201",
+ title: "Cisco Catalyst 9300 48-port Switches (Lot of 6)",
+ description: "County IT surplus. 6x Cisco Catalyst 9300 48-port. Wiped to ROMMON. Powered on before removal.",
+ category: "Networking",
+ manufacturer: "Cisco",
+ model: "Catalyst 9300-48",
+ condition: "USED_GOOD",
+ quantity: 6,
+ weightLbs: 60,
+ city: "San Diego",
+ state: "CA",
+ zip: "92101",
+ currentBid: 700,
+ bidCount: 5,
+ closesInDays: 2,
+ terms: "Shipping available at buyer expense.",
+ seed: "cat9300",
+ anchor: { newRetail: 4800, demandScore: 64 },
+ comparables: [
+ { kind: "SOLD", title: "Catalyst 9300-48 used", price: 850, source: "ebay", soldAt: ago(7) },
+ { kind: "ACTIVE", title: "C9300-48P refurb", price: 1200, source: "ebay" },
+ ],
+ },
+ {
+ source: "GOVDEALS",
+ sourceAuctionId: "GD-451900",
+ sourceUrl: "https://www.govdeals.com/index.cfm?fa=Main.Item&itemid=451900",
+ title: "DeWalt 20V MAX Tool Lot (Drills, Impact, Saws)",
+ description: "Municipal maintenance shop surplus. Mixed DeWalt 20V tools, some with batteries. Used, working.",
+ category: "Power Tools",
+ manufacturer: "DeWalt",
+ model: "20V MAX",
+ condition: "USED_GOOD",
+ quantity: 14,
+ weightLbs: 55,
+ city: "Columbus",
+ state: "OH",
+ zip: "43215",
+ currentBid: 220,
+ bidCount: 6,
+ closesInDays: 1,
+ terms: "Pickup or shipping. Payment within 5 days.",
+ seed: "dewalt",
+ anchor: { newRetail: 2400, demandScore: 58 },
+ comparables: [
+ { kind: "SOLD", title: "DeWalt 20V tool lot used", price: 900, source: "ebay", soldAt: ago(14) },
+ { kind: "ACTIVE", title: "DeWalt 20V combo kit", price: 1200, source: "ebay" },
+ ],
+ },
+ {
+ source: "PUBLIC_SURPLUS",
+ sourceAuctionId: "PS-99870",
+ sourceUrl: "https://www.publicsurplus.com/sms/auction/view?auc=99870",
+ title: "Epson Pro L1500 Laser Projector",
+ description: "Auditorium projector, Epson Pro L1500UH, ~2100 lamp hours (laser). Functional, includes lens.",
+ category: "AV / Projectors",
+ manufacturer: "Epson",
+ model: "Pro L1500UH",
+ condition: "USED_GOOD",
+ quantity: 1,
+ weightLbs: 55,
+ city: "Phoenix",
+ state: "AZ",
+ zip: "85003",
+ currentBid: 650,
+ bidCount: 3,
+ closesInDays: 6,
+ terms: "Shipping available.",
+ seed: "epsonl1500",
+ anchor: { newRetail: 11000, demandScore: 60 },
+ comparables: [
+ { kind: "SOLD", title: "Epson Pro L1500UH used", price: 3800, source: "ebay", soldAt: ago(35) },
+ { kind: "ACTIVE", title: "Epson L1505 laser", price: 5200, source: "ebay" },
+ ],
+ },
+ {
+ source: "GSA_AUCTIONS",
+ sourceAuctionId: "GSA-8890",
+ sourceUrl: "https://gsaauctions.gov/auctions/8890",
+ title: "DJI Matrice 300 RTK Drone with Payloads",
+ description: "Federal agency surplus UAV, DJI Matrice 300 RTK, 2 batteries, H20T payload. Flight-tested.",
+ category: "Drones / UAV",
+ manufacturer: "DJI",
+ model: "Matrice 300 RTK",
+ condition: "LIKE_NEW",
+ quantity: 1,
+ weightLbs: 40,
+ city: "Denver",
+ state: "CO",
+ zip: "80202",
+ currentBid: 3800,
+ bidCount: 8,
+ closesInDays: 3,
+ terms: "Pickup or shipping. No buyer premium (GSA).",
+ seed: "matrice300",
+ anchor: { newRetail: 21000, demandScore: 72 },
+ comparables: [
+ { kind: "SOLD", title: "DJI M300 RTK + H20T used", price: 9800, source: "ebay", soldAt: ago(18) },
+ { kind: "ACTIVE", title: "DJI Matrice 300 RTK", price: 12500, source: "ebay" },
+ ],
+ },
+ {
+ source: "STATE_SURPLUS",
+ sourceAuctionId: "CA-DGS-6612",
+ sourceUrl: "https://www.dgs.ca.gov/surplus/6612",
+ title: "Dell PowerEdge R750 Servers (Lot of 4)",
+ description: "State data center refresh. 4x Dell PowerEdge R750, dual Xeon, drives pulled. Powered before removal.",
+ category: "Servers",
+ manufacturer: "Dell",
+ model: "PowerEdge R750",
+ condition: "USED_GOOD",
+ quantity: 4,
+ weightLbs: 130,
+ city: "Sacramento",
+ state: "CA",
+ zip: "95814",
+ currentBid: 1100,
+ bidCount: 6,
+ closesInDays: 4,
+ terms: "Shipping available at buyer expense.",
+ seed: "r750",
+ anchor: { newRetail: 9000, demandScore: 62 },
+ comparables: [
+ { kind: "SOLD", title: "Dell R750 barebones used", price: 2200, source: "ebay", soldAt: ago(11) },
+ { kind: "ACTIVE", title: "PowerEdge R750 config", price: 3100, source: "ebay" },
+ ],
+ },
+ {
+ source: "COUNTY",
+ sourceAuctionId: "CTY-3040",
+ sourceUrl: "https://county.example.gov/auctions/3040",
+ title: "Steelcase Leap V2 Chairs (Lot of 30)",
+ description: "County office surplus, 30x Steelcase Leap V2. Used, functional, mixed upholstery wear.",
+ category: "Office Furniture",
+ manufacturer: "Steelcase",
+ model: "Leap V2",
+ condition: "USED_GOOD",
+ quantity: 30,
+ weightLbs: 900,
+ city: "Portland",
+ state: "OR",
+ zip: "97204",
+ currentBid: 900,
+ bidCount: 4,
+ closesInDays: 5,
+ terms: "Pickup only.",
+ seed: "leapv2",
+ anchor: { newRetail: 1100, demandScore: 60 },
+ comparables: [
+ { kind: "SOLD", title: "Steelcase Leap V2 used", price: 320, source: "ebay", soldAt: ago(8) },
+ { kind: "ACTIVE", title: "Leap V2 refurb", price: 520, source: "facebook" },
+ ],
+ },
+ {
+ source: "UNIVERSITY_SURPLUS",
+ sourceAuctionId: "MSU-7710",
+ sourceUrl: "https://surplus.msu.edu/7710",
+ title: "Thermo Scientific Sorvall Legend X1R Centrifuge",
+ description: "University lab surplus centrifuge, refrigerated, rotor included. Functional.",
+ category: "Laboratory Equipment",
+ manufacturer: "Thermo Scientific",
+ model: "Sorvall Legend X1R",
+ condition: "USED_GOOD",
+ quantity: 1,
+ weightLbs: 190,
+ city: "East Lansing",
+ state: "MI",
+ zip: "48824",
+ currentBid: 800,
+ bidCount: 2,
+ closesInDays: 7,
+ terms: "Pickup only, freight arrangeable.",
+ seed: "sorvall",
+ anchor: { newRetail: 14000, demandScore: 65 },
+ comparables: [
+ { kind: "SOLD", title: "Sorvall Legend X1R used", price: 3600, source: "labx", soldAt: ago(28) },
+ { kind: "ACTIVE", title: "Legend X1R refrigerated", price: 5400, source: "ebay" },
+ ],
+ },
+ {
+ source: "GOVDEALS",
+ sourceAuctionId: "GD-460210",
+ sourceUrl: "https://www.govdeals.com/index.cfm?fa=Main.Item&itemid=460210",
+ title: "2015 Ford F-250 Super Duty Utility Truck",
+ description: "Municipal fleet retirement. F-250 XL, 118k miles, utility bed. Runs, drives, service records.",
+ category: "Vehicles",
+ manufacturer: "Ford",
+ model: "F-250 Super Duty",
+ condition: "USED_FAIR",
+ quantity: 1,
+ weightLbs: 6800,
+ city: "Tucson",
+ state: "AZ",
+ zip: "85701",
+ currentBid: 8200,
+ bidCount: 14,
+ closesInDays: 2,
+ terms: "Pickup only. Title transfer. As-is.",
+ seed: "f250",
+ anchor: { newRetail: 42000, demandScore: 50 },
+ comparables: [
+ { kind: "SOLD", title: "2015 F-250 utility 118k", price: 16500, source: "kbb", soldAt: ago(20) },
+ { kind: "ACTIVE", title: "2015 F-250 XL", price: 18900, source: "autotrader" },
+ ],
+ },
+ {
+ source: "PUBLIC_SURPLUS",
+ sourceAuctionId: "PS-100240",
+ sourceUrl: "https://www.publicsurplus.com/sms/auction/view?auc=100240",
+ title: "Lot of 40 Dell Latitude 7440 Laptops",
+ description: "School district refresh, 40x Dell Latitude 7440, i5, wiped. Working, cosmetic wear.",
+ category: "Computers / Laptops",
+ manufacturer: "Dell",
+ model: "Latitude 7440",
+ condition: "USED_GOOD",
+ quantity: 40,
+ weightLbs: 120,
+ city: "Orlando",
+ state: "FL",
+ zip: "32801",
+ currentBid: 2600,
+ bidCount: 10,
+ closesInDays: 1,
+ terms: "Pickup or shipping.",
+ seed: "lat7440",
+ anchor: { newRetail: 1400, demandScore: 70 },
+ comparables: [
+ { kind: "SOLD", title: "Dell Latitude 7440 used", price: 430, source: "ebay", soldAt: ago(6) },
+ { kind: "ACTIVE", title: "Latitude 7440 i5", price: 560, source: "ebay" },
+ ],
+ },
+ {
+ source: "GSA_AUCTIONS",
+ sourceAuctionId: "GSA-9021",
+ sourceUrl: "https://gsaauctions.gov/auctions/9021",
+ title: "Midmark 625 Barrier-Free Exam Table",
+ description: "Federal clinic surplus exam table, Midmark 625, powered. Functional.",
+ category: "Medical / Dental Equipment",
+ manufacturer: "Midmark",
+ model: "625",
+ condition: "USED_GOOD",
+ quantity: 1,
+ weightLbs: 350,
+ city: "Kansas City",
+ state: "MO",
+ zip: "64106",
+ currentBid: 450,
+ bidCount: 2,
+ closesInDays: 6,
+ terms: "Pickup only.",
+ seed: "midmark625",
+ anchor: { newRetail: 9800, demandScore: 66 },
+ comparables: [
+ { kind: "SOLD", title: "Midmark 625 used", price: 2900, source: "ebay", soldAt: ago(22) },
+ { kind: "ACTIVE", title: "Midmark 625 barrier-free", price: 4200, source: "dotmed" },
+ ],
+ },
+ {
+ source: "STATE_SURPLUS",
+ sourceAuctionId: "TX-34550",
+ sourceUrl: "https://www.tfc.texas.gov/surplus/34550",
+ title: "Miller Big Blue 400 Pro Welder/Generator",
+ description: "State DOT surplus, Miller Big Blue 400 Pro diesel welder/generator. Runs.",
+ category: "Industrial / Welding",
+ manufacturer: "Miller",
+ model: "Big Blue 400 Pro",
+ condition: "USED_FAIR",
+ quantity: 1,
+ weightLbs: 1100,
+ city: "Houston",
+ state: "TX",
+ zip: "77002",
+ currentBid: 2400,
+ bidCount: 5,
+ closesInDays: 3,
+ terms: "Pickup only.",
+ seed: "bigblue",
+ anchor: { newRetail: 13500, demandScore: 62 },
+ comparables: [
+ { kind: "SOLD", title: "Miller Big Blue 400 used", price: 6200, source: "machinerytrader", soldAt: ago(33) },
+ { kind: "ACTIVE", title: "Big Blue 400 Pro", price: 7800, source: "ebay" },
+ ],
+ },
+ {
+ source: "COUNTY",
+ sourceAuctionId: "CTY-3311",
+ sourceUrl: "https://county.example.gov/auctions/3311",
+ title: "Zebra ZT610 Industrial Label Printers (Lot of 8)",
+ description: "County warehouse surplus, 8x Zebra ZT610 thermal printers. Working, some worn platens.",
+ category: "Industrial / Printing",
+ manufacturer: "Zebra",
+ model: "ZT610",
+ condition: "USED_GOOD",
+ quantity: 8,
+ weightLbs: 190,
+ city: "Charlotte",
+ state: "NC",
+ zip: "28202",
+ currentBid: 600,
+ bidCount: 3,
+ closesInDays: 4,
+ terms: "Shipping available.",
+ seed: "zt610",
+ anchor: { newRetail: 3200, demandScore: 58 },
+ comparables: [
+ { kind: "SOLD", title: "Zebra ZT610 used", price: 850, source: "ebay", soldAt: ago(16) },
+ { kind: "ACTIVE", title: "ZT610 203dpi", price: 1300, source: "ebay" },
+ ],
+ },
+ {
+ source: "UNIVERSITY_SURPLUS",
+ sourceAuctionId: "UCD-8802",
+ sourceUrl: "https://surplus.ucdavis.edu/8802",
+ title: "Agilent 1260 Infinity II HPLC System",
+ description: "University lab surplus HPLC, Agilent 1260 Infinity II, modules included. Powered before removal, untested flow.",
+ category: "Laboratory Equipment",
+ manufacturer: "Agilent",
+ model: "1260 Infinity II",
+ condition: "UNKNOWN",
+ quantity: 1,
+ weightLbs: 160,
+ city: "Davis",
+ state: "CA",
+ zip: "95616",
+ currentBid: 1500,
+ bidCount: 4,
+ closesInDays: 8,
+ terms: "Pickup only, freight arrangeable. Sold as-is, untested.",
+ seed: "hplc1260",
+ anchor: { newRetail: 45000, demandScore: 67 },
+ comparables: [
+ { kind: "SOLD", title: "Agilent 1260 Infinity II used", price: 9500, source: "labx", soldAt: ago(45) },
+ { kind: "ACTIVE", title: "1260 Infinity II stack", price: 14500, source: "ebay" },
+ ],
+ },
+ {
+ source: "GOVDEALS",
+ sourceAuctionId: "GD-462800",
+ sourceUrl: "https://www.govdeals.com/index.cfm?fa=Main.Item&itemid=462800",
+ title: "Genie GS-1930 Scissor Lift",
+ description: "City facilities surplus, Genie GS-1930 electric scissor lift, 19ft. Charges and lifts.",
+ category: "Industrial / Aerial",
+ manufacturer: "Genie",
+ model: "GS-1930",
+ condition: "USED_FAIR",
+ quantity: 1,
+ weightLbs: 2800,
+ city: "Reno",
+ state: "NV",
+ zip: "89501",
+ currentBid: 1900,
+ bidCount: 7,
+ closesInDays: 2,
+ terms: "Pickup only.",
+ seed: "gs1930",
+ anchor: { newRetail: 16000, demandScore: 63 },
+ comparables: [
+ { kind: "SOLD", title: "Genie GS-1930 used", price: 6500, source: "machinerytrader", soldAt: ago(27) },
+ { kind: "ACTIVE", title: "GS-1930 scissor lift", price: 8200, source: "ebay" },
+ ],
+ },
+ {
+ source: "PUBLIC_SURPLUS",
+ sourceAuctionId: "PS-101100",
+ sourceUrl: "https://www.publicsurplus.com/sms/auction/view?auc=101100",
+ title: "Lot of 20 iPad 9th Gen Tablets",
+ description: "School district surplus, 20x iPad 9th gen, wiped. Working, some screen scratches.",
+ category: "Tablets",
+ manufacturer: "Apple",
+ model: "iPad 9th Gen",
+ condition: "USED_GOOD",
+ quantity: 20,
+ weightLbs: 25,
+ city: "San Antonio",
+ state: "TX",
+ zip: "78205",
+ currentBid: 900,
+ bidCount: 9,
+ closesInDays: 1,
+ terms: "Shipping available.",
+ seed: "ipad9",
+ anchor: { newRetail: 329, demandScore: 80 },
+ comparables: [
+ { kind: "SOLD", title: "iPad 9th gen used", price: 165, source: "ebay", soldAt: ago(5) },
+ { kind: "ACTIVE", title: "iPad 9th gen wifi", price: 210, source: "ebay" },
+ ],
+ },
+ {
+ source: "GSA_AUCTIONS",
+ sourceAuctionId: "GSA-9330",
+ sourceUrl: "https://gsaauctions.gov/auctions/9330",
+ title: "FLIR T540 Thermal Imaging Camera",
+ description: "Federal surplus thermal camera, FLIR T540, 464x348, case + lenses. Calibrated in-service.",
+ category: "Test Equipment",
+ manufacturer: "FLIR",
+ model: "T540",
+ condition: "LIKE_NEW",
+ quantity: 1,
+ weightLbs: 12,
+ city: "Atlanta",
+ state: "GA",
+ zip: "30303",
+ currentBid: 2100,
+ bidCount: 6,
+ closesInDays: 3,
+ terms: "Shipping available. No buyer premium (GSA).",
+ seed: "flirt540",
+ anchor: { newRetail: 13000, demandScore: 69 },
+ comparables: [
+ { kind: "SOLD", title: "FLIR T540 used", price: 5200, source: "ebay", soldAt: ago(19) },
+ { kind: "ACTIVE", title: "FLIR T540 42deg", price: 6900, source: "ebay" },
+ ],
+ },
+ {
+ source: "COUNTY",
+ sourceAuctionId: "CTY-3520",
+ sourceUrl: "https://county.example.gov/auctions/3520",
+ title: "Toro Groundsmaster 4000-D Mower",
+ description: "County parks surplus, Toro Groundsmaster 4000-D diesel rotary mower, ~3400 hrs. Runs, mows.",
+ category: "Grounds / Mowers",
+ manufacturer: "Toro",
+ model: "Groundsmaster 4000-D",
+ condition: "USED_FAIR",
+ quantity: 1,
+ weightLbs: 3200,
+ city: "Fresno",
+ state: "CA",
+ zip: "93721",
+ currentBid: 3600,
+ bidCount: 5,
+ closesInDays: 5,
+ terms: "Pickup only.",
+ seed: "toro4000",
+ anchor: { newRetail: 68000, demandScore: 55 },
+ comparables: [
+ { kind: "SOLD", title: "Toro GM 4000-D used", price: 12500, source: "machinerytrader", soldAt: ago(38) },
+ { kind: "ACTIVE", title: "Groundsmaster 4000-D", price: 16900, source: "ebay" },
+ ],
+ },
+ {
+ source: "STATE_SURPLUS",
+ sourceAuctionId: "CA-DGS-6720",
+ sourceUrl: "https://www.dgs.ca.gov/surplus/6720",
+ title: "Lot of 50 Poly VVX 450 Desk Phones",
+ description: "State office surplus, 50x Poly VVX 450 IP phones with handsets. Working.",
+ category: "Telecom",
+ manufacturer: "Poly",
+ model: "VVX 450",
+ condition: "USED_GOOD",
+ quantity: 50,
+ weightLbs: 90,
+ city: "Sacramento",
+ state: "CA",
+ zip: "95814",
+ currentBid: 300,
+ bidCount: 2,
+ closesInDays: 6,
+ terms: "Shipping available.",
+ seed: "vvx450",
+ anchor: { newRetail: 190, demandScore: 52 },
+ comparables: [
+ { kind: "SOLD", title: "Poly VVX 450 used", price: 55, source: "ebay", soldAt: ago(13) },
+ { kind: "ACTIVE", title: "VVX 450 IP phone", price: 85, source: "ebay" },
+ ],
+ },
+ {
+ source: "UNIVERSITY_SURPLUS",
+ sourceAuctionId: "UW-5680",
+ sourceUrl: "https://surplus.uw.edu/5680",
+ title: "Canon EOS R5 + RF 24-70 f/2.8 Kit",
+ description: "University media dept surplus, Canon EOS R5 body + RF 24-70mm f/2.8. Low shutter, functional.",
+ category: "Cameras",
+ manufacturer: "Canon",
+ model: "EOS R5",
+ condition: "LIKE_NEW",
+ quantity: 1,
+ weightLbs: 8,
+ city: "Seattle",
+ state: "WA",
+ zip: "98195",
+ currentBid: 1800,
+ bidCount: 12,
+ closesInDays: 1,
+ terms: "Shipping available.",
+ seed: "eosr5",
+ anchor: { newRetail: 5700, demandScore: 76 },
+ comparables: [
+ { kind: "SOLD", title: "Canon R5 + 24-70 used", price: 3200, source: "ebay", soldAt: ago(4) },
+ { kind: "ACTIVE", title: "EOS R5 kit", price: 3800, source: "mpb" },
+ ],
+ },
+ {
+ source: "GOVDEALS",
+ sourceAuctionId: "GD-465550",
+ sourceUrl: "https://www.govdeals.com/index.cfm?fa=Main.Item&itemid=465550",
+ title: "Pallet of Assorted Networking Gear (Parts/Repair)",
+ description: "Mixed pallet: switches, APs, cabling, mostly untested. Sold for parts/repair.",
+ category: "Networking",
+ manufacturer: "Mixed",
+ model: "Assorted",
+ condition: "FOR_PARTS",
+ quantity: 1,
+ weightLbs: 210,
+ city: "Baltimore",
+ state: "MD",
+ zip: "21202",
+ currentBid: 120,
+ bidCount: 3,
+ closesInDays: 2,
+ terms: "Pickup only. Sold as-is for parts.",
+ seed: "partspallet",
+ anchor: { newRetail: 6000, demandScore: 48 },
+ comparables: [
+ { kind: "SOLD", title: "Networking parts pallet", price: 700, source: "ebay", soldAt: ago(15) },
+ { kind: "ACTIVE", title: "Mixed networking lot", price: 1100, source: "ebay" },
+ ],
+ },
+];
+
+async function main() {
+ console.log("Seeding GovArbitrage…");
+
+ // Reset (idempotent seed).
+ await prisma.listingEvent.deleteMany();
+ await prisma.score.deleteMany();
+ await prisma.comparable.deleteMany();
+ await prisma.research.deleteMany();
+ await prisma.costBreakdown.deleteMany();
+ await prisma.buyerLead.deleteMany();
+ await prisma.buyerInterestPage.deleteMany();
+ await prisma.auctionOutcome.deleteMany();
+ await prisma.note.deleteMany();
+ await prisma.listing.deleteMany();
+
+ // Demo user (password: "changeme" — bcrypt-like placeholder hash; auth is scaffolded).
+ await prisma.user.upsert({
+ where: { email: "admin@govarbitrage.local" },
+ update: {},
+ create: {
+ email: "admin@govarbitrage.local",
+ name: "Demo Admin",
+ role: "ADMIN",
+ passwordHash: "$demo$changeme",
+ },
+ });
+
+ let done = 0;
+ for (const s of LISTINGS) {
+ const listing = await prisma.listing.create({
+ data: {
+ source: s.source,
+ sourceAuctionId: s.sourceAuctionId,
+ sourceUrl: s.sourceUrl,
+ title: s.title,
+ description: s.description,
+ category: s.category,
+ manufacturer: s.manufacturer,
+ model: s.model,
+ condition: s.condition,
+ quantity: s.quantity,
+ weightLbs: s.weightLbs,
+ dimensions: s.dimensions,
+ locationCity: s.city,
+ locationState: s.state,
+ locationZip: s.zip,
+ currentBid: s.currentBid,
+ bidCount: s.bidCount,
+ closingAt: soon(s.closesInDays),
+ imageUrls: img(s.seed),
+ auctionTerms: s.terms,
+ researchStatus: "PENDING",
+ },
+ });
+
+ await prisma.listingEvent.create({
+ data: { listingId: listing.id, type: "IMPORTED", message: `Imported from ${s.source}` },
+ });
+
+ await runResearch(listing.id, { anchor: s.anchor, comparables: s.comparables });
+ done++;
+ process.stdout.write(` [${done}/${LISTINGS.length}] ${s.title.slice(0, 48)}\n`);
+ }
+
+ // A sample buyer-interest page + lead on the standout dental chair.
+ const dental = await prisma.listing.findFirst({ where: { sourceAuctionId: "GD-448120" } });
+ if (dental) {
+ await prisma.buyerInterestPage.create({
+ data: {
+ listingId: dental.id,
+ slug: "adec-511-dental-chair",
+ headline: "A-dec 511 Dental Chair — Contingent Interest",
+ published: true,
+ estDelivered: 9500,
+ },
+ });
+ await prisma.buyerLead.create({
+ data: {
+ listingId: dental.id,
+ name: "Dr. Chen",
+ email: "chen@example-dental.com",
+ offer: 8500,
+ contingent: true,
+ notes: "Interested if it wins; needs delivery to 94103.",
+ },
+ });
+ await prisma.auctionOutcome.create({
+ data: { listingId: dental.id, status: "WATCHING", maxBidSet: 3200 },
+ });
+ }
+
+ console.log(`Seeded ${done} listings with full research, costs, and scores.`);
+}
+
+main()
+ .then(async () => {
+ await prisma.$disconnect();
+ })
+ .catch(async (e) => {
+ console.error(e);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/src/engines/demand.ts b/src/engines/demand.ts
new file mode 100644
index 0000000..e911f2b
--- /dev/null
+++ b/src/engines/demand.ts
@@ -0,0 +1,73 @@
+import { clamp } from "@/lib/utils";
+
+// Deterministic, $0 demand + retail heuristic. Used as the fallback when no AI
+// model is reachable, and as a sanity floor for AI estimates. Keyword-driven so
+// it is fully explainable.
+
+interface CategorySignal {
+ demand: number; // 0..100 baseline secondary-market liquidity
+ retailFloor: number; // rough per-unit new-retail baseline (USD)
+ keywords: string[];
+}
+
+const CATEGORY_SIGNALS: CategorySignal[] = [
+ { demand: 82, retailFloor: 900, keywords: ["laptop", "macbook", "thinkpad", "computer", "workstation"] },
+ { demand: 80, retailFloor: 600, keywords: ["iphone", "ipad", "tablet", "phone", "surface"] },
+ { demand: 70, retailFloor: 400, keywords: ["monitor", "display", "projector", "tv", "television"] },
+ { demand: 74, retailFloor: 4500, keywords: ["dental", "medical", "surgical", "exam", "patient", "dental chair", "autoclave"] },
+ { demand: 68, retailFloor: 1200, keywords: ["microscope", "lab", "laboratory", "analyzer", "centrifuge", "spectrometer"] },
+ { demand: 66, retailFloor: 2200, keywords: ["forklift", "generator", "compressor", "welder", "lathe", "cnc"] },
+ { demand: 60, retailFloor: 800, keywords: ["herman miller", "aeron", "steelcase", "office chair", "ergonomic"] },
+ { demand: 45, retailFloor: 300, keywords: ["desk", "cabinet", "table", "furniture", "shelving", "credenza"] },
+ { demand: 72, retailFloor: 550, keywords: ["drone", "camera", "lens", "nikon", "canon", "sony", "gopro"] },
+ { demand: 64, retailFloor: 700, keywords: ["radio", "motorola", "network", "cisco", "switch", "router", "server"] },
+ { demand: 58, retailFloor: 500, keywords: ["tool", "dewalt", "milwaukee", "makita", "power tool", "generator"] },
+ { demand: 50, retailFloor: 250, keywords: ["vehicle", "truck", "trailer", "atv", "mower", "tractor"] },
+];
+
+const PREMIUM_BRANDS = [
+ "herman miller",
+ "steelcase",
+ "apple",
+ "macbook",
+ "dewalt",
+ "milwaukee",
+ "cisco",
+ "midmark",
+ "adec",
+ "leica",
+ "nikon",
+];
+
+export interface DemandEstimate {
+ demandScore: number;
+ retailFloor: number;
+ matchedCategory: string | null;
+ brandBoost: boolean;
+}
+
+/** Estimate demand + a retail floor from listing text keywords. */
+export function estimateDemand(text: string): DemandEstimate {
+ const hay = text.toLowerCase();
+ let best: CategorySignal | null = null;
+ let bestHits = 0;
+
+ for (const sig of CATEGORY_SIGNALS) {
+ const hits = sig.keywords.filter((k) => hay.includes(k)).length;
+ if (hits > bestHits) {
+ bestHits = hits;
+ best = sig;
+ }
+ }
+
+ const brandBoost = PREMIUM_BRANDS.some((b) => hay.includes(b));
+ const baseDemand = best ? best.demand : 40;
+ const demandScore = clamp(baseDemand + (brandBoost ? 8 : 0), 0, 100);
+
+ return {
+ demandScore,
+ retailFloor: best ? best.retailFloor : 200,
+ matchedCategory: best ? best.keywords[0] : null,
+ brandBoost,
+ };
+}
diff --git a/src/lib/ai.ts b/src/lib/ai.ts
new file mode 100644
index 0000000..725384a
--- /dev/null
+++ b/src/lib/ai.ts
@@ -0,0 +1,122 @@
+// Local-first AI layer. Product identification runs on Ollama (this machine,
+// $0/query). Anthropic is an opt-in fallback only, enabled by setting
+// AI_PROVIDER=anthropic and ANTHROPIC_API_KEY. If no model is reachable, callers
+// fall back to the deterministic heuristic in engines/demand.ts.
+
+const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || "http://localhost:11434";
+const TEXT_MODEL = process.env.OLLAMA_TEXT_MODEL || "qwen3:14b";
+const VISION_MODEL = process.env.OLLAMA_VISION_MODEL || "qwen2.5vl:7b";
+
+export interface AiIdentification {
+ manufacturer?: string;
+ model?: string;
+ category?: string;
+ estimatedNewRetail?: number;
+ demandScore?: number; // 0..100
+ confidence?: number; // 0..1
+ reasoning?: string;
+}
+
+const SYSTEM = `You are a resale-arbitrage product analyst. Given a government-surplus
+auction listing, identify the product and estimate its market. Respond ONLY with
+compact JSON matching:
+{"manufacturer":string,"model":string,"category":string,"estimatedNewRetail":number,"demandScore":number,"confidence":number,"reasoning":string}
+demandScore is 0-100 (secondary-market liquidity). confidence is 0-1. Use USD.`;
+
+async function ollamaJson(prompt: string, model = TEXT_MODEL, images?: string[]): Promise<AiIdentification | null> {
+ try {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 45_000);
+ const res = await fetch(`${OLLAMA_BASE}/api/generate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ signal: controller.signal,
+ body: JSON.stringify({
+ model,
+ prompt: `${SYSTEM}\n\nLISTING:\n${prompt}`,
+ stream: false,
+ format: "json",
+ options: { temperature: 0.1 },
+ ...(images && images.length ? { images } : {}),
+ }),
+ });
+ clearTimeout(timeout);
+ if (!res.ok) return null;
+ const data = (await res.json()) as { response?: string };
+ if (!data.response) return null;
+ return JSON.parse(data.response) as AiIdentification;
+ } catch {
+ return null;
+ }
+}
+
+/** Identify a product from listing text (and optionally base64 images). */
+export async function identifyProduct(args: {
+ title: string;
+ description?: string | null;
+ category?: string | null;
+ imagesBase64?: string[];
+}): Promise<{ result: AiIdentification | null; model: string }> {
+ const provider = process.env.AI_PROVIDER || "ollama";
+ const text = [
+ `Title: ${args.title}`,
+ args.category ? `Category: ${args.category}` : "",
+ args.description ? `Description: ${args.description}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ // Vision path when images are supplied and a vision model is configured.
+ if (args.imagesBase64 && args.imagesBase64.length) {
+ const v = await ollamaJson(text, VISION_MODEL, args.imagesBase64);
+ if (v) return { result: v, model: `ollama:${VISION_MODEL}` };
+ }
+
+ const t = await ollamaJson(text, TEXT_MODEL);
+ if (t) return { result: t, model: `ollama:${TEXT_MODEL}` };
+
+ // Opt-in cloud fallback.
+ if (provider === "anthropic" && process.env.ANTHROPIC_API_KEY) {
+ const a = await anthropicJson(text);
+ if (a) return { result: a, model: "anthropic:claude" };
+ }
+
+ return { result: null, model: "heuristic" };
+}
+
+async function anthropicJson(text: string): Promise<AiIdentification | null> {
+ try {
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": process.env.ANTHROPIC_API_KEY as string,
+ "anthropic-version": "2023-06-01",
+ },
+ body: JSON.stringify({
+ model: "claude-haiku-4-5-20251001",
+ max_tokens: 512,
+ system: SYSTEM,
+ messages: [{ role: "user", content: `LISTING:\n${text}` }],
+ }),
+ });
+ if (!res.ok) return null;
+ const data = (await res.json()) as { content?: { text?: string }[] };
+ const raw = data.content?.[0]?.text;
+ if (!raw) return null;
+ const match = raw.match(/\{[\s\S]*\}/);
+ return match ? (JSON.parse(match[0]) as AiIdentification) : null;
+ } catch {
+ return null;
+ }
+}
+
+/** Is a local model reachable? Used by health checks + the worker banner. */
+export async function ollamaReachable(): Promise<boolean> {
+ try {
+ const res = await fetch(`${OLLAMA_BASE}/api/tags`, { signal: AbortSignal.timeout(3000) });
+ return res.ok;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/pipeline/research.ts b/src/pipeline/research.ts
new file mode 100644
index 0000000..55ef5f2
--- /dev/null
+++ b/src/pipeline/research.ts
@@ -0,0 +1,273 @@
+import { prisma } from "@/lib/db";
+import { identifyProduct } from "@/lib/ai";
+import { estimateDemand } from "@/engines/demand";
+import { computeValuation } from "@/engines/valuation";
+import { computeCosts } from "@/engines/costs";
+import { computeScores } from "@/engines/scoring";
+import { estimateFreight } from "@/engines/freight";
+import { clamp } from "@/lib/utils";
+import type { ConditionKey, SourceKey } from "@/engines/types";
+
+export interface ResearchOptions {
+ /** Run the local AI identifier (Ollama). Off by default for deterministic seeds. */
+ useAI?: boolean;
+ /** Deterministic overrides (used by the seeder to inject realistic anchors). */
+ anchor?: { newRetail?: number; demandScore?: number };
+ comparables?: {
+ kind: "SOLD" | "ACTIVE" | "RETAIL";
+ title: string;
+ price: number;
+ url?: string;
+ source?: string;
+ soldAt?: Date;
+ }[];
+ /** Persist AI-identified manufacturer/model back onto the listing. */
+ writeIdentity?: boolean;
+}
+
+/** Infer local-pickup requirement from weight + auction terms. */
+function inferLocalPickup(weightLbs: number, terms?: string | null): boolean {
+ const t = (terms || "").toLowerCase();
+ if (t.includes("pickup only") || t.includes("pick up only") || t.includes("no shipping")) return true;
+ if (t.includes("shipping available") || t.includes("will ship")) return false;
+ return weightLbs > 300; // heavy lots are usually pickup-only at gov auctions
+}
+
+/**
+ * Full research run for one listing: identify → value → cost → score → persist.
+ * Idempotent (upserts), safe to re-run. Returns a compact summary.
+ */
+export async function runResearch(listingId: string, opts: ResearchOptions = {}) {
+ const listing = await prisma.listing.findUnique({ where: { id: listingId } });
+ if (!listing) throw new Error(`Listing ${listingId} not found`);
+
+ await prisma.listing.update({
+ where: { id: listingId },
+ data: { researchStatus: "IN_PROGRESS" },
+ });
+
+ const text = [listing.title, listing.category, listing.description]
+ .filter(Boolean)
+ .join("\n");
+ const heuristic = estimateDemand(text);
+
+ // Identity + anchor resolution: overrides > AI > heuristic.
+ let manufacturer = listing.manufacturer;
+ let model = listing.model;
+ let category = listing.category;
+ let newRetail = opts.anchor?.newRetail ?? heuristic.retailFloor;
+ let demandScore = opts.anchor?.demandScore ?? heuristic.demandScore;
+ let identificationConfidence = 0.45; // heuristic baseline
+ let identifiedBy = "heuristic";
+ let identifyRaw: unknown = { heuristic };
+
+ if (opts.useAI) {
+ const { result, model: aiModel } = await identifyProduct({
+ title: listing.title,
+ description: listing.description,
+ category: listing.category,
+ });
+ if (result) {
+ manufacturer = result.manufacturer || manufacturer;
+ model = result.model || model;
+ category = result.category || category;
+ if (typeof result.estimatedNewRetail === "number" && result.estimatedNewRetail > 0) {
+ // Blend AI estimate with the heuristic floor to avoid wild outliers.
+ newRetail = Math.max(result.estimatedNewRetail, heuristic.retailFloor * 0.5);
+ }
+ if (typeof result.demandScore === "number") demandScore = clamp(result.demandScore, 0, 100);
+ identificationConfidence = clamp(result.confidence ?? 0.6, 0, 1);
+ identifiedBy = aiModel;
+ identifyRaw = result;
+ }
+ }
+
+ const condition = listing.condition as ConditionKey;
+ const comps = opts.comparables ?? [];
+
+ // --- Valuation ---
+ const valuation = computeValuation({
+ newRetail,
+ condition,
+ quantity: listing.quantity,
+ demandScore,
+ identificationConfidence,
+ comparableCount: comps.length,
+ });
+
+ // --- Costs (winning bid modeled as the current bid) ---
+ const weightLbs = listing.weightLbs ?? 0;
+ const localPickup = inferLocalPickup(weightLbs, listing.auctionTerms);
+ const fr = estimateFreight(weightLbs);
+ const costs = computeCosts({
+ source: listing.source as SourceKey,
+ winningBid: Number(listing.currentBid),
+ quantity: listing.quantity,
+ weightLbs,
+ condition,
+ expectedSalePrice: valuation.expectedSalePrice,
+ daysUntilSold: valuation.daysUntilSold,
+ localPickup,
+ });
+
+ // --- Scores ---
+ const scores = computeScores({
+ roi: costs.roi,
+ expectedNetProfit: costs.expectedNetProfit,
+ demandScore,
+ daysUntilSold: valuation.daysUntilSold,
+ weightLbs,
+ condition,
+ bidCount: listing.bidCount,
+ localPickup,
+ hasBuyerLeads: false,
+ confidenceScore: valuation.confidenceScore,
+ probabilityOfSale: valuation.probabilityOfSale,
+ isFreight: fr.isFreight,
+ });
+
+ // --- Persist ---
+ await prisma.$transaction(async (tx) => {
+ await tx.research.upsert({
+ where: { listingId },
+ create: {
+ listingId,
+ ...valuationToDb(valuation),
+ sources: comps.map((c) => ({ name: c.source ?? "comp", url: c.url, kind: c.kind, price: c.price })),
+ summary: buildSummary(listing.title, valuation, costs, demandScore),
+ model: identifiedBy,
+ },
+ update: {
+ ...valuationToDb(valuation),
+ summary: buildSummary(listing.title, valuation, costs, demandScore),
+ model: identifiedBy,
+ },
+ });
+
+ await tx.costBreakdown.upsert({
+ where: { listingId },
+ create: { listingId, ...costsToDb(costs) },
+ update: { ...costsToDb(costs) },
+ });
+
+ // Replace comparables.
+ await tx.comparable.deleteMany({ where: { listingId } });
+ if (comps.length) {
+ await tx.comparable.createMany({
+ data: comps.map((c) => ({
+ listingId,
+ kind: c.kind,
+ title: c.title,
+ price: c.price,
+ url: c.url,
+ source: c.source,
+ soldAt: c.soldAt,
+ })),
+ });
+ }
+
+ for (const s of scores) {
+ await tx.score.upsert({
+ where: { listingId_profile: { listingId, profile: s.profile } },
+ create: {
+ listingId,
+ profile: s.profile,
+ value: s.value,
+ arbitrage: s.components.arbitrage,
+ demand: s.components.demand,
+ velocity: s.components.velocity,
+ logistics: s.components.logistics,
+ condition: s.components.condition,
+ competition: s.components.competition,
+ buyer: s.components.buyer,
+ risk: s.risk,
+ dropShip: s.dropShip,
+ explanation: s.explanation,
+ factors: s.factors,
+ },
+ update: {
+ value: s.value,
+ arbitrage: s.components.arbitrage,
+ demand: s.components.demand,
+ velocity: s.components.velocity,
+ logistics: s.components.logistics,
+ condition: s.components.condition,
+ competition: s.components.competition,
+ buyer: s.components.buyer,
+ risk: s.risk,
+ dropShip: s.dropShip,
+ explanation: s.explanation,
+ factors: s.factors,
+ },
+ });
+ }
+
+ await tx.listing.update({
+ where: { id: listingId },
+ data: {
+ researchStatus: "COMPLETE",
+ identifiedBy,
+ identifyRaw: identifyRaw as object,
+ ...(opts.writeIdentity ? { manufacturer, model, category } : {}),
+ },
+ });
+
+ await tx.listingEvent.create({
+ data: {
+ listingId,
+ type: "RESEARCH_COMPLETED",
+ message: `Research complete via ${identifiedBy}: est. net ${costs.expectedNetProfit.toFixed(0)}, ROI ${(costs.roi * 100).toFixed(0)}%`,
+ meta: { model: identifiedBy },
+ },
+ });
+ });
+
+ return {
+ listingId,
+ expectedNetProfit: costs.expectedNetProfit,
+ roi: costs.roi,
+ overallScore: scores.find((s) => s.profile === "OVERALL_OPPORTUNITY")?.value ?? 0,
+ identifiedBy,
+ };
+}
+
+function valuationToDb(v: ReturnType<typeof computeValuation>) {
+ return {
+ newRetail: v.newRetail,
+ newReplacement: v.newReplacement,
+ avgRetail: v.avgRetail,
+ usedSoldPrice: v.usedSoldPrice,
+ usedAskingPrice: v.usedAskingPrice,
+ usedLow: v.usedLow,
+ usedHigh: v.usedHigh,
+ wholesaleValue: v.wholesaleValue,
+ liquidationValue: v.liquidationValue,
+ sellTodayValue: v.sellTodayValue,
+ value7Day: v.value7Day,
+ value30Day: v.value30Day,
+ value90Day: v.value90Day,
+ expectedSalePrice: v.expectedSalePrice,
+ probabilityOfSale: v.probabilityOfSale,
+ daysUntilSold: v.daysUntilSold,
+ confidenceScore: v.confidenceScore,
+ };
+}
+
+function costsToDb(c: ReturnType<typeof computeCosts>) {
+ const { assumptions, ...rest } = c;
+ return { ...rest, assumptions: assumptions as object };
+}
+
+function buildSummary(
+ title: string,
+ v: ReturnType<typeof computeValuation>,
+ c: ReturnType<typeof computeCosts>,
+ demand: number,
+): string {
+ return (
+ `${title}: est. resale ${v.expectedSalePrice.toFixed(0)} vs new retail ${v.newRetail.toFixed(0)}. ` +
+ `At the current bid, total-in ${c.totalInvestment.toFixed(0)} → net ${c.expectedNetProfit.toFixed(0)} ` +
+ `(${(c.roi * 100).toFixed(0)}% ROI). Demand ${demand.toFixed(0)}/100, ~${v.daysUntilSold} days to sell, ` +
+ `confidence ${v.confidenceScore.toFixed(0)}/100. Recommended max bid ${c.recommendedMaxBid.toFixed(0)}.`
+ );
+}
← 111d8f1 Phase 2: deterministic valuation/cost/scoring engines + 16 p
·
back to Govarbitrage
·
Phase 4: dashboard (cards + full-column TanStack table + CSV 270f059 →