Grail

Get eBay sold prices in Node.js

You want real sold prices in JavaScript — what an item actually sold for, not its asking price. This is the shortest path: one fetch call, no scraping stack to babysit, no approval to get in.

The whole call

One fetch returns cleaned eBay completed sales for a keyword, plus a median/range summary computed over those rows. Works in Node 18+, Bun, Deno, and any edge runtime.

const KEY = process.env.GRAIL_KEY // free key: https://grail.solutions/signup

const r = await fetch(
  "https://grail.solutions/v1/scrape?keyword=leica+m6&count=60&sold=true",
  { headers: { Authorization: `Bearer ${KEY}` } },
)
if (!r.ok) throw new Error(`Grail ${r.status}`)

const data = await r.json()
console.log(data.rawMedian)      // 1549.5
console.log(data.items.length)   // 58 real completed sales
console.log(data.items[0].soldPrice, data.items[0].endedAt)

Get a key at /signup — 100 requests a month free, no card. Then mint the secret at /dashboard/api-keys (shown once).

It is typed

The response shape is stable and versioned, so you can describe it once — no XML, no cursor chasing, no shifting markup:

type SoldRow = {
  itemId: string
  title: string
  url: string
  condition: string
  soldPrice: string   // decimal string
  shippingPrice: string
  totalPrice: string
  endedAt: string      // yyyy-mm-dd
  buyingFormat: string
}

type ScrapeResponse = {
  keyword: string
  totalItems: number
  rawMedian: number
  rawSampleCount: number
  summary: { count: number; median: number; mean: number; min: number; max: number; p25: number; p75: number }
  items: SoldRow[]
}

Why the obvious paths are harder than they look

  • eBay’s Product Research (Terapeak) is a dashboard — no JSON API, aggregates only.
  • eBay’s official sold-data API (Marketplace Insights) is approval-gated and currently restricted, with no self-serve path in.
  • Scraping eBay yourself means residential proxies, CAPTCHAs, session rotation, and a parser that breaks on layout changes — a second product to maintain.

One gotcha that bites everyone

Prices come back as decimal strings (e.g. "1549.50"), not numbers — this avoids silent float rounding on money. Coerce before you do math:

const prices = data.items
  .map((row) => Number(row.soldPrice))
  .filter((n) => Number.isFinite(n))

Where to go next

Ready? Grab a free key and make the call: grail.solutions/signup.