---
title: "INV321: Constraints I couldn't fake"
description: 'What I learned about working with AI by building something that had to be right.'
date: '2026-06-11'
category: 'On building'
ogImage: '/images/inv321-og.png'
---

I'd been watching how people use Claude and ChatGPT and thinking: people don't actually want to navigate software. They want outcomes. The verb, not the interface.

I wanted to understand it from the inside. So I needed the smallest honest scope I could find. I picked invoicing because it had one constraint I couldn't fake: the output had to be something you'd actually send to a client.

<video src="/video/inv321-video-1.mp4" poster="/images/inv321-video-1-poster.png" width="1748" height="1080" autoplay muted loop playsinline aria-label="Screen recording: typing one sentence, an invoice appearing, and the link being shared"></video>

From there, I focused on one user: a solo operator who doesn't avoid invoicing because it's hard. They avoid it because it's one more mental thread to hold when their head is already full. They don't need invoicing faster. They need it to disappear.

[INV321](https://inv321.antoniodesign.work/) turns a single sentence into an instant, shareable invoice link, without an account or setup. That set every decision that followed. Starting with a date.

## Due next Friday

A wrong date on a client invoice is a problem that comes back. So my first instinct: let the model handle dates. You type "due next Friday." The model knows what Friday means, so let it calculate.

"Due next Friday" came back as next week's Friday instead of this week's. Without today's date, the model's reasoning drifts. I hadn't thought about that until it failed.

The right move was to change the prompt and tell Claude to extract the date phrase exactly as the user typed it. "Next Friday," "Dec 5," whatever they wrote. _Do not calculate. Just return the text._ The code does the math.

> The boundary isn't elegant. It's just where the model stops being reliable.

## The right model for the task

Before I wrote code, I'd set one time target: idea to invoice link in under ten seconds. A frontier model call can take five or more seconds on its own. Most of the ten seconds, before the invoice is generated.

The task never needed a frontier model anyway. It's extraction from a single sentence: no reasoning, no long context, no synthesis. Just parse this into structured JSON and return it. Given the target and the task, Haiku was the answer. Around two seconds, accurate enough, at $0.001 per invoice. Essentially free.

Once the boundary was clear, the model choice was easy.

![parser.ts: the ParsedInvoice interface, with the due date kept as the raw string the user typed — "Friday", "in 7 days", "Dec 5" — for the code to calculate](/images/inv321-img-1.png)

![parser.ts: the Haiku prompt — extract invoice details from the text and return valid JSON only, in an exact structure, no calculation](/images/inv321-img-2.png)

## The constraints held

The ten-second target wasn't the only constraint I wrote down before code. The product had to be conversational too. Anything that could be expressed that way had to be. The verb, not the interface, written down as a rule.

It got tested first when business info needed a home. Name, ABN, payment details. By the conversational constraint's own logic, it could go in the chat. But the chat is for the invoice. One sentence. Mixing config into that flow breaks it.

The constraint contradicted itself. The resolution was to separate what's stable from what's transient. A modal. Still a form, but inside the same surface. Config off to the side. Invoice in the chat.

![The Business Information modal: business name, ABN, tax rate and payment details in an overlay, with the chat surface dimmed behind it](/images/inv321-img-3.png)

The ten-second target got tested too. Speech input was there because saying one sentence is even less friction than typing it. Whisper was the obvious choice: good quality, negligible cost. But transcription took 5-8 seconds after pressing confirm. Same problem as the frontier model call. I replaced Whisper with the browser's native Web Speech API: instant, no server call, no cost.

> These aren't architectural preferences. They're actual requirements.

## What it taught

The date failure, the modal, the ten-second target: each one traced back to the same user. A solo operator who doesn't want to hold another mental thread. Every decision was in service of that.

The learning wasn't in the decisions. It was in the domain. Invoicing forced constraints that were real. A wrong date comes back. A slow tool becomes another thing to avoid.

![The finished invoice: line items, totals, payment details and a due date calculated in code — the thing you'd actually send to a client](/images/inv321-img-4.png)

Eventually, foundation models will extract dates natively. Frontier calls will fit inside the ten-second target. These specific choices will age.

The pattern won't. Pick a domain, understand what can't be wrong, and the building teaches you the rest.

> The thinking lives in understanding what can't be wrong.

Try [inv321.antoniodesign.work](https://inv321.antoniodesign.work)