Case study · practice build

A K-1 traced to a 1040, box by box.

A solo weekend build on two open tax standards — OTD and OpenTax — where every number on the return keeps a visible line back to the PDF box it came from. Built to learn the open code, not to sell tax software.

9h 02m
build time
16
build phases
118
tests passing
56
MCP tools

Synthetic data only. Every client, K-1 and figure in the demo is made up — it isn't tax advice and nothing is ever sent to the IRS. Independent project, not affiliated with or endorsed by Filed or Crimson Tree Software.

Why

Practice, not a product

I wanted hands-on time with the Open Tax Technology Alliance's open code — the OTD data standard for K-1s and the OpenTax 1040 engine — and a reason to use the Model Context Protocol for something with real domain complexity. This isn't a Drivkraft LLC product and I have no plan to enter the tax software market with it. It's a fully separate codebase from my actual consulting work, sharing nothing but a coat of paint. OTD and OpenTax are only usable because their authors made them open — putting this build out publicly under MIT, build log included, is my way of contributing back to that same spirit.

The interesting problem wasn't the UI. It was the seam between two standards that were never designed to meet: a document format for describing what's on a K-1, and a calculation engine that decides what to do with it. Building the bridge that lets those two open standards actually talk to each other — with a disposition ledger tracking every value end to end — became the actual point of the build.

Walkthrough

How it works

Four screens from the live demo, in the order a return actually moves through them.

01 · CASES

A sandbox per visitor, seeded and ready

Every visitor gets their own cookie-scoped set of three cases plus two shared read-only reference cases, seeded ahead of time so the first load is instant. A guided tour walks new visitors through the review screen, the return and the chat panel.

DRIVKRAFT_DEMO=1nightly resetno signup
Cases list in the Drivkraft Tax demo, showing three sandbox cases and two read-only reference cases
02 · K-1 REVIEW

Every box next to the region of the PDF it came from

The review screen puts the source PDF beside the extracted value. Each item carries a disposition — mapped, derived, informational or unverified — and an edit needs a reason and keeps the original. Approval is blocked until every flag is acknowledged; nothing reaches the calculation silently.

keyboard-firstj / k / e / adisposition ledger
K-1 review screen showing the source PDF beside a table of extracted boxes with their dispositions
03 · THE RETURN

Click a line, see the boxes that built it

OpenTax calculates the 1040. Clicking any line opens a leave-one-out waterfall back through the K-1 boxes and inputs that feed it, and a banner names exactly which K-1 amounts couldn't reach the calculation at all — instead of quietly leaving them out.

line attributionwhat-if scenarios
Form 1040 return view with adjusted gross income, taxable income and total tax, and a line-by-line breakdown
04 · OPERATOR

The app watches itself

Tool latency (p50/p95), estimated AI spend against a monthly cap, and every MCP/HTTP call with its error count, pulled straight from the events log. This page is also how I caught a live bug on launch day — see the build log.

p50 / p95spend caperror tracking
Operator dashboard showing cases, K-1s processed, tool calls, estimated spend, and a calls-per-day chart
Architecture

One tool registry, three doors in

Every capability is a plain Python function registered once. The server exposes each one twice — as an MCP tool and as a FastAPI route — so the web app never holds logic that Claude can't also reach.

Web app
Next.js 16 · Vercel · proxies /api
Claude Desktop / Code
connects over MCP directly
Web chat
SSE loop over the same MCP tools
↓ ↓ ↓
FastAPI /api
HTTP route per tool
MCP /mcp
stdio locally · HTTP + invite code hosted
↓
Tool registry
server/tools · 56 functions, one definition each
↓
Bridge
OTD → OpenTax + disposition ledger
SQLite + case files
per-visitor sandboxes on Fly volume
Optional: Anthropic + hooks
chat, meeting analysis — with room for a research tool (e.g. Bizora, BlueJ) or a note-taker (e.g. Vinyl, Jump.ai) on the back end
↓
otd-spec (pinned)
PDF → OTD extraction, validator
opentax v2.0.4 (pinned)
1040 calc, MeF export — run unmodified
What's mine

Everything above the two pinned engines

otd-spec and OpenTax run unmodified, as a subprocess — a data standard and a calculation engine, neither with a UI or any of the layers below. Everything from here down is this build.

01

Design & UI

The whole visual system — type (Fraunces/IBM Plex), layout, and the case list, K-1 review, return and operator screens — designed and built from scratch. OTD is JSON; OpenTax is a calc binary. Neither renders a pixel.

02

The bridge & disposition ledger

The translation layer between the two standards, with a per-field audit trail — mapped, derived, informational or unverified — so nothing reaches the calculation silently.

03

Chat

A web chat panel running the same MCP tools the desktop client uses, with anything it proposes landing in an Inbox for review rather than applying itself.

04

Research hook

A pluggable slot on each case for a research tool to attach to — built as an extension point, not wired to a live API in this build.

05

Meeting notes → proposals & to-dos

Transcripts turn into proposals plus a per-case checklist, so a meeting's follow-ups have somewhere to live instead of getting lost.

06

Outputs, e-file & operator page

An Excel workpaper round-trip with a cell-level diff, a PDF review packet, a MeF export against a fake IRS transmitter, a batch e-file queue across cases with per-case readiness badges, and a live dashboard watching tool latency, AI spend and errors.

Build log

Friday evening to Monday, in sixteen phases

Three sessions, paired end to end with Claude Code, listed here in phase order rather than the order they actually shipped in — see the note below. The clock is a real accounting, not a guess.

