# PaperTrail
### *Every expense. Proven.*
**Project Spec & Design Doc · HuggingFace Build Small Hackathon 2026**
---
| Field | Detail |
|---|---|
| **Hackathon Track** | Chapter One — Backyard AI |
| **Submission Deadline** | June 15, 2026 |
| **Team** | 2 people |
| **Target Badges** | All 6 — Off the Grid, Llama Champion, Off-Brand, Well-Tuned, Sharing is Caring, Field Notes |
| **Model** | Nemotron 3 Nano Omni 31B + LoRA (single model — vision + text) |
| **Inference Runtime** | llama.cpp — RTX 3090 (primary) / Modal A100-80GB (fine-tune) |
| **Collaboration** | Tailscale Funnel — brother's Gradio app hits inference server on 3090 |
| **Fine-Tune Budget** | $250 Modal hackathon credits |
| **UI Framework** | Gradio — custom theme |
| **Storage** | SQLite — fully local, no cloud dependency |
---
## Table of Contents
1. [Problem](#1-problem)
2. [Solution](#2-solution)
3. [Architecture](#3-architecture)
4. [Data & Fine-Tuning](#4-data--fine-tuning)
5. [Database Schema](#5-database-schema)
6. [Tiered MVP](#6-tiered-mvp)
7. [7-Day Sprint (June 8–15)](#7-7-day-sprint-june-815)
8. [Contest Strategy](#8-contest-strategy)
9. [Constraints & Risks](#9-constraints--risks)
10. [Team Work Split](#10-team-work-split)
11. [Appendix](#appendix--quick-reference)
---
## 1. Problem
Anyone who claims medical expenses against a tax-advantaged account — HSA in the US, METC in Canada, Krankheitskosten in Germany, HMRC self-employment claims in the UK — is legally required to retain documentation for every expense. In practice, most people manage this with a folder of receipts and good intentions. When an audit notice arrives, the process is entirely manual.
**Core pain points:**
- No quick way to verify whether a past expense is eligible under applicable tax rules
- Document formats vary wildly — paper receipts, insurance EOBs, emailed invoices, scanned superbills
- Existing tools require cloud upload, creating a privacy risk with sensitive medical documents
- Documentation gaps only surface during an audit, not during the year when they can still be fixed
> **Real User:** Primary demo user is a team member — an actual HSA account holder with real receipts across pharmacy, dental, and medical categories. On-screen demo uses synthetic documents only. Real receipts used for offline personal testing only.
---
## 2. Solution
PaperTrail is a fully local, privacy-first Gradio application. Drop in any medical expense document and a single fine-tuned Nemotron 3 Nano Omni 31B model extracts structured fields, classifies eligibility against a static IRS Pub 502 lookup table, scores audit readiness out of 100, and stores a correctable record to SQLite. Nothing leaves the machine.
### 2.1 Why a Single Omni Model
Nemotron 3 Nano Omni 31B handles vision and text in a single model — no pipeline handoffs between a VLM and a separate language model. Despite 31B total parameters, only ~3B are active per inference step (MoE architecture), making it fast on a single 3090. It delivers strong document-intelligence and OCR performance directly relevant to receipt and EOB extraction. 256K context window handles multi-page documents trivially.
> **Parameter Budget Check**
> - Nemotron 3 Nano Omni total params: **31B ✓** (hackathon limit: 32B; model card: 3.1×10¹⁰, branded "Nano Omni 30B")
> - Active params per inference step: **~3B** (MoE — fast on single GPU)
> - Single model replaces the prior two-model architecture entirely
> **⚠️ Reasoning Model Note**
> This is a **Reasoning** model. For extraction tasks use **Instruct mode** (temp 0.2, top_k 1). The model response includes a `reasoning_content` / `…` block before the JSON — **strip it before JSON parsing**. The mock stub must mirror this response shape so brother's UI parser works identically against both stub and live server.
### 2.2 Core Features (V1 — must ship)
**Structured Extraction**
The fine-tuned model extracts merchant, date, amount, line items, provider type, and per-field confidence scores as structured JSON. Low-confidence fields are highlighted for manual correction. Extraction quality is the product — everything else depends on it.
**Audit Readiness Score**
Every record receives a 0–100 score based on field completeness and confidence. The panel shows exactly what is present and what is missing — actionable, not opaque.
```
Audit Readiness 87 / 100
✓ Provider name
✓ Service date
✓ Amount paid
✗ Itemised line items (−8)
✗ Provider NPI / tax ID (−5)
```
**Eligibility Classification**
Each expense is classified as `eligible / ineligible / partial / flagged` using a static lookup table governed by **IRC §213(d) / IRS Pub 969 / Notice 2004-2**, with IRS Pub 502 as the medical-expense base table. HSA-specific overrides apply where HSA rules differ from the general medical deduction (OTC drugs and menstrual products are **eligible** for HSA post-CARES Act 2020, even without a prescription). Fast, reliable, explainable — no LLM reasoning chain required for standard cases. Every verdict carries the IRS rule reference.
**Manual Correction UI + Training Loop**
Low-confidence fields are editable inline. Every correction is written to SQLite and simultaneously appended to `corrections.jsonl` in training-ready `(image, instruction, corrected JSON)` format — building a verified dataset for future fine-tuning runs automatically.
**Local-First Privacy**
All inference runs via llama.cpp on the user's own GPU. No API keys, no cloud upload, no telemetry. Medical documents never leave the machine.
### 2.3 Features Deliberately Cut
> ❌ **Intentionally out of scope**
> - OCR reconciliation layer — single fine-tuned model handles this; add back only if extraction quality is poor post fine-tune
> - Separate orchestrator model — Nemotron Omni replaces the prior two-model architecture entirely
> - Audio ingestion — llama.cpp audio support for this model is broken; not a demo feature
> - Gap detection — needs sufficient history to be meaningful; V3 stretch only
> - Document classes beyond receipts and EOBs — scope risk; invoices and statements are V3
> - CPU HF Space optimisation — Space hosts UI only; inference runs on live 3090 via tunnel
> - Vector database — all queries are structured (date, merchant, eligibility); SQLite with indexes is sufficient
---
## 3. Architecture
| Component | Tool | Role | Notes |
|---|---|---|---|
| Pre-processor | Python / OpenCV + pdf2image | Image prep and doc-type routing | Deskew, contrast enhance, PDF→image |
| Inference | Nemotron 3 Nano Omni 31B + LoRA | Extraction (vision + text) | Single model — vision + language; strip reasoning block before parse |
| Eligibility DB | Static JSON / Python dict | IRC §213(d) / IRS Pub 502 lookup table | No LLM call for standard cases |
| Storage | SQLite + JSONL | Local persistence + training loop | Corrections auto-append to JSONL |
| Export | Python / csv | Audit report generation | Sorted by Audit Readiness Score |
| UI | Gradio | User interface | Custom theme, hosted on HF Space (UI only — inference via tunnel) |
### 3.1 Pipeline Flow
```
User drops receipt photo or EOB PDF into Gradio UI
↓
[Pre-processor]
Detect doc type, deskew, normalise contrast, render PDF pages to images
↓
[Nemotron 3 Nano Omni 31B — fine-tuned LoRA]
via llama-server OpenAI-compatible endpoint (image_url base64 content block)
→ strip reasoning_content / block
→ structured JSON with per-field confidence scores
↓
[Static IRS Pub 502 / IRC §213(d) Eligibility Lookup]
provider_type + line items → eligible / ineligible / partial / flagged
+ IRS rule citation
↓
[Audit Readiness Score]
0–100 based on field completeness and confidence
↓
[SQLite write]
Full record persisted locally
↓
[Gradio UI]
Low-confidence fields highlighted for correction
Every correction → SQLite + corrections.jsonl (training-ready)
```
### 3.2 Inference Commands
> **⚠️ CUDA Version:** Do **not** use CUDA 13.2 — known gibberish output for this model. Use CUDA 12.x.
> **⚠️ Ollama:** Ollama multimodal does not work for this model (separate mmproj architecture). Use llama.cpp only.
The command below is derived from the confirmed-working Unsloth Studio invocation (tested June 8 — 21.8 tok/s on RTX 3090, 100% extraction accuracy on real docs).
```bash
MODEL_DIR=/path/to/nemotron-gguf
MODEL=${MODEL_DIR}/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_XL.gguf
MMPROJ=${MODEL_DIR}/mmproj-BF16.gguf
# Server mode — production (thinking on: ~71s, highest accuracy)
llama-server \
-m ${MODEL} \
--mmproj ${MMPROJ} \
--port 8080 \
--host 0.0.0.0 \
--flash-attn on \
--no-context-shift \
--fit on \
--threads -1 \
--jinja \
--spec-default \
--chat-template-kwargs '{"enable_thinking": true}' \
-c 32768 \
--parallel 1
# Demo / speed mode (thinking off: much faster, slightly lower accuracy)
llama-server \
-m ${MODEL} \
--mmproj ${MMPROJ} \
--port 8080 \
--host 0.0.0.0 \
--flash-attn on \
--no-context-shift \
--fit on \
--threads -1 \
--jinja \
--spec-default \
--chat-template-kwargs '{"enable_thinking": false}' \
-c 32768 \
--parallel 1
```
> **Context budget:** Unsloth Studio loaded with `-c 4096`; a small invoice + receipt used ~2700 tokens of that. For multi-page EOBs, test `-c 32768` (may need VRAM headroom check on 3090). If 32768 OOMs, process one page at a time and concatenate results.
>
> **Thinking mode tradeoff:** `enable_thinking: true` → 71s latency, higher accuracy (tested 100% on real docs). `enable_thinking: false` → much faster, suitable for demo; quality difference worth measuring and noting in the blog post.
>
> The server exposes an **OpenAI-compatible `chat/completions` endpoint**. Send images as `image_url` with base64-encoded data. Strip `reasoning_content` / `…` from the response before JSON parsing.
### 3.3 Hardware & Hosting
**Local Dev & Demo — RTX 3090 (24 GB VRAM + 32 GB RAM)**
```
Q4_K_XL GGUF requires ~25 GB — fits across VRAM + system RAM with layer offload
llama-server bound to 0.0.0.0 on port 8080; exposed via Tailscale Funnel for Gradio Space
Target latency: < 20 seconds end-to-end per document
```
**Brother's Machine — RTX 3060 Ti (8 GB VRAM)**
```
Cannot run 31B model locally — 8 GB VRAM insufficient even with full offload
Days 1–5: Mock stub locally (returns realistic fake JSON matching live server shape) for UI development
Days 6–7: Gradio app points to 3090 inference server via Tailscale Funnel
Tailscale setup: ~10 minutes, zero router config, works across any network
Server command (your machine):
llama-server \
-m ${MODEL_DIR}/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_XL.gguf \
--mmproj ${MODEL_DIR}/mmproj-BF16.gguf \
--port 8080 --host 0.0.0.0 \
--n-gpu-layers 99 --jinja --ctx-size 32768
Brother's .env:
LLAMA_BASE_URL=http://:8080
Integration contract (Day 1 deliverable):
POST /v1/chat/completions — image_url base64 in, JSON (after stripping reasoning) out
Mock stub returns identical shape so brother's parser works against both stub and live server.
```
**Fine-Tuning — Modal (A100-80GB, $250 hackathon credits)**
```
Nemotron 3 Nano Omni vision LoRA via Unsloth (NVIDIA-confirmed day-zero support)
A100-80GB required — 30B+ LoRA needs ~60-80GB VRAM; A100-40GB is insufficient
Modal A100-80GB rate: ~$2.50/hr → ~$5-10 for a 2-4h run (well within $250 budget)
Estimated training time: 2–4 hours for 80 examples / 3 epochs
Day 1 gate: confirm credits are granted AND A100-80GB is available on the credit tier.
If 80GB unavailable: attempt QLoRA-4bit on A100-40GB as documented fallback (same Day 1 spike gates it).
Publish adapter to HF Hub on completion.
```
**HF Space — Submission**
```
Gradio Space hosts the UI only — no inference in-Space.
Inference calls the live RTX 3090 over Tailscale Funnel during the judging window.
This is on-brand for "Off the Grid": own hardware, no third-party cloud API.
README documents:
- Local-first design intent and hardware requirements
- That inference runs on author's 3090 via tunnel during judging
- Pre-recorded demo video link (backstop if tunnel is unreachable at judging time)
- Scheduled live window for judges who want to test interactively
```
---
## 4. Data & Fine-Tuning
### 4.1 Fine-Tuning Objective
**Primary path (vision LoRA):** Instruction-following supervised fine-tune: given an image of a medical expense document plus a fixed extraction instruction, the model outputs a structured JSON object. Training examples are `(image, instruction, JSON response)` triples. Gated by the Day-1 full de-risk spike.
**Fallback path (text LoRA):** If the vision-LoRA→GGUF+mmproj round-trip fails the Day-1 spike, switch to a text LoRA: the base Omni model already does OCR/extraction via prompting; the LoRA maps extracted text → normalized eligibility JSON. Text LoRA merges to GGUF cleanly and still earns the "Well-Tuned" badge.
```json
// Training format — each example (vision LoRA)
{
"instruction": "Extract all expense fields from this document as JSON.",
"response": {
"merchant": "City Dental Group",
"date": "2026-03-14",
"amount": 85.00,
"provider_type": "dental",
"line_items": ["cleaning", "x-rays"],
"confidence": {
"merchant": 0.97,
"date": 0.95,
"amount": 0.99
}
}
}
```
### 4.2 Dataset Split
| Split | Size | Purpose |
|---|---|---|
| Train | ~68 docs (80%) | LoRA fine-tuning on Modal |
| Validation | ~8 docs (10%) | Monitor overfitting during training |
| Test | ~8 docs (10%) | Held-out eval — report before/after field accuracy in blog post and demo |
**Primary metric:** per-field exact-match accuracy on `amount`, `date`, and `merchant` across the test set. Baseline (un-fine-tuned model) recorded first so improvement is quantifiable and citable.
### 4.3 Synthetic Document Plan
| Document Type | Tool | Count | Key Scenarios |
|---|---|---|---|
| Pharmacy receipt | ReportLab | 25 | Prescriptions (eligible) vs OTC (also eligible post-CARES Act 2020); cosmetic/gym ineligible |
| Insurance EOB | ReportLab | 25 | Single and multi-page; service amount vs. patient responsibility |
| Doctor visit receipt | ReportLab | 15 | Co-pays, superbills with procedure codes |
| Handwritten receipt | Pillow + fonts | 10 | Caveat / Homemade Apple font overlaid on receipt template |
| Ineligible edge cases | ReportLab | 10 | Gym membership, vitamins, cosmetic — tests eligibility refusal |
| Hard negatives | ReportLab | 15 | Partial receipts, obscured amounts, missing dates, multi-page totals |
| **Total** | | **100** | |
### 4.4 Image Degradation Pipeline (Albumentations)
All synthetic docs pass through an augmentation pipeline before training. Clean PDFs will not generalise to real-world phone photographs without this step.
| Augmentation | Simulates |
|---|---|
| Random rotation ±15° | Document photographed at a slight angle |
| JPEG compression artifacts | Low-quality phone camera or MMS-forwarded receipt |
| Brightness / contrast shift | Faded thermal receipt paper or poor lighting |
| Gaussian blur | Out-of-focus phone camera shot |
| Perspective warp | Receipt photographed from the side |
| Salt-and-pepper noise | Low-DPI scanner output |
### 4.5 Corrections-to-Training Loop
Every manual field correction in the UI is written to SQLite and appended to `corrections.jsonl` in training-ready format. This gives PaperTrail a production ML story: every correction is gold-standard training data for the next fine-tuning run.
```python
# Appended on every user correction
{
"image_path": "receipts/doc_00042.jpg",
"instruction": "Extract all expense fields from this document as JSON.",
"response": { ...corrected_fields... },
"source": "human_correction",
"corrected_at": "2026-06-10T14:23:00"
}
```
---
## 5. Database Schema
### 5.1 `documents` table
Stores one record per uploaded file.
```sql
CREATE TABLE documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
file_path TEXT NOT NULL, -- local path to stored image/PDF
doc_type TEXT NOT NULL, -- 'receipt_photo' | 'eob_pdf' | 'invoice' | 'statement'
upload_timestamp TEXT NOT NULL, -- ISO 8601
raw_model_output TEXT, -- full JSON from Nemotron (audit log)
status TEXT DEFAULT 'pending' -- 'pending' | 'validated' | 'flagged' | 'manual_review'
);
CREATE INDEX idx_documents_status ON documents(status);
CREATE INDEX idx_documents_upload ON documents(upload_timestamp);
```
### 5.2 `expenses` table
Stores one record per extracted line item. One document can produce multiple expense records (e.g. an EOB with several services).
```sql
CREATE TABLE expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id INTEGER NOT NULL REFERENCES documents(id),
-- Extracted fields
merchant TEXT,
provider_type TEXT, -- 'pharmacy' | 'dental' | 'vision' | 'medical' | 'other'
service_date TEXT, -- ISO 8601 date string
amount REAL,
line_item_description TEXT,
patient TEXT,
-- Eligibility
hsa_eligible TEXT, -- 'eligible' | 'ineligible' | 'partial' | 'flagged'
irs_citation TEXT, -- e.g. 'IRS Pub 502 p.8 — Dental Treatment'
eligibility_confidence REAL, -- 0.0 – 1.0
-- Extraction quality
extraction_confidence REAL, -- overall confidence across all fields
low_confidence_fields TEXT, -- JSON array of field names below threshold
manually_corrected INTEGER DEFAULT 0, -- 1 if user edited any field
-- Audit readiness
audit_readiness_score INTEGER, -- 0–100
readiness_breakdown TEXT, -- JSON object: field → points deducted
-- Deduplication
duplicate_of INTEGER REFERENCES expenses(id),
duplicate_confidence REAL
);
CREATE INDEX idx_expenses_doc_id ON expenses(doc_id);
CREATE INDEX idx_expenses_service_date ON expenses(service_date);
CREATE INDEX idx_expenses_eligible ON expenses(hsa_eligible);
CREATE INDEX idx_expenses_merchant ON expenses(merchant);
```
### 5.3 `corrections` table
Full audit trail of every manual field edit. Used both for compliance ("AI-extracted, human-verified") and as a source of training data.
```sql
CREATE TABLE corrections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
expense_id INTEGER NOT NULL REFERENCES expenses(id),
field_name TEXT NOT NULL, -- e.g. 'amount', 'service_date', 'merchant'
original_value TEXT,
corrected_value TEXT,
corrected_at TEXT NOT NULL -- ISO 8601 timestamp
);
CREATE INDEX idx_corrections_expense ON corrections(expense_id);
```
### 5.4 Common Queries
```sql
-- YTD eligible expenses total
SELECT SUM(amount) FROM expenses
WHERE hsa_eligible = 'eligible'
AND service_date >= '2026-01-01';
-- All flagged / low-confidence records needing review
SELECT e.*, d.filename FROM expenses e
JOIN documents d ON e.doc_id = d.id
WHERE e.hsa_eligible = 'flagged'
OR e.audit_readiness_score < 70
ORDER BY e.audit_readiness_score ASC;
-- Potential duplicates
SELECT * FROM expenses
WHERE duplicate_of IS NOT NULL;
-- Audit report export — sorted by risk
SELECT
d.filename,
e.service_date,
e.merchant,
e.amount,
e.hsa_eligible,
e.irs_citation,
e.audit_readiness_score,
e.manually_corrected
FROM expenses e
JOIN documents d ON e.doc_id = d.id
ORDER BY e.audit_readiness_score ASC;
```
---
## 6. Tiered MVP
### ✅ V1 — Guaranteed Finish (Days 1–5)
- Synthetic data generation with Albumentations degradation pipeline
- Nemotron 3 Nano Omni 31B LoRA fine-tune on Modal A100-80GB — eval split with before/after accuracy metrics
- **Vision LoRA** (image→JSON): default path, gated by Day-1 full de-risk spike
- **Text LoRA** (extracted-text→JSON): committed fallback if vision-LoRA→GGUF round-trip fails Day-1 spike
- Receipt + EOB extraction via fine-tuned model (structured JSON output; reasoning block stripped)
- Eligibility classification via static IRC §213(d) / IRS Pub 502 lookup table (OTC drugs eligible post-CARES Act)
- SQLite storage with all three tables + corrections-to-JSONL training loop
- Manual correction UI with low-confidence field highlighting
- Audit Readiness Score (0–100) with missing field breakdown
### 🔶 V2 — Strong Submission (Days 6–7)
- Audit report CSV export sortable by readiness score
- Evaluation dashboard showing fine-tune before/after metrics
- Custom Gradio theme and full UI polish
- Duplicate detection (same merchant + amount + date window)
- Demo video + Field Notes blog post
### 🔴 V3 — Stretch (only if clearly ahead of schedule)
- Multi-page EOB document linking
- International document format variants (UK, Canada, Germany)
- Gap detection across full dataset
---
## 7. 7-Day Sprint (June 8–15)
| Day(s) | Phase | Goal | Owner |
|---|---|---|---|
| 1 (Jun 8) | Setup & Fine-Tune Spike | **Vision model confirmed working** (Jun 8 test: 21.8 tok/s, 100% accuracy on real docs, Unsloth Studio). Remaining Day 1: (1) Confirm Modal credits + A100-80GB access. (2) Train tiny vision LoRA on ~5-10 docs → merge → convert to GGUF+mmproj → verify extraction still works. **If loop fails → text-LoRA fallback.** (3) Set up Tailscale Funnel (expose port 8080). (4) Brother sets up mock stub matching real OpenAI-compatible response shape (with reasoning block). (5) Test `-c 32768` context on 3090 — needed for multi-page EOBs. | Both |
| 2–3 (Jun 9–10) | Data | Generate 100 synthetic docs (ReportLab + Pillow). Run Albumentations degradation pipeline. Create 80/10/10 train/val/test split. Record baseline extraction accuracy on test set. | Brother (gen) / You (eval) |
| 3–4 (Jun 10–11) | Fine-Tune | Run full Nemotron 3 Nano Omni LoRA on Modal A100-80GB. Record before/after field accuracy on test set. Publish adapter + dataset to HF Hub. | You |
| 4–5 (Jun 11–12) | Pipeline | Build pre-processor (OpenCV + pdf2image). Wire fine-tuned model extraction end-to-end (OpenAI-compatible client, strip reasoning, parse JSON). Build static eligibility lookup. Audit Readiness Score logic. | You |
| 5–6 (Jun 12–13) | Storage & UI Core | SQLite schema (all 3 tables) + corrections JSONL loop. Manual correction UI. Readiness score widget. Full upload → result flow working end-to-end against real model via Tailscale. | Both |
| 6–7 (Jun 13–14) | V2 Features + Demo | Audit report CSV export. Duplicate detection. Eval dashboard. Gradio custom theme and polish. Demo video with synthetic docs. Field Notes blog post. Social media post copy. | Both |
| 7 (Jun 15) | Ship | HF Space deployment (UI + tunnel README). Agent trace publish. Full checklist review. Submit by deadline. V3 only if everything else is done. | Both |
---
## 8. Contest Strategy
### 8.1 Track
**Chapter One — Backyard AI.** Judged on: real problem, real user, honest fit with small-model constraints, Gradio polish. PaperTrail qualifies on all four axes. The international reframe (HSA / METC / HMRC / Krankheitskosten) broadens appeal without changing the core scope.
### 8.2 Target Badges
| Badge | Requirement | How We Qualify |
|---|---|---|
| 🔌 Off the Grid | No cloud APIs at runtime | All inference via local llama.cpp on 3090; Gradio Space UI calls live 3090 over Tailscale Funnel — own hardware, no third-party API |
| 🦙 Llama Champion | Runs via llama.cpp | `llama-server` with `--mmproj` for vision + text inference on 3090 |
| 🎨 Off-Brand | Custom Gradio UI | Custom theme, document viewer, readiness score widget, dashboard |
| 🎯 Well-Tuned | Fine-tuned model published to HF Hub | Nemotron 3 Nano Omni LoRA adapter (vision or text) trained on Modal, published |
| 📡 Sharing is Caring | Publish agent trace to HF Hub | Full pipeline traces logged and pushed as HF dataset |
| 📓 Field Notes | Blog post on HF | Before/after fine-tune metrics, VLM limits on real receipts, IRS lookup design decisions |
### 8.3 Submission Checklist
- [ ] Confirm team is registered under hackathon HF org — verify before Day 1
- [ ] Confirm Nemotron sponsor prize criteria in hackathon Discord — check Day 1
- [ ] Confirm Modal credits granted and A100-80GB tier available — check Day 1
- [ ] App submitted under hackathon HF org namespace
- [ ] HF Space running — UI only, inference via Tailscale Funnel to live 3090, documented in README
- [ ] Pre-recorded demo video uploaded and linked in README (backstop if tunnel unreachable at judging)
- [ ] Scheduled live judging window noted in README (so judges can request a live session)
- [ ] Demo video: synthetic receipt → extraction → readiness score → audit report
- [ ] Social media post live before deadline
- [ ] LoRA adapter published to HF Hub
- [ ] Synthetic dataset published to HF Hub
- [ ] Agent trace dataset published to HF Hub
- [ ] Field Notes blog post published on HF
---
## 9. Constraints & Risks
### 9.1 Hard Constraints
- Total model parameters ≤ 32B — Nemotron 3 Nano Omni: **31B ✓** (model card: 3.1×10¹⁰; branded "30B"; well within 32B limit)
- UI must be Gradio
- Space submitted under hackathon HF org
- Submission deadline: June 15, 2026
### 9.2 Risk Register
| Risk | Likelihood | Mitigation |
|---|---|---|
| 3090 runs OOM on Q4_K_XL GGUF | Low | **Jun 8 test: model runs on 3090 at 21.8 tok/s.** Test `-c 32768` for multi-page EOBs; drop to `-c 4096` per-page if needed. Q3_K_M fallback (~20GB) if OOM. |
| Context limit too small for multi-page EOBs | Medium | 2 small docs used ~2700 of 4096 tokens. Test 32768 context; if VRAM-constrained, split at page boundaries and merge results in pipeline. |
| Thinking mode too slow for demo (71s observed) | Low | Use `enable_thinking: false` for demo mode. Measure accuracy delta; document tradeoff in blog post. |
| Vision-LoRA adapter cannot be converted to GGUF+mmproj for llama.cpp | Medium | Day-1 fine-tune spike gates the decision. Text-LoRA fallback committed — still earns Well-Tuned badge. |
| Modal A100-80GB unavailable on credit tier | Medium | Confirm on Day 1. QLoRA-4bit on A100-40GB is documented fallback (same spike gates it). |
| Modal fine-tune cost overruns | Low | $250 budget. Est. spend $5-10 training + $8-10 dev inference. Well within budget. |
| Tailscale tunnel / host unavailable during judging | Medium | Pre-recorded demo video as backstop; scheduled live window in README. |
| Nemotron sponsor prize criteria unclear | Medium | Check Discord on Day 1. Single-model Nemotron is the strongest possible qualifying position. |
| Fine-tune overruns Days 3–4 | Medium | Time-box strictly. Submit base model if needed — write honestly about attempt in blog post. |
| Reasoning block in model output breaks JSON parse | Low | Strip `reasoning_content` / `…` before parsing. Mock stub mirrors this shape from Day 1. |
---
## 10. Team Work Split
| Area | Owner |
|---|---|
| llama.cpp + Nemotron GGUF + mmproj setup, layer offload tuning | You |
| Tailscale Funnel setup (expose 3090 inference server) | You |
| Pre-processor — OpenCV + pdf2image | You |
| Fine-tuning pipeline on Modal (Unsloth) | You |
| Extraction prompt engineering + JSON schema (reasoning block stripping) | You |
| Static IRC §213(d) / IRS Pub 502 eligibility lookup table | You |
| Audit Readiness Score logic | You |
| SQLite schema + corrections JSONL loop | Both |
| Synthetic data generation scripts (ReportLab + Pillow) | Brother |
| Albumentations degradation pipeline | Brother |
| Mock inference stub (matching real server response shape, with reasoning block) | Brother |
| Gradio UI — upload flow, extraction results, corrections | Brother |
| Gradio UI — dashboard, search, audit report export | Brother |
| HF Space deployment + README (tunnel docs + demo video link) | Brother |
| Demo video recording and editing | Both |
| Field Notes blog post | You |
| Social media post | Both |
| Agent trace logging + HF dataset publish | Both |
---
## Appendix — Quick Reference
### Model Download
```bash
# Correct repo and filenames — download both model and multimodal projector
huggingface-cli download unsloth/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF \
--include "*UD-Q4_K_XL*" \
--include "*mmproj-BF16*"
# Model file: NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_XL.gguf
# Projector: mmproj-BF16.gguf
```
### llama.cpp Inference Commands
> Commands derived from confirmed-working Unsloth Studio invocation (Jun 8 — 21.8 tok/s, 100% accuracy on real docs).
```bash
MODEL_DIR=/path/to/nemotron-gguf
MODEL=${MODEL_DIR}/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_XL.gguf
MMPROJ=${MODEL_DIR}/mmproj-BF16.gguf
# Server mode — production (thinking on: ~71s latency, highest accuracy)
llama-server \
-m ${MODEL} \
--mmproj ${MMPROJ} \
--port 8080 \
--host 0.0.0.0 \
--flash-attn on \
--no-context-shift \
--fit on \
--threads -1 \
--jinja \
--spec-default \
--chat-template-kwargs '{"enable_thinking": true}' \
-c 32768 \
--parallel 1
# Server mode — demo / speed (thinking off: much faster, slightly lower accuracy)
llama-server \
-m ${MODEL} \
--mmproj ${MMPROJ} \
--port 8080 \
--host 0.0.0.0 \
--flash-attn on \
--no-context-shift \
--fit on \
--threads -1 \
--jinja \
--spec-default \
--chat-template-kwargs '{"enable_thinking": false}' \
-c 32768 \
--parallel 1
```
### Brother's Mock Stub (local UI dev — no GPU needed)
```python
# Returns shape identical to the live llama-server OpenAI-compatible response
# (including reasoning block) so brother's UI parser works against both stub and server.
def mock_extract(image):
# Simulates full OpenAI-compatible chat/completions response shape
return {
"choices": [{
"message": {
"role": "assistant",
"reasoning_content": "This is a pharmacy receipt...",
"content": """{
"merchant": "City Pharmacy",
"date": "2026-05-14",
"amount": 42.50,
"provider_type": "pharmacy",
"line_items": ["lisinopril 10mg — 30 day supply"],
"confidence": {
"merchant": 0.95,
"date": 0.98,
"amount": 0.99,
"line_items": 0.87
}
}"""
}
}]
}
# Caller strips reasoning_content before JSON parsing:
def extract_json(response):
content = response["choices"][0]["message"]["content"]
return json.loads(content)
```
### IRS Pub 502 Eligibility Lookup (structure)
```python
# Governed by IRC §213(d) / IRS Pub 969 / Notice 2004-2.
# IRS Pub 502 is the medical-expense base table; HSA-specific overrides apply.
# Key override: OTC drugs and menstrual products are HSA-ELIGIBLE post-CARES Act (2020),
# even without a prescription (IRC §223(d)(2)(A) as amended).
ELIGIBILITY = {
"dental": {"status": "eligible", "citation": "IRS Pub 502 — Dental Treatment"},
"vision": {"status": "eligible", "citation": "IRS Pub 502 — Eye Exams"},
"prescription": {"status": "eligible", "citation": "IRS Pub 502 — Medicines"},
"otc_drug": {"status": "eligible", "citation": "IRC §223(d)(2)(A) — OTC drugs eligible post-CARES Act 2020"},
"menstrual": {"status": "eligible", "citation": "IRC §223(d)(2)(A) — Menstrual products eligible post-CARES Act 2020"},
"gym": {"status": "ineligible", "citation": "IRS Pub 502 — Weight-Loss Programs"},
"cosmetic": {"status": "ineligible", "citation": "IRS Pub 502 — Cosmetic Surgery"},
"chiropractic": {"status": "eligible", "citation": "IRS Pub 502 — Chiropractic Care"},
"therapy": {"status": "eligible", "citation": "IRS Pub 502 — Psychiatric Care"},
# ... extend for all Pub 502 categories
}
```
### Key Links
| Resource | URL |
|---|---|
| Hackathon | huggingface.co/build-small-hackathon |
| Nemotron 3 Nano Omni Reasoning GGUF | huggingface.co/unsloth/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF |
| Unsloth fine-tuning | github.com/unslothai/unsloth |
| Tailscale Funnel | tailscale.com/kb/1223/funnel |
| Albumentations | albumentations.ai |
| IRS Publication 502 | irs.gov/pub/irs-pdf/p502.pdf |
| IRS Publication 969 (HSA rules) | irs.gov/pub/irs-pdf/p969.pdf |
| Modal | modal.com |