№ IV · MMXXVI projects

№ XI · 2026 · parked, not buried

Odvolanie

A Slovak-language web tool that generates a formal appeal letter for a traffic or parking pokuta, grounded in real slov-lex statute text and delivered as a PDF five seconds after Stripe pays. 76 tests, 31 commits, end-to-end TDD. Parked before launch — the build is done; the operational surface is not.

What it is

Slovak drivers get one of ten parking-or-traffic pokuta categories. They pick a situation, fill 5–8 form fields, pay 5 € on a Stripe Payment Link, and a webhook kicks off the pipeline: scrape-indexed slov-lex paragraphs are retrieved by BM25, a Llama-3.3-70B call fills in the legal-reasoning block, every § the model emits is checked against the retrieved corpus, a WeasyPrint PDF is composed, and the letter lands in the user's inbox with a 7-day download link.

Ten templates cover the realistic parking and traffic offence surface: TAB-karta disputes, kamerové merania, jazda na červenú, blokovka preskúmanie, formal odvolanie, žiadosť o odpustenie zvyšku zákazu činnosti, splátkový kalendár. Each is a hand-curated Jinja file with explicit ai_fill blocks the LLM completes and explicit statute_cite markers the retriever resolves against the indexed paragraphs.

Why it exists

Two beats. The first: there is no Slovak-language tool that does this. Every adult Slovak driver has, at some point, considered appealing a pokuta and given up because the procedural shape is unobvious. Templates exist on epravo.sk and zakony.sk, but they are static and require the user to know which template applies, what § to cite, and what tone to write in. Odvolanie collapses that decision into a 60-second flow.

The second: a stress test for grounded LLM output. Generic AI-letter sites produce confidently-wrong references because the model hallucinates statute numbers. Odvolanie indexes the actual slov-lex corpus, only allows citations the retriever found, and strips any that don't survive validation. If the LLM hallucinated every reference, the user gets a refund email instead of a legally-empty letter.

How the citation guardrail works

The most load-bearing decision in the spec is that the LLM is allowed to write legal prose but not allowed to invent § numbers. Three rails, all enforced at runtime:

Architecture

┌──────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ FastAPI +    │ ←→ │ SQLite + FTS5    │    │ Groq Llama-3.3   │
│ Jinja + HTMX │    │ statutes_fts     │ ←→ │ + Gemini Flash   │
│ Tailwind CDN │    │ (BM25 retrieval) │    │ (fallback)       │
└──────┬───────┘    └──────────────────┘    └──────────────────┘
       │                     ▲                       ▲
       │                     │                       │
       ▼                     │                       │
┌──────────────┐    ┌────────┴─────────┐    ┌────────┴─────────┐
│ Stripe       │    │ Citation         │    │ WeasyPrint       │
│ Payment Link │    │ validator        │    │ (PDF render)     │
└──────┬───────┘    │ (strip-on-fail)  │    └────────┬─────────┘
       │            └──────────────────┘             │
       ▼                                             ▼
┌──────────────┐                            ┌──────────────────┐
│ Stripe       │                            │ Zoho SMTP        │
│ webhook      │ ──── (BackgroundTask) ───→ │ (3-attempt retry)│
│ (idempotent) │                            └──────────────────┘
└──────────────┘

Single VPS, single Docker container on a shared alphafeed_default network alongside other Receptiv subdomains. The webhook handler returns 200 immediately and dispatches the generation pipeline via BackgroundTasks — Stripe never times out. The letters INSERT and the orders UPDATE run in one explicit SQLite transaction so a crash mid-pipeline can't leave the user paid but unfulfilled. Idempotency is enforced by a UNIQUE constraint on stripe_events.event_id; duplicate webhook deliveries are silently absorbed.

The failure surface

Five distinct failure paths exist, each with a deliberate behaviour:

How it was built

Same discipline pattern as Mintly, slightly tightened. One brainstorm session → single design spec → 18-task TDD plan with explicit failing tests before any implementation → subagent-driven execution with two-stage review (spec compliance, then code quality) on every task. 31 commits, no batching, every review finding fixed before the next task started.

The most useful artefact from the process is the spec document itself (docs/superpowers/specs/) — eight sections covering scope, data model, generation pipeline, error handling, legal posture, testing, distribution, timeline. It is what convinced me the build was bounded enough to commit to.

Why it isn't live

The shell is done. What is not done is the surrounding operational and legal work — none of which is technical, all of which is real:

With Receptiv as the active income line and a hard limit on how many parallel public surfaces I want maintaining, this gets a clean park: build complete, documented, GitHub-mirrored, ready to revive when the rest of the surface is ready or the moment is right.

Stack

What's actually here

The repo is a complete, deployable reference for a single-developer Slovak-language SaaS — spec, plan, tests, deploy artefacts, README runbook. Every commit message is a complete sentence. No TODOs, no placeholder error handling, no "add validation later." The pre-deploy gaps are enumerated in the final-review commit and in the README; nothing is hidden.

If a Slovak driver ever asks me to fix their pokuta and the timing is right, the shell is already there. Until then it's a study in retrieval-grounded LLM output, Slovak-law parsing, and the exact shape of "I am not going to launch this right now."