# Frisske — API for AI Agents

> Public, read-only JSON API for AI agents (Claude, ChatGPT, custom bots) to search and browse Hungarian grocery prices. No authentication required. All data is publicly displayed on frisske.hu.

**Base URL:** `https://frisske.hu`
**Response format:** JSON
**Currency:** HUF (Ft), all prices include VAT
**Language:** Product names are in Hungarian; search is accent-insensitive
**Update frequency:** continuous; varies by source (web-store prices refreshed regularly, flyer prices with each new flyer)
**Terms:** https://frisske.hu/regulamin (section 11: database right, permitted API use)

---

## Usage rules

- **No auth required** for any endpoint below.
- **Not for other apps or services:** this API is for AI agents answering a user's question in the moment. Loading, copying or storing the data for use in another app, website or database is not allowed without written permission from Frisske.
- **Rate limits** are per IP. Exceeding returns `429` with `Retry-After` header.
  - Search endpoints: 150 requests/minute
  - Standard endpoints: 100 requests/minute
  - Heavy endpoints (cross-store comparison): 60 requests/minute
- **Identify your agent** via `User-Agent` (e.g., `User-Agent: MyAgent/1.0 (contact@example.com)`). Anonymous traffic may be throttled more aggressively.
- **Caching:** responses include `Cache-Control` headers; respect them.
- **Attribution:** when surfacing data to users, link back to the product page at `https://frisske.hu/produkt/{id}` and/or `https://frisske.hu`.

---

## Endpoints

### 1. Keyword search (recommended for most queries)

`GET /api/search/smart?q={query}&limit={n}&store={storeName}`

Accent-insensitive Hungarian keyword search across all stores. Best general-purpose endpoint.

**Parameters:**
- `q` (required): search term, min 2 characters. Hungarian with or without diacritics works (`teszta` matches `tészta`).
- `limit` (optional, default 20, max 100): number of results.
- `store` (optional): restrict to one store name (see store list below).

**Example:** `GET /api/search/smart?q=vaj&limit=10`

**Response:**
```json
{
  "products": [
    {
      "id": "clx123...",
      "name": "Vaj 82% 200g",
      "price": 699,
      "image": "https://...",
      "store": "LIDL",
      "amount": "200 g",
      "stock": 1
    }
  ],
  "categories": [],
  "totalResults": 47,
  "query": "vaj"
}
```

---

### 2. Category browse

`GET /api/category/{slug}`

Returns up to 500 products in a category, ranked by semantic relevance (AI embeddings).

**Category slugs:**
`tej`, `pekaru`, `vaj`, `tojas`, `sajt`, `joghurt`, `hus`, `felvagottak`, `gyumolcs`, `zoldseg`, `italok`, `sor`, `bor`, `edesseg`, `gabonapehely`, `teszta`, `rizs`, `olaj`, `kave`, `tea`

**Example:** `GET /api/category/tej`

**Response:**
```json
{
  "category": { "id": "tej", "name": "Tej", "emoji": "🥛", "keywords": ["tej", "milk"] },
  "products": [
    {
      "id": "clx...",
      "name": "Friss tej 2,8% 1l",
      "price": 389,
      "amount": "1 l",
      "store": "Tesco",
      "imageUrl": "https://...",
      "validUntil": "2026-08-02T00:00:00.000Z",
      "lastUpdated": "2026-07-26T04:11:00.000Z",
      "similarity": 0.812
    }
  ],
  "stores": ["Auchan", "Tesco", "LIDL", "SPAR", "..."],
  "total": 384
}
```

`validUntil` is non-null only for flyer-promo products and indicates when the promo expires.

---

### 3. Store catalog

`GET /api/store/{storeName}`

Returns all currently active products in one store.

**Store names (exact, case-sensitive — copy verbatim; URL-encode if needed):**
`LIDL`, `Tesco`, `Auchan`, `Kifli`, `Metro`, `SPAR`, `INTERSPAR`, `Aldi`, `Penny`, `Reál`, `CBA`, `COOP`

**Example:** `GET /api/store/LIDL`

**Response:**
```json
{
  "storeName": "LIDL",
  "products": [ { "id": "...", "name": "...", "price": 999, "amount": "...", "store": "LIDL", "imageUrl": "...", "validUntil": null, "lastUpdated": "..." } ],
  "total": 2841
}
```

Some stores (Kifli, Auchan, Tesco, Metro) have 10K+ products — paginate client-side or filter the array.

---

### 4. Product detail

`GET /api/products/{id}`

Single-product lookup. `{id}` comes from any of the list endpoints above.

**Response:**
```json
{
  "product": {
    "id": "clx...",
    "name": "Vaj 82% 200g",
    "price": 699,
    "amount": "200 g",
    "store": "LIDL",
    "imageUrl": "https://...",
    "validUntil": null,
    "lastUpdated": "2026-07-26T04:11:00.000Z",
    "weighed": false,
    "pricePerUnit": null
  }
}
```

`validUntil` is non-null only for flyer-promo products; permanent assortment returns `null`.

`weighed` is `true` for products sold by weight (e.g. Kifli meat, cheese or fruit listed as `kb. 500 g`). The store prices them per kg: `pricePerUnit` is its own price per kg, `amount` is only today's typical piece and `price` is the per-kg price × that weight. Compare such products by `pricePerUnit`, not by `price`. Product lists, category and semantic-search results carry the same two fields, and their `unitPrice` is then `pricePerUnit`.

---

