[object Object]

← 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

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 →