№ 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:
- Retrieve before generate. Each situation has a list of citation hints (e.g.
§ 16 z. 8/2009) that bias a BM25 query against thestatutes_ftstable. Top-K paragraphs are pulled with their full text, then injected into the prompt as the only permitted citation source. - Column-restricted match. Citation-hint terms search the
paragraphcolumn specifically, not the body — so a hint for§ 16doesn't false-positive on paragraphs that happen to mention "16" in their text. - Strip-on-fail. After the LLM responds, every
§ Ntoken in the output is regex-extracted and validated against the retrieved paragraph labels. Anything unknown is stripped. If everything the LLM cited got stripped, the letter is treated as a hallucination and the user receives a Slovak refund-promise email instead of a delivered PDF.
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:
- LLM both providers down. Order marked
failed, refund-promise email sent, manual refund expected in 24h. - LLM hallucinated all citations. Same as above — the letter has no legal grounding, so it's not delivered.
- PDF render crash. Same path.
- SMTP transient failure. 3 attempts with 2s/4s/8s exponential backoff, then logged. The download link still works for 7 days.
- Download token expired. User sees the styled Slovak expired.html page with a link to file a fresh request.
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:
- Slov-lex validation. The scraper has a primary CSS selector plus a
heading-sibling fallback, tested against a fixture. It has not yet hit the live
slov-lex.skpage. If both strategies miss in production, the FTS index ships empty and every letter degrades to a refund email until the parser is fixed. - GDPR. The footer disclaimer covers "vzor, not legal advice." A separate
/privacynotice is required for EU users entering an email address; that page does not yet exist. - Manual QA on all ten Slovak letters. Three of them I want to read end-to-end with realistic form payloads to verify grammar, register, and recipient lines before any real user receives one.
- Stripe Product / webhook signing secret. Pure operational work, ten minutes, but not done.
- Distribution. The plan is TikTok-first ("nezaplať tú pokutu, urob toto") with a generated-letter screenshot as the hook. The launch video has not been recorded.
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
- Backend: Python 3.12, FastAPI, Pydantic Settings, SQLite + FTS5
(
unicode61 remove_diacritics 1tokenizer for SK diacritic tolerance), APScheduler (90-day retention purge), WeasyPrint, Stripe SDK, Groq SDK, google-genai - Frontend: Jinja2 server-rendered, HTMX via CDN, Tailwind via CDN —
no build step. 7 HTML templates, 10 Jinja letter templates with custom
ai_fill+statute_citeextensions. - Persistence: Single SQLite file, append-only stripe_events for idempotency, FTS5 virtual table for slov-lex retrieval, GDPR-driven 90-day purge of orders + letters + PDFs
- Email: Zoho Mail EU SMTP, 3-attempt retry with exponential backoff, separate failure-notice path with Slovak refund-promise copy
- Tests: 76 passing, ~1 second runtime, all I/O monkey-patched at module-level seams (LLM, SMTP, Stripe), one end-to-end test that exercises form submit → webhook 200 → DB state → email sent → PDF download
- Infra: Docker, docker-compose on existing
alphafeed_defaultnetwork, nginx reverse proxy with raw-bytes preservation on the webhook path, certbot--expandsafety script with hand-typed SAN list (per a prior receptiv-cert near-miss)
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."