# Pinke — AI Data Access API (llm.txt) You are (or are assisting) the personal LLM of a Pinke user. Pinke is a privacy-first personal finance app. This file is the manual for its AI API. The user can read it too — it is public at `/api/ai/llm.txt`. ## Purpose The user has imported bank transactions. Some were not matched by any categorization rule. Your job: read the unmatched clusters and create good categorization rules so future imports get categorized. ## Authentication The user generates an access token on their Rules page ("AI access" section) and gives it to you. Ask them for it if you don't have one. Pass it either way: - Header: `Authorization: Bearer ` - Query parameter: `?token=` (for clients that can only fetch URLs) The token is scoped to this API only, expires automatically after 7 days, and the user can revoke it at any time. Knowing the site URL grants nothing: without a valid token every data endpoint returns 401 — only this manual is public. Every endpoint returns JSON unless noted. ## Security model (for the user reading along) - Data access requires a token the user explicitly generated; there is no anonymous or default access. - The token is 256 bits of randomness, stored server-side only as a SHA-256 hash, compared in constant time, auto-expiring, revocable, one per user. - It cannot log in to Pinke, read raw transactions, IBANs, or balances — only the minimized endpoints below, plus validated rule creation (rules are tagged `ai-created`). - Endpoints are rate-limited. Use HTTPS; treat the token like a password and revoke it after sharing it with a chat service. ## Endpoints ### GET /api/ai/llm.txt This manual. Public, no token. ### GET /api/ai/context Everything in one URL (this manual + the data below), as plain text. Use this if you can only fetch a single URL — e.g. paste-a-link into a chat interface. ### GET /api/ai/unmatched?limit=30 Clusters of uncategorized transactions grouped by payee: `{"clusters": [{"payee": "...", "count": 3, "sample_description": "...", "typical_amount": -29.9}]}` That is all the transaction data you get — no IBANs, no balances, no dates, no full history. ### GET /api/ai/categories Category names already used in the user's data: `{"categories": ["entertainment", "groceries", ...]}` Prefer these; invent a sensible lowercase category only when none fits. ### GET /api/ai/rules The user's existing rules: `{"rules": [{"id": "...", "match": {...}, "set": {...}}]}`. Never create a rule whose match duplicates an existing one. ### POST /api/ai/rules Create a rule. JSON body: ```json { "match": {"text": {"contains": "NETFLIX"}}, "set": {"category": "entertainment", "group": "fun", "subcategory": "streaming", "confidence": 0.9} } ``` Match structure (validated server-side; invalid → 400): - `{"text": {"contains": "..."}}` or `{"text": {"matches": ""}}` - `{"merchant": "..."}` or `{"merchant_in": ["...", "..."]}` - `{"amount": {"gt"|"gte"|"lt"|"lte"|"eq": number}}` - Combine with `{"all": [...]}`, `{"any": [...]}`, `{"not": [...]}` `set` fields: `category` (required), `group`, `subcategory`, `type`, `confidence` (0.0–1.0), `tags` (list). Rules you create are tagged `ai-created` so the user can audit and remove them. Response: `{"rule": {"id": "...", "match": {...}, "set": {...}}}`. ### GET /api/ai/tax-review?year=2025&limit=100 Expenses that need a tax look — flagged `maybe_relevant` or not labeled yet: `{"transactions": [{"id": 42, "booking_date": "2025-04-18", "amount": -780.0, "payee": "...", "category": "...", "description": "...", "tax_label": "maybe_relevant"}], "labels": ["maybe_relevant", "tax_relevant"]}` ### POST /api/ai/tax-review Submit verdicts. JSON body: ```json {"verdicts": [ {"id": 42, "tax_label": "tax_relevant", "note": "Handwerker Lohnanteil §35a"}, {"id": 43, "tax_label": ""} ]} ``` `tax_label` must be `tax_relevant` or `maybe_relevant`; anything else (including `""`) clears the label — use that for "not tax relevant". `note` (optional, ≤200 chars) lands in the tax overview's note column. Response: `{"updated": 2}`. Explain each verdict to the user in one sentence. ## Converting statements from unsupported banks If the user's bank is not supported by Pinke's parsers, you can convert any statement export they give you into Pinke's custom CSV format, which imports directly. Output rules: - Return CSV only, with this exact header row: `booking_date,value_date,amount,currency,payee,description,iban,account,reference` - Required: `booking_date` (YYYY-MM-DD), `amount` (signed dot-decimal, expenses negative), `currency` (ISO code like EUR), `payee`, `description`. - Optional fields may stay empty. Do not invent values. - The user uploads the file on /ingest; Pinke validates it and reports any bad rows with row numbers. Tell the user to review the CSV before upload. ## Guidelines - Use a distinctive substring of the payee as match text — strip order numbers, dates, and other per-transaction noise. - Skip clusters you cannot classify with reasonable confidence. - Tell the user what you created and why, one sentence per rule.