### 5. Price history

`GET /api/products/{id}/price-history?days={n}`

Returns recorded price points for a product, plus min/max/avg and percent change.

**Parameters:**
- `days` (optional, default 30, range 1–365)

**Response:**
```json
{
  "product": { "id": "...", "name": "...", "currentPrice": 699, "currency": "HUF", "store": "LIDL", "amount": "200 g", "imageUrl": "..." },
  "history": [
    { "price": 749, "recordedAt": "2026-06-26T00:00:00.000Z" },
    { "price": 699, "recordedAt": "2026-07-10T00:00:00.000Z" }
  ],
  "stats": {
    "minPrice": 699,
    "maxPrice": 749,
    "avgPrice": 724,
    "priceChangePercent": -6.68,
    "dataPoints": 2
  },
  "days": 30,
  "basis": { "weighed": false, "unit": null, "currentUnitPrice": null }
}
```

`priceChangePercent` is computed from first vs. last data point in the window (negative = price dropped).

Products sold by weight (see `weighed` in Product detail) return `"basis": { "weighed": true, "unit": "kg", "currentUnitPrice": 2789 }`. Their `history` holds one point per change of the per-kg price, and each point adds `unitPrice` (HUF per kg) and `amount` (the piece listed at that time, e.g. `"kb. 1,2 kg"`). `price` and `stats` are then what today's piece would have cost at that per-kg price, so they stay comparable with `currentPrice`. To describe the trend of such a product, use `unitPrice` against `currentUnitPrice`.

Products whose pack size changed under the same listing (e.g. a croissant 80 g -> 70 g) return `"basis": { "weighed": false, "unit": "kg", "currentUnitPrice": 14271.43, "resized": true }`. Each `history` point then adds `unitPrice` (HUF per `unit`) and `amount` (the pack listed at that time), and `price` is what today's pack would have cost at that per-unit price — so a smaller pack at the same price reads as the price rise it is.

---

### 6. Similar products (cross-store match)

`GET /api/products/{id}/matches?limit={n}&otherStores=true&minSimilarity={0-1}`

Finds semantically similar products to `{id}` using AI embeddings. Returns each match with a price-difference and savings percent relative to the source product.

**Parameters:**
- `limit` (default 10, max 50)
- `otherStores=true` — only matches from different stores (most useful for "is it cheaper elsewhere?")
- `sameStore=true` — only matches from the same store
- `store={storeName}` — filter to one specific store
- `minSimilarity` (default 0.55) — cosine similarity threshold

**Response:**
```json
{
  "product": { "id": "...", "name": "Vaj 82% 200g", "price": 699, "amount": "200 g", "store": "LIDL", "imageUrl": "..." },
  "matches": [
    {
      "id": "...",
      "name": "Tesco Vaj 200g",
      "price": 599,
      "amount": "200 g",
      "store": "Tesco",
      "imageUrl": "...",
      "validUntil": null,
      "lastUpdated": "2026-07-26T04:11:00.000Z",
      "similarity": 0.94,
      "priceDifference": 100,
      "savingsPercent": 14
    }
  ],
  "totalMatches": 7
}
```

`priceDifference` = source price − match price. Positive means switching to the match saves money.

---

### 7. Biggest cross-store savings

`GET /api/products/biggest-savings?limit={n}&minSimilarity={0.55-1.0}&minSavings={HUF}`

Returns pairs of semantically similar products across different stores, sorted by savings percentage. Useful for "where is X cheapest" style questions.

**Parameters:**
- `limit` (default 12, max 50)
- `minSimilarity` (default 0.80) — cosine similarity threshold for matching
- `minSavings` (default 50) — minimum HUF price difference

**Response:**
```json
{
  "savings": [
    {
      "cheapestProduct": { "id": "...", "name": "...", "price": 1490, "store": "LIDL", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "expensiveProduct": { "id": "...", "name": "...", "price": 2290, "store": "CBA", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "savings": 800,
      "savingsPercent": 35,
      "similarity": 0.91
    }
  ],
  "totalFound": 184,
  "cached": true
}
```

---

## Errors

| Status | Meaning |
|---|---|
| 400 | Bad request (missing/invalid params) |
| 404 | Resource not found (unknown category, store, or product id) |
| 429 | Rate limit exceeded. Honor `Retry-After`. |
| 500 | Server error |

Error body: `{ "error": "message" }`.

---

## Example agent workflows

**"Find the cheapest butter"**
1. `GET /api/search/smart?q=vaj&limit=50`
2. Sort by `price` (consider `amount` for fair unit-price comparison).
3. Return top result with store name and link.

**"Compare milk prices across stores"**
1. `GET /api/category/tej`
2. Group `products` by `store`, pick min `price` per store.

**"Where can I save the most this week?"**
1. `GET /api/products/biggest-savings?limit=20&minSavings=50`
2. Surface top pairs with `savingsPercent`.

---

## What is NOT exposed

- Write endpoints (cart, ratings, support tickets) — not for agents.
- Admin / partner / auth endpoints — not public.
- User-specific data — site has no user accounts for shoppers.

---

## Machine-readable schema

OpenAPI 3.1 spec: https://frisske.hu/openapi.json — feed this to tool-using agents (Claude tool use, ChatGPT actions, custom function-calling) to auto-generate typed clients.

---

## Contact

Operator: David Vávra · Liberec, Czech Republic
Support / questions: https://frisske.hu/support
Site description for LLMs: https://frisske.hu/llms.txt
