const EBAY_TOKEN_URL = "https://api.ebay.com/identity/v1/oauth2/token"; const EBAY_BROWSE_SEARCH_URL = "https://api.ebay.com/buy/browse/v1/item_summary/search"; const EBAY_SCOPE = "https://api.ebay.com/oauth/api_scope"; const EPN_CAMPAIGN_ID = process.env.EPN_CAMPAIGN_ID || "5339155697"; const EPN_CUSTOM_ID = "cardsignal-data"; let cachedToken = null; exports.handler = async (event) => { const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Content-Type": "application/json" }; if (event.httpMethod === "OPTIONS") { return { statusCode: 204, headers, body: "" }; } const query = (event.queryStringParameters?.q || "").trim(); if (!query) { return json(400, { error: "Missing card search query." }, headers); } if (!process.env.EBAY_CLIENT_ID || !process.env.EBAY_CLIENT_SECRET) { return json(200, { status: "setup_required", message: "Add EBAY_CLIENT_ID and EBAY_CLIENT_SECRET in Netlify environment variables to enable live aggregation.", query, activeListings: null, soldListings: { status: "limited_release", message: "Sold-sales aggregation requires eBay Marketplace Insights API access or another compliant data provider." } }, headers); } try { const token = await getEbayToken(); const activeListings = await searchActiveListings(query, token); return json(200, { status: "ok", query, activeListings, soldListings: { status: "limited_release", message: "Sold-sales data is not enabled. eBay Marketplace Insights API access is limited release and must be approved by eBay." } }, headers); } catch (error) { return json(500, { status: "error", message: "Card Signal could not fetch live marketplace data.", detail: error.message }, headers); } }; async function getEbayToken() { const now = Date.now(); if (cachedToken && cachedToken.expiresAt > now + 60_000) { return cachedToken.accessToken; } const auth = Buffer.from(`${process.env.EBAY_CLIENT_ID}:${process.env.EBAY_CLIENT_SECRET}`).toString("base64"); const body = new URLSearchParams({ grant_type: "client_credentials", scope: EBAY_SCOPE }); const response = await fetch(EBAY_TOKEN_URL, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" }, body }); if (!response.ok) { throw new Error(`eBay token request failed with ${response.status}`); } const data = await response.json(); cachedToken = { accessToken: data.access_token, expiresAt: now + (Number(data.expires_in || 7200) * 1000) }; return cachedToken.accessToken; } async function searchActiveListings(query, token) { const url = new URL(EBAY_BROWSE_SEARCH_URL); url.searchParams.set("q", query); url.searchParams.set("category_ids", "212"); url.searchParams.set("limit", "20"); url.searchParams.set("sort", "price"); const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, "X-EBAY-C-MARKETPLACE-ID": "EBAY_US", "X-EBAY-C-ENDUSERCTX": `affiliateCampaignId=${EPN_CAMPAIGN_ID},affiliateReferenceId=${EPN_CUSTOM_ID}` } }); if (!response.ok) { throw new Error(`eBay Browse search failed with ${response.status}`); } const data = await response.json(); const items = (data.itemSummaries || []).map((item) => ({ title: item.title, price: amount(item.price), shipping: amount(item.shippingOptions?.[0]?.shippingCost), condition: item.condition, buyingOptions: item.buyingOptions || [], itemWebUrl: item.itemAffiliateWebUrl || item.itemWebUrl, image: item.image?.imageUrl || null })); const prices = items .map((item) => item.price?.value) .filter((value) => typeof value === "number" && Number.isFinite(value)); return { total: data.total || items.length, countReturned: items.length, minPrice: summarize(prices, "min"), maxPrice: summarize(prices, "max"), averagePrice: summarize(prices, "avg"), items }; } function amount(value) { if (!value || value.value === undefined) { return null; } return { value: Number(value.value), currency: value.currency || "USD" }; } function summarize(values, type) { if (!values.length) { return null; } if (type === "min") { return Math.min(...values); } if (type === "max") { return Math.max(...values); } return Math.round((values.reduce((sum, value) => sum + value, 0) / values.length) * 100) / 100; } function json(statusCode, body, headers) { return { statusCode, headers, body: JSON.stringify(body) }; }