# Grail API — agent skill

Teach your coding agent (Claude Code, Codex, Cursor, Windsurf, or a custom agent) the
Grail sold-comps API so it writes correct calls the first time. Save this file where
your agent reads it (e.g. `AGENTS.md`, `.cursor/rules/grail.md`, or a skill doc), or
paste it into the system prompt.

- Base URL: `https://grail.solutions`
- Auth: `Authorization: Bearer gk_live_<key>`
- Get a free key: https://grail.solutions/signup
- Full schema: https://grail.solutions/openapi.json

## Search sold comps

```
GET /v1/scrape?keyword=<text>&count=<60|120|240>&page=<n>
```

Optional filters:

| Param | Values |
| --- | --- |
| `sold` | `true` (sold, default) / `false` (active) |
| `relevance` | `true` (AI-cleaned, default) / `false` (raw eBay feed) |
| `minPrice` / `maxPrice` | numbers |
| `itemCondition` | `any` / `new` / `used` |
| `conditionId` | eBay numeric condition id |
| `ebaySite` | `ebay.com`, `ebay.co.uk`, `ebay.de`, … (8 marketplaces) |
| `sortOrder` | `endedRecently`, `timeNewlyListed`, `pricePlusPostageLowest`, `pricePlusPostageHighest`, `distanceNearest` |
| `soldAfter` / `soldBefore` | ISO dates (inclusive) |
| `exactMatch` | `true` / `false` |
| `categoryId` | eBay category id |

## Response shape

```json
{
  "keyword": "nintendo switch oled",
  "page": 1,
  "totalItems": 240,
  "hasNextPage": false,
  "summary": {
    "count": 46, "currency": "USD",
    "median": "170.00", "mean": "181.42",
    "min": "120.00", "max": "289.99",
    "p25": "155.00", "p75": "199.00",
    "avgShipping": "8.50"
  },
  "items": [
    {
      "itemId": "…", "title": "Nintendo Switch OLED …", "url": "https://…",
      "condition": "Used", "conditionId": "3000",
      "soldPrice": "170.00", "shippingPrice": "8.50", "totalPrice": "178.50",
      "endedAt": "2026-10-01T12:00:00Z"
    }
  ]
}
```

## Rules an agent gets wrong

- **Prices are decimal STRINGS.** Parse (`parseFloat`) before any math.
- Check `hasNextPage` before paginating; `count` caps at 240.
- On `429 quota_exceeded`, back off — do not hot-loop. For bulk work, enqueue via
  `POST /v1/scrape/jobs` and poll `GET /v1/scrape/jobs/{job_id}`.
- Retry `5xx` with exponential backoff + jitter; never retry `400` / `401`.
- Read `X-Usage-Remaining` / `X-RateLimit-Remaining` instead of guessing.
- Use `relevance=false` only when you genuinely want the raw eBay feed.

## Other endpoints

| Endpoint | Purpose |
| --- | --- |
| `POST /v1/scrape/bulk` | Up to 25 keywords in one call (cache-first, metered once each). |
| `POST /v1/scrape/jobs` | Enqueue deep/bursty work → `202` + `job_id`. |
| `GET /v1/scrape/jobs/{job_id}` | Poll an async job (Max Mode). |
| `POST /v1/shipping/rates` | Observed shipping + landed-cost estimate for a keyword. |
| `GET /v1/usage` | Current key usage, plan, quota, reset window. |
| `GET /v1/status` | Service status and capabilities. |

## MCP

If your client speaks MCP, connect to the hosted connector at
`https://grail.solutions/mcp` (Bearer key) instead of writing HTTP calls by hand.
See https://grail.solutions/docs/mcp.
