DealScore MCP
by fjordskii
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues