# tasteHQ API

Open dataset of design taste — built for AI agents and humans. No auth, CORS open, JSON.

**Base URL:** `https://taste-hq.vercel.app`

---

## Static JSON endpoints

| Endpoint | Description | Cache |
|----------|-------------|-------|
| `GET /api/styles.json` | Full catalog of every brand entry (lean fields). | `public, max-age=300, s-maxage=3600` |
| `GET /api/styles/<slug>.json` | Full taste-DNA for one brand (tokens, components, render_tokens, live_tokens). | `public, max-age=300, s-maxage=3600` |
| `GET /api/styles/<slug>/design.md` | Raw markdown DESIGN.md export — frontmatter + body sections, LLM-context friendly. | `public, max-age=300, s-maxage=3600` |
| `GET /api/grammar.json` | Graded grammar block per brand (8-axis grammar v2 schema). | `public, max-age=300, s-maxage=3600` |
| `GET /api/graph.json` | Pre-computed similarity graph between brands. | `public, max-age=300, s-maxage=3600` |
| `GET /api/embeddings.json` | Brand corpus used by `POST /api/search`. | `public, max-age=300, s-maxage=3600` |

CORS headers on every API route:

```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```

---

## `GET /api/styles.json`

Catalog of every brand entry. Lean fields only — fetch the per-entry endpoint for full data.

```bash
curl https://taste-hq.vercel.app/api/styles.json
```

```json
{
  "count": 101,
  "version": "1.0",
  "styles": [
    {
      "id": "d469cba4-c448-4a43-a033-883f8bfcdc42",
      "name": "Anthropic",
      "slug": "anthropic",
      "source": "https://styles.refero.design/style/d469cba4-...",
      "official_url": "https://www.anthropic.com",
      "docs_url": "https://docs.anthropic.com",
      "tier": "reference",
      "theme": "light",
      "era": ["1970s research broadsheet", "Swiss Modernism"],
      "mood": ["editorial", "scholarly", "restrained"],
      "density": "medium",
      "signature_move": "Word-level underlines replace color as the sole emphasis mechanism…",
      "voice_persona": "research librarian",
      "voice_score": 75,
      "has_video": true,
      "has_screenshot": true
    }
  ]
}
```

**Tier values:** `reference` (★★★ Golden Set, 5 entries), `verified` (★★ pipeline-extracted), `community` (★ legacy refero data), `unrated`.

---

## `GET /api/styles/<slug>.json`

Full taste-DNA for one brand: tokens, components, body sections (signature_move, era_lineage, doc_voice, anti_patterns), code variants, render_tokens (heuristic), live_tokens (extracted from rendered DOM).

```bash
curl https://taste-hq.vercel.app/api/styles/anthropic.json
```

Returns the full lean+heavy merged record. Use this as the single source of truth for an agent generating UI in a brand's style.

---

## `GET /api/styles/<slug>/design.md`

Raw markdown DESIGN.md export. Direct content of `styles/styles/<slug>.md` — frontmatter + all body sections. Optimized for LLM context windows (paste directly into a prompt).

```bash
curl https://taste-hq.vercel.app/api/styles/anthropic/design.md
```

---

## `POST /api/search`

Keyword-expansion BM25-style semantic search across the brand catalog.

**Request body**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `q` | string | yes | — | Natural-language query. Synonym-expanded against an aesthetic / era / persona map. |
| `limit` | integer | no | `10` | Number of results (clamped to `1..100`). Non-integer values return 400. |

```bash
curl -X POST https://taste-hq.vercel.app/api/search \
  -H "Content-Type: application/json" \
  -d '{"q": "warm research aesthetic", "limit": 5}'
```

**Response (200)**

```json
{
  "results": [
    {
      "slug": "anthropic",
      "name": "Anthropic",
      "signature_move": "Word-level underlines replace color…",
      "era": ["1970s research broadsheet", "Swiss Modernism"],
      "mood": ["editorial", "scholarly", "restrained"],
      "persona": "research librarian",
      "score": 0.0421,
      "match_type": "vibe"
    }
  ],
  "query": "warm research aesthetic",
  "expanded_terms": ["warm", "earthy", "research", "academic", "..."],
  "total": 5,
  "version": "v1-keyword-expansion"
}
```

**Status codes**

| Status | Condition |
|--------|-----------|
| `200` | Match returned (may be empty `results`). |
| `400` | Invalid JSON, missing `q`, or non-integer `limit`. |

**Cache:** `public, max-age=60, s-maxage=300`.

---

## `POST /api/extract`

URL → brand-entry starter. Fetches the page, runs a stdlib HTML parser, extracts a candidate palette, voice hint, and signature-move draft. If the URL is already in the catalog (matched on `official_url`), returns the existing entry under `source: "catalog"`.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `url` | string | yes | Must be `http://` or `https://`. Private / localhost / link-local IPs are rejected (SSRF guard). |

```bash
curl -X POST https://taste-hq.vercel.app/api/extract \
  -H "Content-Type: application/json" \
  -d '{"url": "https://anthropic.com"}'
```

**Response (200)** — newly extracted candidate

```json
{
  "url": "https://anthropic.com",
  "source": "extracted",
  "candidate": {
    "name": "Anthropic",
    "slug": "anthropic",
    "tagline": "Anthropic is an AI safety company…",
    "favicon_url": "https://anthropic.com/favicon.ico",
    "preview_image": "https://anthropic.com/og.png",
    "starter_palette": ["#cc7860", "#1a1a1a", "#fefbf6"],
    "voice_hint": "research-org gravitas, terse declarative",
    "signature_move_draft": "stacked short declaratives: '…' / '…' — terse authority; 3-color palette anchors visual identity",
    "headings_sample": ["Claude is here.", "Build with Claude.", "…"],
    "next_steps": "Run `python tools/extract_live_tokens.py --url https://anthropic.com` locally for full pipeline; open a PR with the new entry."
  }
}
```

**Response (200)** — already in catalog

```json
{
  "url": "https://stripe.com",
  "source": "catalog",
  "candidate": {
    "name": "Stripe",
    "slug": "stripe",
    "catalog_entry": { "...": "full styles.json entry" },
    "next_steps": "This brand is already in the catalog as 'stripe'. Open the gallery and search by name to see the full entry."
  }
}
```

**Status codes**

| Status | Condition |
|--------|-----------|
| `200` | Extraction succeeded (or catalog hit). |
| `400` | Invalid JSON, missing `url`, or URL fails validation (scheme, hostname, private IP). |
| `502` | Upstream HTTP error fetching the page. |
| `500` | Unexpected extraction error. |

**Cache:** `no-store` — every extract call hits the live page; caching responses (and errors) at the edge would serve stale results.

---

## `POST /api/compose`

Deterministic hybrid synthesizer — merges grammar axes from N graded brands into one synthesized brand entry. No LLM call; the `signature_move` is templated from the merged grammar.

**Request body**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `sources` | object | yes | — | Map of axis category → source brand slug. Categories: `surface`, `palette`, `type`, `emphasis`, `whitespace`, `voice`, `motion`, `imagery`. Every slug must exist in `api/grammar.json` and be graded. |
| `name` | string | no | autogenerated | Friendly name for the hybrid. Defaults to `"<Source1> × <Source2> hybrid"`. |
| `output_format` | string | no | `"json"` | `"json"` (default) returns the synthesized entry. `"design_md"` returns a rendered Markdown DESIGN.md document. |

```bash
curl -X POST https://taste-hq.vercel.app/api/compose \
  -H "Content-Type: application/json" \
  -d '{
    "sources": {
      "surface": "stripe",
      "palette": "anthropic",
      "voice": "anthropic",
      "whitespace": "acne-studios"
    },
    "name": "Stripe × Anthropic × Acne hybrid"
  }'
```

**Response (200, `output_format: "json"`)**

```json
{
  "name": "Stripe × Anthropic × Acne hybrid",
  "slug": "stripe-anthropic-acne-hybrid",
  "tier": "unrated",
  "provenance": "composed",
  "signature_move": "A taste hybrid of a warm cream ground, museum-grade negative space, and a research-librarian's restraint, with monochrome accent strategy.",
  "grammar": {
    "surface": { "ground": "cream", "border": "…" },
    "palette": { "strategy": "mono", "...": "..." },
    "voice": { "archetype": "librarian", "...": "..." },
    "whitespace": { "discipline": "extreme", "...": "..." }
  },
  "_compose": {
    "sources": { "surface": "stripe", "palette": "anthropic", "voice": "anthropic", "whitespace": "acne-studios" },
    "source_quotes": [
      { "slug": "stripe", "name": "Stripe", "signature_move": "…" },
      { "slug": "anthropic", "name": "Anthropic", "signature_move": "…" },
      { "slug": "acne-studios", "name": "Acne Studios", "signature_move": "…" }
    ],
    "axis_count": 14,
    "version": "v1-templated"
  }
}
```

**Response (200, `output_format: "design_md"`)**

Returns `Content-Type: text/markdown; charset=utf-8` — a frontmatter + body Markdown document ready to paste into an LLM context.

**Status codes**

| Status | Condition |
|--------|-----------|
| `200` | Compose succeeded. |
| `400` | Invalid JSON, empty `sources`, unknown axis category, unknown brand slug, or brand has no graded grammar. |
| `500` | Unexpected compose error. |

**Cache:** `public, max-age=60, s-maxage=300`.

---

## `POST /api/log`

Anonymized event ingestion for the tasteHQ closed-loop scoring scaffold. No auth. No user content stored. v0 writes JSONL to ephemeral `/tmp` on the function instance — production will move to Vercel KV / Blob.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `event` | string | yes | One of `brand_pull`, `compose`, `search`, `extract`, `rate_outcome`. |
| `client` | string | no | One of `mcp`, `cli`, `web`, `agent`, `unknown` (default `unknown`). |
| `ts` | string | no | ISO-8601 timestamp ≤ 64 chars. Server stamps current UTC if absent. |
| `brand_slug` | string | conditional | Required for `brand_pull` and `rate_outcome`. Slug pattern: `[a-z0-9][a-z0-9-]{0,80}`. |
| `rating` | number | conditional | Required for `rate_outcome`. Range `1..10`. |
| `sources` | object | optional | For `compose` events. Only string slug values matching the slug pattern are stored. |

```bash
curl -X POST https://taste-hq.vercel.app/api/log \
  -H "Content-Type: application/json" \
  -d '{"event": "brand_pull", "brand_slug": "stripe", "client": "mcp"}'
