TendForm: 176 commits, 85% co-authored by an agent
I built a HIPAA-capable form builder alone, on my own time, with Claude on 149 of 176 commits. What the agent did, what broke, and where the money goes.
On this page
- 176 commits, mostly in three weeks
- What runs where, and what it costs
- The MCP server is a product surface, not a plugin
- The stale cache only an AI write path could find
- claude.ai is not a browser
- Three hostnames, the same bug five times
- HIPAA in two days, edge cases for weeks
- My guardrails were a type checker and screenshots
- The agent docs drifted, and the agents believed them
- What I’d delegate differently
- If you’re running a team, attribute the agent like a cost center
At a glance
- 149 of TendForm's 176 commits carry a Claude co-author trailer. 74% of the history landed in the first three weeks.
- The worst bug was a stale edge cache that only surfaced once an AI started editing forms through MCP. New writers break caches designed for old writers.
- Most architecture choices were cost choices: static edge rendering, one container image, in-cluster Redis, a cheap classifier before every generation, self-run Postgres for HIPAA.
- The guardrails were a type checker, 'verified live' checks and screenshots, not a test suite. That's why HIPAA edge cases lived for weeks.
- The co-author trailer is great attribution for work. I have no attribution for the tokens behind it, and that's the gap teams should close first.
TendForm is a form builder. You can build a form by hand, paste in markdown, or ask Claude or ChatGPT to make one and get a live link and a QR code back. There’s a free tier, a Pro tier at $19.99 a month, and a HIPAA tier at $49 a month with its own encrypted Postgres enclave.
I built it alone, on my own time, while working my day job as a Senior Cloud Engineer at Apple. The git history runs from June 24 to September 12, 2026: 176 commits, and 149 of them end with a Co-Authored-By: Claude trailer.
This is Part 1 of Built with Agents. The agent didn’t do everything, and it isn’t a toy either. So here are the receipts: the commit timeline, what runs where, what it costs, and the bugs that only showed up because an agent was in the loop.
My day job is cost attribution for private cloud infrastructure, so I read everything the same way. Every AI system has two architectures: the one in the diagram, and the one on the invoice.
Commits
176
Agent co-authored
149
85% carry a Claude trailer
Fix commits
38
30 of them agent co-authored
Pro / month
$19.99
176 commits, mostly in three weeks
This chart comes straight from git log: one bar per week, split by whether the commit carries a Claude co-author trailer.
Day one was not a prototype. The first real commit, c28faeb Build TendForm: WYSIWYG builder, renderer, FastAPI backend, infra , is 9,371 lines across 92 files, including CI/CD and a Helm chart. Three hours later, 81c645f Backend deploy: plain k8s manifests, migrate Job threw the Helm chart out. When code is this cheap, deleting your first infrastructure decision the same afternoon is a normal move, not a sunk-cost fight.
74% of the history landed in the first three weeks. That’s 130 of 176 commits, and it covers a feature blitz (June 25 alone had 26 commits), the pivot to AI and MCP on June 29, and the HIPAA enclave on July 6 and 7.
Then it went quiet, twice. Nothing from July 26 to August 21, and nothing again until September 11. That’s what “on my own time” looks like in a commit graph.
The human commits tell on me. The 27 commits without a trailer include 6 merges and messages like “css tweaks”, “updaes to css” and “demo fix”. The two largest commits after day one are both mine: e5f5712 “support latest public private setup tendform” at +4,269/−806, and 205bf18 “demo fix” at +3,115. The agent’s commits explain the failure mode in the body. Mine have vibes.
What runs where, and what it costs
I’m not going to quote a monthly bill. The repo doesn’t contain one, and a made-up number in a post about cost visibility would be a special kind of irony. What the repo does show is the shape of the bill, because most of these decisions were cost decisions in an engineering costume:
- The public form path barely touches the backend. Respondents hit Pages and a Worker that caches rendered HTML at the edge.
CLAUDE.mdmakes it a rule: “Public form path must stay lean.” - One image, four modes. The backend runs as
api,worker,migrateorseed. Cron jobs run inside the worker instead of as a separate CronJob fleet. - No managed Redis line item. Redis is a single
redis:7-alpinepod requesting 25m of CPU and 64Mi of memory. The manifest admits the tradeoff: “the queue is transient; nightly cron re-enqueues.” - Shared Postgres, not dedicated. The app connects through a PgBouncer pool on a managed cluster that already exists.
- Cheap model first. Every AI generation runs a classifier on Gemini 2.5 Flash-Lite ($0.10 input, $0.40 output per million tokens) before spending tokens on the form itself. The classifier doubles as an abuse gate, backed by Redis rate limits.
The other cost lesson was about revenue. The free tier was supposed to be ad-supported. Twelve days after the first commit, 3efce3d Disable live AdSense on form completion screen (AdSense thin-content flag) pulled the ads, because the only ad surface was a form’s thank-you screen and Google flagged it as low-value content. A revenue line in a README is not a revenue line.
The MCP server is a product surface, not a plugin
On June 29 I repositioned the whole product around AI. 5060e9d Add TendForm MCP server with OAuth 2.1 (Claude/ChatGPT integration) landed at 9:01 a.m. with seven tools. By 11:06 the server auto-published forms on create, returned faithful previews, and served preview images. It now has 27 tools covering forms, packets, responses, exports, workspaces, integrations and billing, in a 1,085-line route file.
Three design decisions held up:
- Real OAuth, no new tables. OAuth 2.1 with dynamic client registration, PKCE and refresh-token rotation. Tokens live in Redis, so the whole thing shipped without a database migration.
- Tools reuse the route logic. The MCP layer calls the same code paths as the web app, gated by read and edit scopes. There’s no second implementation to drift.
- Every chat ends with a live link. Forms publish on create by default, so the conversation finishes with a URL and a QR code, not “now go open the dashboard.”
The part I’d steal for any MCP server: tool descriptions are prompts, so put the rules there. Models kept creating “phantom blank fields” by putting a type tag like [email] on its own line. The fix wasn’t smarter parsing. It was a MARKDOWN_GUIDE appended to the tool descriptions, under a code comment that reads “Authoring rules the model MUST follow.” The model reads the tool description every time it calls the tool. It never reads your docs site.
Bonus: Claude dogfooded the product while building it. The local permission file shows it calling TendForm’s own create_form, update_form and delete_form tools.
The stale cache only an AI write path could find
This is my favorite bug of the project, because it was invisible until something other than a human started editing forms.
The public Worker cached rendered HTML under a key built from the slug, the form version and the build ID. version only bumps when answer fields change, which made sense when I designed it, because responses are versioned by their questions. Then the MCP update_form tool started making cosmetic edits: titles, descriptions, themes. Those refreshed the JSON in Redis but left version alone, so the edge kept serving the old HTML indefinitely.
49a26d3 Fix: editing a live form never reached end users (stale edge cache)
The commit body calls them “silent edits that never went live.” The fix adds a content hash, rev, to the cache key, so any edit busts the edge cache. A sibling bug had the same shape: bc6a402 Bust public-form cache on plan change so Pro upgrade removes ads immediately . Upgrading to Pro didn’t remove ads until the cache TTL ran out, which is a great way to annoy someone who just paid you.
claude.ai is not a browser
Same morning, smaller lesson. At 10:30, 3b79af6 MCP preview: include a live screenshot image (Cloudflare Browser Rendering) returned a real screenshot as an image block. At 11:06, b651414 MCP preview: public cached preview-image URL (renders in chat clients) replaced it, because claude.ai drops image and resource_link blocks returned by third-party connector tools. The screenshot never reached the user.
The fix was a public PNG endpoint that only renders published forms, cached for an hour in Redis, plus a note telling the model to embed it as a markdown image. Thirty-six minutes from wrong assumption to working fix. Headless rendering isn’t free, and that cache keeps “show me a preview” from becoming one render per chat message.
Design MCP output for what the client actually renders, and test in the real client.
Three hostnames, the same bug five times
TendForm lives on tendform.com (public site and forms), app.tendform.com (the builder) and api.tendform.com. The split is good for caching and deploys. It also created a bug class that kept coming back:
-
02f87abFix analytics beacons dropped cross-origin : beacons sent asapplication/jsonneed a CORS preflight thatsendBeaconcan’t perform, so the browser silently dropped every analytics event. -
5e291b0email: send via Cloudflare Email Sending API : the verify link bounced to localhost in production. -
2e13fe4fix(web): polish loading & redirects : Google sign-in landed users on the marketing site. -
7fd8ea3fix(workspaces): point invite link at the builder, not the public site : “the very first click 404’d.” -
b97b36cfix(links): route builder-only deep links to app.tendform.com , twenty minutes after the invite fix.
The agent fixed each one quickly. It never generalized, because I never asked it to. One URL-builder module with a test per link type would have killed the class. That’s a delegation failure, not a model failure.
HIPAA in two days, edge cases for weeks
On July 6 and 7 I built the PHI enclave: envelope encryption, separate write and read paths, Prometheus, Grafana and Loki, a WORM log archive, compaction crons, auditor tooling and a BAA click-through. Several of those commits end in “(verified live)”: the agent ran the change against production and said so. The sprint’s first commit was mine, all 1,793 lines of it, titled “support hippa compliance.” Spelling is a Pro feature.
“Verified live” was true. It just wasn’t complete. Over the next two and a half weeks:
-
a890a71fix(pages): BAA gate checks the acting user : packets with a teammate’s form “402’d forever and re-opened the BAA modal in a loop.” -
73f365dfix(app): don’t show ads to HIPAA-plan accounts : the check looked for exactly"pro", and HIPAA is a Pro superset. -
d597a91fix(app): copy existing responses into the PHI enclave : flipping a form to HIPAA showed “1 response” and “no responses yet” at once.
None of these are exotic. They live at the edges between features: plan tiers, team members, existing data. An agent verifying its own happy path won’t find those edges unless you tell it where they are.
Read the covered-products list, not the headline
The enclave runs Postgres with CloudNativePG inside the cluster instead of DigitalOcean’s managed database. That wasn’t a preference. From docs/LESSONS.md: DigitalOcean signs a BAA, but its Managed Databases product isn’t on the covered list, while DOKS, Droplets, Volumes and Spaces are. “They sign a BAA” is not the same as “the product you’re using is covered.”
It turned out cheaper, too. The storage design doc notes that running CNPG in the existing cluster “removes the separate managed-Postgres cost entirely; the enclave is ‘just’ two more pods + PVCs.” Cold audit data goes to Spaces at about $0.02 per GB-month, with the first 250 GB inside the base subscription. And 90a7521 HIPAA logs: quiet pgaudit noise + funnel logs to Spaces (6y WORM archive) turned off pgaudit read logging, because the metrics exporter’s own SELECTs dominated log volume while every PHI read was already audited in the app.
My guardrails were a type checker and screenshots
The honest part: the test suite is thin. There are 35 pytest functions across 8 files and 5 Playwright tests, and the last local Playwright run on disk says "status": "failed". On July 9, 7f3b802 ci: disable auto-deploy on push (deploy manually from local for now) switched off CI deploys because they were broken. As of the last commit, they’re still off, and deploys run from make targets on my laptop.
So what kept it from falling over?
- The type checker. The builder build runs
tsc -bfirst, so a type error fails the deploy. - Verified live. The agent checked changes against production and said so in the commit message.
- Screenshots. The
mobile-testskill runs Playwright at 390×844 and tells the agent toReadeach PNG. The agent literally looks at the UI, and the run fails if any page scrolls horizontally. - Skills as scar tissue. The
deploy-backendskill opens with “ALWAYS build for linux/amd64”, because my Mac is arm64 and the cluster isn’t. It also documents a zsh quirk where"$REG:latest"silently pushed to an image calledtendform-appatest, so:latestnever updated.
That was enough for a solo product. It’s also why the HIPAA edge cases lived for weeks. Verified live proves the paths you tried. Tests prove the paths you wrote down.
The agent docs drifted, and the agents believed them
CLAUDE.md is the most important file in the repo for an agent: every session reads it first. It’s also wrong in places. It still says email goes through a Cloudflare Email Worker with MailChannels. The code has used the Cloudflare Email Sending API since 5e291b0, on day one. infra/cloudflare/README.md still describes a renderer app and a forms.tendform.com domain that no longer exist, plus cert-manager, which left with the Helm chart.
In September I added AGENTS.md so Codex and other agents get the same playbook. It’s a byte-for-byte copy of CLAUDE.md, so the drift now comes in two copies.
Docs that describe infrastructure are a cache with no invalidation. And agents read them with total confidence.
What I’d delegate differently
- Every fix commit ships a regression test. The agent writes tests happily. I just didn’t ask. The cache key, the plan check and the cross-domain links are each a ten-line test.
- Ask for the bug class, not the bug. After the second cross-domain fix, the prompt should have been “find every place we build a URL across hostnames.”
- The agent owns the docs it depends on. Infrastructure changes update
CLAUDE.mdin the same diff, andAGENTS.mdis a symlink. - The agent writes my commit messages too. My biggest diffs have the worst messages.
- I keep the contracts and the pricing. The BAA covered-products list, the AdSense policy and the $49 price mattered most to the business, and none of them were code.
- Put a stop condition on pixel-pushing. Cheap edits get you five heading-weight commits in fifteen minutes, or a packet QR code made “large enough to scan across a desk” at 10:29 and dialed “back to a scannable-but-calm 132px” at 10:31.
If you’re running a team, attribute the agent like a cost center
The Co-Authored-By trailer is the cheapest attribution tag you’ll ever get. One git log tells me 149 of 176 commits had an agent on them, which model, and when. It tells me 30 of the 38 fix commits were agent co-authored. That’s better attribution than most teams have for their cloud spend.
What it can’t tell me is what those 149 commits cost in tokens, or which features the tokens went to. I never tagged it. That’s the same gap I spend my day job closing on server fleets: the spend exists, the work exists, and nothing joins them.
If your team is adopting agents, do three things before the invoice gets interesting:
- Tag the work. Trailers on commits, and a ticket or feature ID on every agent session.
- Tag the spend. Per-team or per-repo API keys and workspaces, so tokens land on someone’s ledger.
- Join them every month. Cost per merged change, and how much of it went to rework like the fixes above.
Nobody owns a cost they can’t see. An agent that co-writes 85% of your commits is a cost center with very good commit messages. Give it a line on the ledger.