Not a round-number guess — summed from git history. 32 commits across three sessions (Sep 25–28, 2026); the gap between each consecutive pair counts as build time, capped at 30 minutes so overnight and weekend breaks between sessions don't inflate it. What's left is 9h 02m 09s of real, accounted work.

Friday, September 25
Phase 0 — Bootstrap
Pinned otd-spec + OpenTax binary, upstream smoke tests, the web app shell.
Phase 1 — The bridge
OTD → OpenTax translation with a disposition ledger for every node.
Phase 1.5 — MCP server
One tool registry, exposed over stdio.
Phase 2 — Cases & review
K-1 intake and keyboard-first review UI.
Phase 3 — The return
Calculation, scenarios, line attribution, data reset.
Phase 4 — Chat
Web chat over MCP, proposals and the Inbox.
Phase 5 — Research
A pluggable tax-research hook, a cost gate, and a demo cache.
Saturday, September 26
Phase 6 — Meeting notes
Transcripts become proposals; case checklist.
Phase 7 — Outputs
Excel workpaper round-trip with a cell-level diff, PDF review packet.
Phase 8 — E-file dry run
MeF XML export, business-rule validation, a fake IRS transmitter.
Phase 9 — Operator page
Tool latency, AI spend, bridge gaps, upstream health.
Phase 10 — Demo mode
Per-visitor sandboxes, guardrails, tour, deploy config written.
Monday, September 28
Phase 11 — Source documents
Drop W-2s and 1099s onto a case; text-layer PDF reader.
Launch
MIT license, deployed to Fly.io and Vercel, repo made public.
Live QA catches a real bug
A smoke test against the deployed server found analyze_meeting failing a live Anthropic call: a nullable-enum JSON schema the structured-output API now rejects. Fixed, tests re-run, redeployed — the operator dashboard's own error count is what confirmed it.
Phase 12 — Decisions become to-dos
Meeting decisions get the same accept/reject path as document requests, landing as a preparer to-do instead of a read-only recap.
Phase 13 — Case-list badges + batch e-file
Per-case status chips on the case list; a Queue/Filed board pushes several ready returns through e-file in one pass.
Phase 14 — Fix the stale Vercel deployment
The Vercel project auto-building from GitHub pushes was erroring on every deploy; the real, working project was a separate, correctly-scoped one the app had been quietly serving from all along. Case study and README repointed at the stable domain.
Phase 14 — Host the case study live
This page moves from a standalone artifact into the app itself, at /case-study, linked both ways with the live demo.
Phase 15 — Close the loop
A permanent 'Read the case study' link goes into the app sidebar. This page's own timeline and phase count catch up to what actually shipped.
Phase 16 — The build log, finished
The '~8 hrs' guess is replaced by the clock above — 9h 02m 09s, summed from git history, not rounded. The timeline is reordered into phase order and its times dropped, since build order and phase order aren't the same thing.

Every phase kept its own tests and a planning-doc update; the operator page's live error count above is the demo genuinely catching its own bug post-launch, not a staged example. This list runs in phase order, not build order: Phases 13 and 14 ran as separate, unsynchronized sessions on the same repo, and Phase 13's work actually finished after Phase 14's — it's placed here by its number, not its timestamp.

Stack

Tools used

Backend

Python 3.11FastAPISQLiteuvpytest

Tax engines (pinned, unmodified)

OTD specOpenTax 2.0.4

AI & protocol

Claude Opus 5Model Context ProtocolAnthropic API

Frontend

Next.js 16Tailwind CSSGeist

Outputs

openpyxlfpdf2pdfplumber / pypdfium2

Extensibility (optional, not wired in)

Research hook — e.g. Bizora, BlueJNote-taker hook — e.g. Vinyl, Jump.ai

Hosting & deploy

Fly.ioVercelDockerGitHub Actions

Dev tooling

Claude CodePuppeteerLighthousegit
Try it

Two ways in

Just click around

  1. Open the live demo ↗ — no signup.
  2. Take the guided tour, or jump straight into the Rivera or Chen case.
  3. Open the Copperleaf K-1 review to see a box traced to its PDF region.
  4. Check Calculate return for the line-by-line waterfall.

Your sandbox is private to your browser and resets nightly at 08:00 UTC.

Talk to it from Claude

Local (stdio), in claude_desktop_config.json:

{ "mcpServers": { "drivkraft-tax": {
  "command": "/path/to/drivkraft-tax/.venv/bin/drivkraft-tax-mcp" } } }

Or run it yourself:

git clone https://github.com/michaelrenwick-a11y/Drivkraft-tax
scripts/bootstrap.sh
uv run drivkraft-tax-server
cd web && npm install && npm run dev

Hosted MCP needs an invite code (rate-limited) — ask if you want one.

Credits

Built on, and thanks to

  • OpenTax by Filed Inc. — AGPL v3, run unmodified as a subprocess, pinned at v2.0.4.
  • Open Tax Document (OTD) by Tom O'Sullivan, Crimson Tree Software — CC BY 4.0.
  • Visual design borrows from the Drivkraft platform's slate/blue Tailwind system; fonts are Geist (OFL).
  • Independent project — not affiliated with or endorsed by Filed or Crimson Tree Software.
About

Michael Renwick

Drivkraft is Danish for the driving force that powers a machine or carries a business or a person forward — and it's your everyday advisor. It keeps the business owner and their professional advisors aligned and informed, so nothing gets lost between the books, the return and the conversation about what comes next.

Like what you saw here? See what else I've built at drivkraft.io ↗