Poultry Ledger
My parents run a poultry business. Every sale arrives as a trader's handwritten slip. This app photographs the slip, reads it with a vision model, and turns it into a table they can check, correct and save.
Both brightnesses come from the same tokens in design/tokens.dart. Switch it and the case study repaints with the product.
- Role
- Design · Flutter · Python
- Users
- My parents, and two others
- Context
- Family poultry business, Hyderabad
- Status
- In daily use
The app allows exactly one gradient per screen, reserved for the number that answers how the business is doing right now. This is the only one on this page, for the same reason.
A photograph, and then a table you can argue with.
The model is never trusted on its own word. Every number it returns lands in an editable cell, an unreadable digit comes back as null rather than a guess, and the row it could not read is the one the interface points at.
Photograph the slip to fill this table.
What a family business can actually afford.
Eight points above the iOS floor, and the OS text scaler is clamped to a 1.1× minimum.
A local model on a Mac Mini, then a hosted one. Both are in the repo; only one is called.
Reserved for the single number that answers “how are we doing right now”. Everything else is flat.
The users were my mother and father.
Poultry trading runs on paper. A trader arrives, loads crates, weighs them, and writes the counts on a slip by hand — often in mixed script, Telugu numerals alongside Arabic ones. That slip is the only record of the sale until someone types it into a book.
Designing for people in their fifties who did not grow up with apps changed every decision. Large targets, large numerals, English throughout, and as few taps as the task allows. The camera is the first thing on the home screen because it is the thing they do every day — and manual entry sits beside it, because a product that only works when the clever part works is not finished.
- The number is the interfaceTabular figures everywhere, so a column of rupees lines up digit for digit.
- One hero per screenExactly one gradient — the number that answers how we are doing right now.
- Never a dead endIf the reader is unreachable, you type it and the photo queues.
- Flag, don't guessAn unreadable digit becomes null, not a number.
Where the photograph goes, and where it used to go.
This app has had two readers. The first ran on a Mac Mini in the house and cost nothing per photo; the second is a hosted model the phone calls directly. Switching between them below switches the diagram — and the trade was not free in either direction.
The migration dropped something I had argued was the most important part of the design. The v1 server re-added every column and compared the result against the total the trader had written at the bottom of the slip — two independent sources agreeing is evidence, where a confidence score is not. That check lived in schema.py · finalize(). The v2 client still reads a totals_match field and still has the warning banner wired to it — but nothing computes it any more, so the banner cannot fire. The arithmetic is four lines and belongs on the phone. It is the next thing I am fixing, and it is here rather than quietly omitted because a case study that only shows the parts that went well is a brochure.
Five screens, designed for two people in their fifties.
These captures are from the start of a cycle — 22,000 day-old chicks placed, almost nothing sold yet. The flock figures are real and the money figures are near zero, which is what this app looks like for the first weeks of every batch. The empty states took as much thought as the full ones because they are on screen just as long.





One counted number, then a paragraph about it.
22,000 birds in the farm now, at arm's length, in a shed, in daylight. Under it sits Farm Analyst — a generated read of the batch in plain sentences, brooding temperature and vaccination schedule and what to watch this week. It is labelled, it sits below the counted figure rather than mixed into it, and nothing it says is written to the ledger. Advice and record are different things and the layout has to say so.
The column adds itself up while you type.
Crates, bulk or fixed, because trade actually happens three ways. The Σ row under the crate table recomputes on every keystroke, and the footer pins birds, weight and rupees where you can see them with the keyboard up. Same argument as the slip reader above: a total you watched being derived is worth more than one the app asserts.
“Nothing pending with this trader.”
Poultry runs on credit — birds leave today and money arrives later. Each trader carries a running balance, a history of sales and payments, and one button to record cash coming in. Settled is a green card and a full sentence, not a zero someone has to interpret.
FCR is an em dash until it has earned a number.
22,000 placed, 0 lost. Feed conversion, mortality and age are the three figures that decide whether a batch made money. With no feed logged yet, FCR shows a dash rather than 0.00 — a ratio the app hasn't earned is worse than no ratio at all, because someone will act on it.
“Pending photos · All caught up.”
When a slip can't be read right away the photo waits here instead of failing. Giving the queue its own section — above Settings, not buried inside it — is what makes that state countable rather than something you discover a week later. Underneath: farms and sheds, flock, expenses. The parts of the domain you touch weekly rather than daily.
Two sheds, two times of day, one screen.

The app has no theme setting, because the phone already has one.
Adding a theme picker would have meant one more preference to explain to someone who did not ask for it. The system value is already correct — a shed at six in the morning and a kitchen table at nine at night want different screens, and the phone knows which one it is.
What that costs is discipline about colour. Green carries meaning in this product — settled balances, live birds, the primary action — so it has to survive both grounds at the same contrast. The selected farm row above is brand500 on brand50 in light and brand400 on a translucent brand fill in dark. Not the same hex — the same role, resolved twice.
Screens never reference a ramp step. They read the semantic layer, which is what makes the two brightnesses different designs rather than one inverted design — below, the same seven roles resolved for whichever theme this page is currently in.
- --canvas
- page ground
- --surface
- cards
- --surface-sunken
- wells, table headers
- --ink
- primary text
- --ink-soft
- secondary text
- --border
- the hairline doing most of the work
- --brand
- fills, the one action
Four screens you see once, and then never again.
Onboarding for two people who will call me if it does not make sense. That constraint is worth more than any usability lab: every sentence here survived being read aloud to the people it was written for.

Not accounts and roles and invitations. One shared family login, because the thing being modelled is a household, not an org chart.

“You can also pick one they sent on WhatsApp.” Traders already send slips as photos, so the second sentence of onboarding meets the habit that exists.

“Fix anything that looks wrong.” The correction step is introduced as normal on the way in, not surfaced as an error the first time the reader misses a digit.

The last card is the reason to keep using it. Photographing slips is the chore; knowing what each trader still owes is the payoff.
Shipping to people who will tell you the truth.
Building for my own parents removed every layer between me and the feedback. There was no research panel and no proxy — if a button was too small or a label was confusing, I heard about it at dinner. That is the fastest design loop I have ever worked in, and the least forgiving.
It also settled how I think about designing with models. The interesting question was never how accurate the extraction is. It is what the interface does with the part the model got wrong — and the answer is: show it, flag it, and let the person who was standing there correct it in one tap. The reconciliation check I lost in the migration is exactly that answer, which is why losing it matters more than the model swap did.
The ledger records what the shed produced. The other half was keeping the shed alive.
Farm OS is the hardware side of the same business — ESP32 sensor nodes in printed enclosures, watching temperature and ammonia around the same 22,000 birds this ledger counts.
Farm OS