```

**Response (204)** — empty body.

**Status codes**

| Status | Condition |
|--------|-----------|
| `204` | Accepted (or silently dropped if the filesystem is read-only). |
| `400` | Invalid JSON, unknown `event`, unknown `client`, invalid `brand_slug`, or out-of-range `rating`. |

**Cache:** `no-store`.

---

## `POST /api/score`

Score a live URL against a plain-English design brief. Fetches the page, extracts grammar signals, parses the brief into axis targets, then returns a weighted match score with per-axis breakdown.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `url` | string | yes | Live URL to score. Fetched server-side via the extract pipeline. |
| `brief` | string | yes | Plain-English description of the desired design (e.g. `"white background, minimal, terse copy"`). Must produce at least one parseable axis target or returns 400. |

```bash
curl -X POST https://taste-hq.vercel.app/api/score \
  -H "Content-Type: application/json" \
  -d '{"url": "https://stripe.com", "brief": "white background, minimal, monochrome, terse copy"}'
```

**Response (200)**

```json
{
  "score": 82,
  "verdict": "pass",
  "summary": "Strong ground color and color strategy. weakened by mismatched whitespace generosity.",
  "strongest": "surface.ground",
  "weakest": "whitespace.discipline",
  "failure_mode": "Whitespace generosity reads as 'medium' rather than the 'generous' the brief calls for",
  "next_action": "Increase whitespace generosity to generous",
  "axes": [
    {
      "key": "surface.ground",
      "brief_target": "white",
      "predicted": "white",
      "score": 1.0,
      "status": "pass"
    },
    {
      "key": "whitespace.discipline",
      "brief_target": "generous",
      "predicted": "medium",
      "score": 0.5,
      "status": "fail",
      "note": "Brief calls for whitespace generosity 'generous'; page shows 'medium'"
    }
  ],
  "brief_axes_matched": 4,
  "brief_axes_scored": 4,
  "url": "https://stripe.com",
  "brief_excerpt": "white background, minimal, monochrome, terse copy"
}
```

**Verdict values:** `pass` (score ≥ 80), `revise` (55–79), `reject` (< 55).

**Status codes**

| Status | Condition |
|--------|-----------|
| `200` | Scoring complete. |
| `400` | Missing `url` or `brief`, or brief too vague to parse any axis targets. |
| `500` | Extraction pipeline error fetching the URL. |

**Cache:** `no-store`.

---

## `GET /styles/screenshots/:slug.webp`

Static homepage screenshot, 1280×1600, WebP, ~50–200KB.

## `GET /styles/videos/:slug.mp4`

Animated scroll-recording, H.264 baseline, ~1–2MB, no audio.

---

## Agent integration recipes

### Claude Code / Cursor / Copilot — pull a brand into a prompt

```bash
curl -s https://taste-hq.vercel.app/api/styles/stripe/design.md | head -200
```

Then ask the agent: "Build a pricing page in Stripe's style using the design tokens above."

### Programmatic taste retrieval

```python
import urllib.request, json
catalog = json.loads(urllib.request.urlopen("https://taste-hq.vercel.app/api/styles.json").read())
editorial_brands = [s for s in catalog["styles"] if "editorial" in s["mood"]]
# 32 brands as of v1.0
```

### Search → compose pipeline (Python)

```python
import urllib.request, json

def post(path, body):
    req = urllib.request.Request(
        f"https://taste-hq.vercel.app{path}",
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json"},
    )
    return json.loads(urllib.request.urlopen(req).read())

top = post("/api/search", {"q": "editorial luxury", "limit": 3})["results"]
hybrid = post("/api/compose", {
    "sources": {"voice": top[0]["slug"], "surface": top[1]["slug"]},
})
print(hybrid["signature_move"])
```

### Schema reference

Frontmatter schema, controlled vocabularies (mood / density / tier), failure taxonomy:
[docs/superpowers/specs/2026-05-03-styles-gallery-enrichment-design.md](https://github.com/MustBeSimo/tasteHQ/blob/main/docs/superpowers/specs/2026-05-03-styles-gallery-enrichment-design.md)

---

## Versioning

- `version: "1.0"` in catalog response
- `api_version: "1.0"` in per-entry response

Breaking changes get a major bump and a new `/api/v2/` namespace. Additive fields don't bump.

## Rate limits

None. Cache headers vary by endpoint — see each endpoint section above.

## License

Open. Attribution appreciated.
