Skip to main content
Glama
fjordskii

DealScore MCP

README.md
# DealScore

Agent-callable car deal grade + brief. Thin Next.js MVP for dogfood testing and GTM.

Compare an asking price to expected market value and return a grade (Great / Good / Fair / High / Overpriced), numeric score, comps summary, risks, and confidence — without scraping Facebook Marketplace or Craigslist.

**Env / dogfood / MarketCheck wiring:** [docs/ENV.md](docs/ENV.md)

## Live dogfood

**Use:** [https://dealscore-xi.vercel.app](https://dealscore-xi.vercel.app)

Do **not** use `https://dealscore.vercel.app` — that alias points at another Vercel project (`sigue`), not this app.

The production site is public (SSO off). Web Analytics is on. Open the URL and use the form (no API key). For REST and MCP, send `x-api-key` or `Authorization: Bearer` from the Production `DEALSCORE_API_KEYS` value in Vercel — the secret is not in this repo.

**MarketCheck is live in production.** `MARKETCHECK_API_KEY` is already set (via Vercel API, not a dashboard paste). Scores use live comps (`data_source: live`). Unset or remove that key and redeploy to fall back to deterministic mock comps. With the key set, upstream failures return `coverage: cannot_score` — they do not silently mock.

Known smoke (live): 2020 Camry, $18,500 ask, 62k mi, ZIP `33803` → Great, expected ~$22,378, 17 comps, `data_source: live`.

## Stack

- **Next.js App Router** on Vercel
- **MCP Streamable HTTP** at `/api/mcp` via `mcp-handler` + `@modelcontextprotocol/server`
- **REST gateway** for agents (`POST /api/score`, `POST /api/explain`)
- **MarketCheck** for live comps + price prediction (on in production)
- **NHTSA vPIC** for VIN decode
- **Deterministic mock comps** only when `MARKETCHECK_API_KEY` is unset (`data_source: mock`, `coverage: partial`)

## Local development

```bash
cp .env.example .env.local
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) and score a vehicle via the web form (no API key required). Leave `MARKETCHECK_API_KEY` empty locally to stay on mock comps.

**Live vs mock:** Production has the MarketCheck key set. Failures return `cannot_score` (no silent mock). Local/dev without the key uses mock.

## Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `MARKETCHECK_API_KEY` | Production: set (live). Local: optional | Live inventory + MarketCheck Price. **Set = live** (prod today). **Unset = mock.** When set, upstream failures return `coverage: cannot_score` (never silent mock). |
| `DEALSCORE_API_KEYS` | Production yes | Comma-separated keys for `x-api-key` / Bearer on `POST /api/score`, `/api/explain`, and `/api/mcp`. |

See [`.env.example`](.env.example) and [docs/ENV.md](docs/ENV.md). Production secrets live in Vercel only; `.env.example` uses `dev-local-key` as a placeholder.

## Authentication

Programmatic routes (`POST /api/score`, `POST /api/explain`, `/api/mcp`) accept either:

- `x-api-key: YOUR_KEY`
- `Authorization: Bearer YOUR_KEY`

Keys must match a value in `DEALSCORE_API_KEYS`. In development, when that env var is empty, programmatic routes are open without a key.

`GET /api/health` is unauthenticated.

## API

Base URL for production: `https://dealscore-xi.vercel.app`

### Health

```bash
curl -sS https://dealscore-xi.vercel.app/api/health
```

Production should report `coverage.marketcheck.key_configured: true` and `mode: live_when_key_set`. Health only means the env var is present, not that a given score call succeeded.

### Score a deal (REST)

Replace `YOUR_DEALSCORE_API_KEY` with a value from Production `DEALSCORE_API_KEYS` in Vercel.

```bash
curl -X POST https://dealscore-xi.vercel.app/api/score \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_DEALSCORE_API_KEY" \
  -d '{
    "year": 2020,
    "make": "Toyota",
    "model": "Camry",
    "asking_price": 18500,
    "mileage": 62000,
    "zip": "33803"
  }'
```

VIN example:

```bash
curl -X POST https://dealscore-xi.vercel.app/api/score \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_DEALSCORE_API_KEY" \
  -d '{
    "vin": "1HGCM82633A004352",
    "asking_price": 16000,
    "mileage": 62000,
    "zip": "78701"
  }'
```

### Explain a grade

```bash
curl -X POST https://dealscore-xi.vercel.app/api/explain \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_DEALSCORE_API_KEY" \
  -d '{ "delta_pct": -0.08 }'
```

### Response shape (score)

```json
{
  "grade": "Great",
  "score": 100,
  "expected_price": 22378,
  "expected_price_low": 13999,
  "expected_price_high": 32997,
  "delta_pct": -0.1733,
  "comps_summary": "...",
  "brief": "...",
  "risks": ["..."],
  "confidence": 0.92,
  "coverage": "scored",
  "rubric_version": "score_v1",
  "data_source": "live",
  "vehicle": { "year": 2020, "make": "Toyota", "model": "Camry" },
  "comps_count": 17
}
```

Illustrative live payload from the Camry smoke (ask $18,500 / 62k mi / ZIP `33803`). Price band and expected price move with the market. `data_source` is `live` (key set + usable comps), `mock` (key unset), or `error` (key set but cannot score).

## MCP (Streamable HTTP)

DealScore exposes an MCP server at **`/api/mcp`** with tools mirroring the REST API:

| Tool | REST equivalent |
|------|-----------------|
| `score_deal` | `POST /api/score` |
| `explain_grade` | `POST /api/explain` |
| `health` | `GET /api/health` |

Production URL: `https://dealscore-xi.vercel.app/api/mcp`

### Cursor — remote MCP

Add to `.cursor/mcp.json` (or Cursor MCP settings):

```json
{
  "mcpServers": {
    "dealscore": {
      "url": "https://dealscore-xi.vercel.app/api/mcp",
      "headers": {
        "x-api-key": "YOUR_DEALSCORE_API_KEY"
      }
    }
  }
}
```

Bearer auth also works:

```json
{
  "mcpServers": {
    "dealscore": {
      "url": "https://dealscore-xi.vercel.app/api/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_DEALSCORE_API_KEY"
      }
    }
  }
}
```

### Claude Desktop — remote MCP

Use [mcp-remote](https://www.npmjs.com/package/mcp-remote) for stdio bridge, or connect directly if your client supports Streamable HTTP:

```json
{
  "mcpServers": {
    "dealscore": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://dealscore-xi.vercel.app/api/mcp",
        "--header",
        "x-api-key:YOUR_DEALSCORE_API_KEY"
      ]
    }
  }
}
```

### Directory publish (later)

`server.json` in the repo root is a stub for MCP directory listings (e.g. Glama). Suggested topics when publishing: `automotive`, `pricing`, `agents`, `market-data`. Remote URL MCP is sufficient for GTM — npm publish is not required.

## Rubric (`score_v1`)

Implementation: [`src/lib/dealscore/rubric.ts`](src/lib/dealscore/rubric.ts).

```
delta_pct = (asking_price - expected_price) / expected_price

≤ -10%  → Great
≤ -3%   → Good
≤ +3%   → Fair
≤ +10%  → High
else    → Overpriced
```

Score maps linearly: `score = clamp(100 - delta_pct * 200, 0, 100)`.

Confidence adjusts by comp count and whether data is live vs mock.

## Cache

Comps are fetched per request (MarketCheck ToS R2). No cross-request persistence — each `/api/score` or `score_deal` call fetches MarketCheck (or mock) comps once for that request only.

## Deploy (Vercel)

Production deployment: [https://dealscore-xi.vercel.app](https://dealscore-xi.vercel.app) (not `dealscore.vercel.app`).

1. Import `fjordskii/dealscore` in Vercel (team: **fjordskiis-projects**).
2. Environment variables (see [docs/ENV.md](docs/ENV.md)) — Production already has both set via Vercel API:
   - `DEALSCORE_API_KEYS` (required in production)
   - `MARKETCHECK_API_KEY` (set in production = live comps; unset + redeploy = mock)
3. Deploy from `main`.

Web Analytics (`@vercel/analytics`) is enabled in the root layout.

## Tests

```bash
npm test
npm run build
```

## Limitations (honest)

- No Facebook Marketplace or Craigslist URL paste/scraping
- Mock comps are deterministic demo data, used only when `MARKETCHECK_API_KEY` is absent
- With a MarketCheck key configured, auth/rate-limit/upstream errors and insufficient comps return `coverage: cannot_score` with `data_source: error` — never silent mock fallback
- `/api/health` reports `key_configured` for MarketCheck, not live connectivity
- No Stripe billing, affiliates, or PII collection in MVP
- Fail-closed: returns `coverage: cannot_score` rather than inventing a Great deal

## License

Private — fjordskii holdings.