# Card Valuation API

eBay sold-listing lookup helper for trading card valuation.

**Base URL (production):** `https://ebay.codenlighten.org`
**Base URL (local):** `http://localhost:3000`

All responses are JSON unless otherwise noted. No authentication required.

---

## Endpoints

### `GET /api/search`

Search eBay sold/completed listings and return matching items plus aggregate price statistics.

#### Query parameters

| Name       | Type    | Required | Default | Description |
|------------|---------|----------|---------|-------------|
| `q`        | string  | yes      | —       | Search query (e.g. `2018 Bowman Chrome Juan Soto Auto`). |
| `category` | string  | no       | `all`   | One of `all`, `sports`, `tcg`, `nonsport`. See `/api/categories`. |
| `grade`    | string  | no       | —       | Filter results by grade. Accepts: `ungraded` / `raw` (no grade in title), `graded` / `any` (any grade present), or a specific grade like `PSA 10`, `BGS 9.5`, `CGC 10`, `SGC 9.5`. |
| `limit`    | integer | no       | `60`    | Max listings to fetch from eBay. Capped at `240`. |

#### Response `200 OK`

```json
{
  "query": "charizard base set",
  "category": "all",
  "gradeFilter": null,
  "ebayUrl": "https://www.ebay.com/sch/i.html?_nkw=...",
  "summary": {
    "count": 60,
    "mean": 383.57,
    "median": 74,
    "min": 0.99,
    "max": 6099,
    "stdev": 812.4
  },
  "summaryTrimmed": {
    "count": 54,
    "mean": 150.05,
    "median": 56.19,
    "min": 0.99,
    "max": 679,
    "stdev": 142.8
  },
  "listings": [
    {
      "title": "1999 Pokemon Base Set Charizard Holo 4/102",
      "price": 187.00,
      "priceText": "$187.00",
      "shipping": "+$5.00 delivery",
      "condition": "Pre-Owned",
      "grade": null,
      "soldDate": "2026-05-26",
      "url": "https://www.ebay.com/itm/123456",
      "imageUrl": "https://i.ebayimg.com/images/g/.../s-l500.jpg"
    }
  ]
}
```

**Field notes**
- `summary` is computed across **all** matching listings.
- `summaryTrimmed` removes outliers using the 1.5 × IQR rule — use this as the buy-quote anchor in-store; it filters out bid wars and accidental lot listings.
- `grade` is `null` for raw cards, otherwise `{ service: "PSA"|"BGS"|"CGC"|"SGC", grade: number }` parsed from the listing title.
- `ebayUrl` is the live eBay search URL — handy for clicking through and sanity-checking.

#### Errors

| Status | Body                                                            | Cause                                            |
|--------|-----------------------------------------------------------------|--------------------------------------------------|
| `400`  | `{"error": "Missing required query parameter: q"}`              | No `q` provided.                                 |
| `400`  | `{"error": "Unknown category \"...\". Use one of: ..."}`        | `category` not in the preset list.               |
| `500`  | `{"error": "eBay returned HTTP 403"}`                           | eBay blocked the request; session is auto-reset and a retry should succeed. |
| `500`  | `{"error": "..."}`                                              | Other upstream/scraper failure.                  |

#### Examples

```bash
# Basic search
curl 'https://ebay.codenlighten.org/api/search?q=charizard+base+set'

# PSA 10 only
curl 'https://ebay.codenlighten.org/api/search?q=2018+bowman+chrome+juan+soto+auto&grade=PSA+10'

# Ungraded Pokemon, scoped to the TCG category
curl 'https://ebay.codenlighten.org/api/search?q=pikachu+illustrator&category=tcg&grade=ungraded'

# Fetch more results for a noisier search
curl 'https://ebay.codenlighten.org/api/search?q=topps+chrome&limit=240'
```

---

### `GET /api/categories`

List the supported `category` preset keys.

#### Response `200 OK`

```json
["all", "sports", "tcg", "nonsport"]
```

| Key        | eBay category          | What it covers                              |
|------------|------------------------|---------------------------------------------|
| `all`      | (no filter)            | Everything — broadest search.               |
| `sports`   | 212                    | Sports Trading Cards (baseball, basketball, football, etc.). |
| `tcg`      | 183454                 | CCG Individual Cards (Pokemon, Magic, Yu-Gi-Oh, etc.). |
| `nonsport` | 183050                 | Non-Sport Trading Card Singles.             |

---

### `GET /api/docs`

Returns this document as raw markdown (`text/markdown`). Useful for piping into a renderer or for clients that want the full human-readable spec.

### `GET /api/docs.json`

Returns a structured JSON description of the API — endpoint list, parameter schemas, example requests. Useful for client codegen.

#### Response shape

```json
{
  "title": "Card Valuation API",
  "baseUrl": "...",
  "endpoints": [
    {
      "method": "GET",
      "path": "/api/search",
      "summary": "...",
      "params": [{ "name": "q", "type": "string", "required": true, "description": "..." }],
      "example": "..."
    }
  ]
}
```

---

## Tips for accurate valuations

1. **Use the trimmed median** (`summaryTrimmed.median`) as the anchor — it ignores bid wars and accidental lot listings.
2. **Match the query specifically.** "Charizard" alone returns plushies and binders. Use card number, set name, and year: `Charizard 4/102 Base Set 1999`.
3. **Filter by grade** when relevant. A raw Near-Mint card and a PSA 10 of the same card can be 10× apart in price.
4. **Sanity-check** by opening `ebayUrl` from the response and skimming the actual listings — automated parsing misses context that a human catches in seconds.

## How it works (briefly)

The server scrapes eBay's public sold-listings search page. It primes a session cookie from `ebay.com/` (cached 30 min) to get past Akamai's bot wall, parses the result HTML, and computes stats. There is no eBay developer account or API key involved.

If eBay changes their HTML, the selectors in `lib/ebay-scraper.js` will need updating.
