# Antonka — API for AI Agents

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

**Base URL:** `https://antonka.pl`
**Response format:** JSON
**Currency:** PLN, all prices include VAT
**Language:** Product names are in Polish; search is accent-insensitive
**Update frequency:** continuous; varies by source (web-store prices refreshed regularly, flyer prices with each new flyer)
**Terms:** https://antonka.pl/regulamin (pkt 13: 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 the operator of Antonka.
- **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://antonka.pl/produkt/{id}` and/or `https://antonka.pl`.

---

## Endpoints

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

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

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

**Parameters:**
- `q` (required): search term, min 2 characters. Polish with or without diacritics works (`maslo` matches `masło`).
- `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=maslo&limit=10`

**Response:**
```json
{
  "products": [
    {
      "id": "clx123...",
      "name": "Masło ekstra 200g",
      "price": 2.29,
      "image": "https://...",
      "store": "LIDL",
      "amount": "250 g",
      "stock": 1
    }
  ],
  "categories": [],
  "totalResults": 47,
  "query": "maslo"
}
```

---

### 2. Category browse

`GET /api/category/{slug}`

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

**Category slugs:**
`mleko`, `pieczywo`, `maslo`, `jajka`, `ser`, `jogurt`, `mieso`, `wedliny`, `owoce`, `warzywa`, `napoje`, `piwo`, `wino`, `slodycze`, `platki`, `makarony`, `ryz`, `olej`, `kawa`, `herbata`

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

**Response:**
```json
{
  "category": { "id": "mleko", "name": "Mleko", "emoji": "🥛", "keywords": ["mleko", "milk"] },
  "products": [
    {
      "id": "clx...",
      "name": "Mleko świeże 2% 1l",
      "price": 1.09,
      "amount": "1 l",
      "store": "Kaufland",
      "imageUrl": "https://...",
      "validUntil": "2026-06-02T00:00:00.000Z",
      "lastUpdated": "2026-05-26T04:11:00.000Z",
      "similarity": 0.812
    }
  ],
  "stores": ["Biedronka", "Kaufland", "LIDL", "Netto", "..."],
  "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`, `Kaufland`, `Biedronka`, `Netto`, `Dino`, `Aldi`, `Żabka`, `Auchan`, `Carrefour`, `Stokrotka`, `Frisco`, `Selgros`, `Makro`, `Delikatesy Centrum`, `Intermarché`

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

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

Some stores (Makro, Selgros) have 30K+ 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": "Masło ekstra 200g",
    "price": 2.29,
    "amount": "250 g",
    "store": "LIDL",
    "imageUrl": "https://...",
    "validUntil": null,
    "lastUpdated": "2026-05-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. meat, cheese or fruit a store lists as `ok. 500 g`; no store sends these yet). 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": 2.29, "currency": "PLN", "store": "LIDL", "amount": "250 g", "imageUrl": "..." },
  "history": [
    { "price": 2.49, "recordedAt": "2026-04-26T00:00:00.000Z" },
    { "price": 2.29, "recordedAt": "2026-05-10T00:00:00.000Z" }
  ],
  "stats": {
    "minPrice": 2.29,
    "maxPrice": 2.49,
    "avgPrice": 2.39,
    "priceChangePercent": -9.11,
    "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": 27.90 }`. Their `history` holds one point per change of the per-kg price, and each point adds `unitPrice` (PLN per kg) and `amount` (the piece listed at that time, e.g. `"ok. 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 yoghurt 400 g -> 370 g) return `"basis": { "weighed": false, "unit": "kg", "currentUnitPrice": 12.57, "resized": true }`. Each `history` point then adds `unitPrice` (PLN 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": "Masło ekstra 200g", "price": 2.29, "amount": "250 g", "store": "LIDL", "imageUrl": "..." },
  "matches": [
    {
      "id": "...",
      "name": "Masło K-Classic 200g",
      "price": 1.99,
      "amount": "250 g",
      "store": "Kaufland",
      "imageUrl": "...",
      "validUntil": null,
      "lastUpdated": "2026-05-26T04:11:00.000Z",
      "similarity": 0.94,
      "priceDifference": 0.30,
      "savingsPercent": 10
    }
  ],
  "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={PLN}`

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 0.15) — minimum PLN price difference

**Response:**
```json
{
  "savings": [
    {
      "cheapestProduct": { "id": "...", "name": "...", "price": 1.49, "store": "LIDL", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "expensiveProduct": { "id": "...", "name": "...", "price": 2.29, "store": "Carrefour", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "savings": 0.80,
      "savingsPercent": 40,
      "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=maslo&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/mleko`
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=0.5`
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://antonka.pl/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://antonka.pl/support
Site description for LLMs: https://antonka.pl/llms.txt
