← back to Govarbitrage
src/importers/grays-free.ts
89 lines
import type { RawListing } from "./ingest";
// FREE GraysOnline (Australia) importer via its public Algolia search index —
// harvested App-Id + search key from the site's client (feed-first). $0, no
// scraping. Grays prices are AUD; converted to USD so the valuation engine stays
// coherent (rough fixed rate — refine with a live FX feed if needed).
const APP_ID = process.env.GRAYS_ALGOLIA_APP_ID || "CKPAMVUUBE";
const API_KEY = process.env.GRAYS_ALGOLIA_KEY || "1a31357659d5c40f4641cc0d46c172d0";
const INDEX = "GOL_MAIN";
const AUD_TO_USD = Number(process.env.AUD_USD || 0.66);
interface Hit {
objectID?: string;
ObjectTitle?: string;
ObjectType?: string;
Category?: string;
ParentCategory?: string;
CONDITION?: string;
UnitQuantity?: number;
ObjectPrice?: number;
BidActionCount?: number;
LOT_DEFAULT_END_TIME?: number;
LOT_SKU?: string;
LOT_ID?: number;
ITEM_URL?: string;
ImageURLS?: string[];
SaleSuburb?: string;
SaleState?: string;
}
function mapHit(h: Hit): RawListing | null {
if (!h.ObjectTitle || !(h.LOT_SKU || h.LOT_ID)) return null;
const closing = h.LOT_DEFAULT_END_TIME ? new Date(h.LOT_DEFAULT_END_TIME * 1000).toISOString() : null;
return {
source: "GRAYS_AU",
sourceAuctionId: String(h.LOT_SKU || h.LOT_ID),
sourceUrl: h.ITEM_URL ? `https://www.grays.com${h.ITEM_URL}` : "https://www.grays.com",
title: h.ObjectTitle.trim(),
category: h.Category || h.ParentCategory || undefined,
condition: (h.CONDITION || "").toUpperCase().includes("NEW") ? "NEW" : "UNKNOWN",
quantity: h.UnitQuantity && h.UnitQuantity > 0 ? Math.floor(h.UnitQuantity) : 1,
locationCity: h.SaleSuburb,
locationState: h.SaleState,
// AUD → USD so downstream USD math is coherent.
currentBid: h.ObjectPrice ? Math.round(h.ObjectPrice * AUD_TO_USD * 100) / 100 : 0,
bidCount: h.BidActionCount ?? 0,
closingAt: closing,
imageUrls: Array.isArray(h.ImageURLS) ? h.ImageURLS.slice(0, 6) : [],
auctionTerms: "GraysOnline (AU). Prices shown converted AUD→USD; freight from Australia applies.",
};
}
/** Fetch newest Grays lots via Algolia, paging until `limit`. Free, $0. */
export async function fetchGraysFree(opts: { limit?: number } = {}): Promise<RawListing[]> {
const limit = opts.limit ?? 100;
const perPage = 100;
const out: RawListing[] = [];
for (let page = 0; out.length < limit && page < 20; page++) {
const res = await fetch(`https://${APP_ID}-dsn.algolia.net/1/indexes/*/queries`, {
method: "POST",
headers: {
"X-Algolia-Application-Id": APP_ID,
"X-Algolia-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
requests: [
{
indexName: INDEX,
query: "",
params: `hitsPerPage=${perPage}&page=${page}&filters=${encodeURIComponent("ObjectType:LOT OR ObjectType:RETAIL")}`,
},
],
}),
});
if (!res.ok) throw new Error(`Grays Algolia ${res.status}: ${(await res.text()).slice(0, 160)}`);
const j = (await res.json()) as { results?: { hits?: Hit[] }[] };
const hits = j.results?.[0]?.hits ?? [];
if (!hits.length) break;
for (const h of hits) {
const m = mapHit(h);
if (m) out.push(m);
}
if (hits.length < perPage) break;
}
return out.slice(0, limit);
}