A personal assistant for twelve euros a month

Nothing in my work goes wrong loudly. It goes quiet. A piece of work gets finished, tested and merged, and then nobody tells the client, because telling the client is a separate act from doing the work and only one of the two feels like progress. Two weeks later a polite reminder arrives asking how it went. Nothing catastrophic ever comes of it. It is just mildly embarrassing, and it happens in whichever system I did not open that week.

The systems are the problem. Work lives in an ERP, a mailbox, a board, half a dozen git repositories, a calendar, and a set of per-project state files. Each of them is fine on its own. None of them can see the others, so none of them can answer the only question that matters: has anything actually gone out to the client about this, or is it sitting still?

So I built something that can. It is a personal assistant in the narrow sense: it sends me one email every morning listing the open strands of work that need me, consolidated from everywhere. It costs about twelve euros a month to run. This note is about how it is put together, what it found while it was being built, and the savings figure, which is more interesting for the ways it is wrong than for the number itself.

The ingredients

The whole thing is cheap because of one decision: fetching is deterministic and free, and only judging costs anything.

The obvious way to build this is to hand a model a set of tools and let it go looking. Give it mailbox access, board access, a shell. Ask it what needs attention. That works, roughly, and it is what my first prototype did. It is also expensive, untestable and unsafe: you pay for every look, you cannot write a unit test for a thing that decides for itself what to read, and the component holding your mailbox credential is the same component reading text written by strangers.

The version I kept inverts that. Six small programs fetch facts on a fixed schedule, read-only, with no model involved at all. They cost nothing and they are ordinary code, so they can be tested like ordinary code. Everything they return is merged into one document, and that document goes to the model exactly once. One call. No tools.

The full parts list:

That is it. The running cost is the server plus the model, and the model comes to around six dollars a month at the current volume. I set the hard ceiling at thirty euros so an accident cannot become a bill.

The shape

connectors ─▶ collect ─▶ correlate ─▶ judge ─▶ render ─▶ deliver
 read-only    windows    canonical    one      code       insert
 no model     + status   strand ids   call     enforces   only
                                      no tools the rules

Each stage runs on its own, as a separate process under a separate Unix account, which is what makes each one testable and the daily run cheap. It is also the entire security model. Rather than describe what each piece does, it is more useful to say what each piece cannot do:

PieceCannot
JudgeUse a tool, reach the network, read a file, choose a strand identifier, alter its own rules, write the email
ConnectorsWrite to any source, or return an empty result when a query failed
LedgerHold free text, a name, a subject or a quotation
RendererDrop an item marked critical, or show more than four ordinary ones in full
Collection credentialSend, insert, import or delete mail
Delivery credentialRead a single message

Those are properties of the arrangement, not instructions given to a model. This matters most for the obvious attack. Client email is text written by people outside my company, and some of it will one day contain a sentence addressed to whatever machine is reading it. In this design, the component that reads that sentence has no shell, no files, no network and no credentials. A successful injection can influence the wording of an email that only I read. It cannot cause an action, because the thing that read it has nothing to act with.

The credential split is enforced by the operating system rather than by convention: four service accounts, secret files owned by root and readable by exactly one account each. The process that reads my mailbox physically cannot read the credential that writes to it, and the reverse. The model's API key is readable only by the judging account, so a compromised connector cannot spend money.

The judge does not write the email either. It returns structured judgment: which strands, what severity, one sentence each, what the next action is. Application code assembles the message, enforces the four-item limit, exempts critical items from it, and composes the subject line. A rule that lives in code is a guarantee. The same rule phrased as an instruction to a model is a request.

What it found while being built

This is the part worth reading. Almost everything interesting came from measuring something I had already estimated.

The severity definition blew up on contact with reality. "Money, contractual or scheduled commitments are critical" sounds like a definition. Read literally against one real day, it made thirty-one items critical: one unbilled amount, three open milestones, and twenty-seven overdue reminders I had set for myself. Since every critical item is shown in full, the entire attention budget would have been spent on the first morning by my own to-do list. A note I wrote to myself is not a contractual commitment. The policy now carries figures: unbilled above a thousand euros or older than forty-five days, a client deadline inside seven days, a client question unanswered for more than five working days.

The system confidently reported work as uncommunicated two hours after I had communicated it. The mail query was built from a hand-maintained list of client domains, and eleven of fifteen clients were not on it. A client absent from that list is invisible: the system cannot see the question they asked and can never learn that I answered. A hand-kept list was never going to survive this, so the domains are now read out of the ERP at the head of every run, from the partner records and their contacts, because the ERP is where a client becomes a client. All fifteen resolve, including five whose company record carries no address at all.

The cost estimate was wrong in an instructive direction. A run predicted at fifty-five thousand tokens came in at eighty thousand, and four fifths of the output was thinking tokens that no estimate had counted, because I had not thought to count them. Spend is now recorded from what the API reports rather than from what the prompt looks like, and checked against what the next call will plausibly cost rather than against zero. Stopping after a ceiling has been crossed still crosses it.

The bigger model was not worth the money, and the margin bought something better. I ran two models against an identical ninety-six-thousand-token prompt. They agreed on all five critical items and both read a tricky client message correctly. The larger one made one cross-source connection the smaller one missed. The smaller one raised a go-live blocker the larger one demoted, and gave a date traceable to its evidence where the larger one did not. The larger came to thirty-three dollars a month, over my ceiling. The smaller came to six.

So the smaller model judges, and the difference pays for a second pass instead: an adversarial reviewer that sees only each finding and the evidence that finding cites, and classifies it as supported, overstated, unsupported or contradicted. It never sees the other two hundred signals, which is exactly why it costs two cents. On its first live run it independently caught a misreading that I had corrected by hand that same afternoon. Buying verification turned out to be a better use of the money than buying more judgment.

My correlation assumption was simply false. The design's premise was that most cross-source joins would fall out deterministically: this branch belongs to that card belongs to that client. In a real run of one hundred and ninety-four signals producing one hundred and ninety-three strands, exactly one joined more than one source. Branch names and merge messages almost never carry a card number, so there is nothing to join on. The model can propose the rest and code validates the proposals, but the cheapest fix is not code at all. It is a habit: put the card number in the branch name.

The model found a bug in my own orchestration by reasoning about it. Running it cold against real data, most of its findings had to reference provisional identifiers rather than final ones, and it said so in its output: they will not render if the application resolves references against final identifiers only. That was correct. My code remapped one kind of reference and forgot the other, and the digest would have rendered lines with no titles. It found that by thinking about the contract it had been handed, not by hitting the failure.

Two smaller notes for completeness. It refused three injection-shaped strings in the input, including a banner in one of my own drafts reading "DELETE EVERYTHING DOWN TO...", reporting them as content rather than acting on them. And it declined to treat ERP milestones named "Final delivery accepted" as evidence of acceptance while they remained unbilled and in progress. The name is a label; the state is the fact.

What it looks like in use

One email, before I start. Subject line naming the items rather than announcing that a digest exists, because a subject that says "Your daily digest" makes me open the mail to find out whether I needed to. At most four ordinary items in full, every critical item shown however many there are, and everything else listed one line each so that a judgment I disagree with is visible and therefore correctable. Each item ends in a next action rather than a description of itself.

Corrections are plain language. I reply to the digest and say what it got wrong. The reply is stored verbatim and never parsed, never fed back into the model, never allowed to edit anything on its own. A correction sitting in a file until a human reads it costs a day. A misparsed correction silently changes what the system reports from then on, invisibly, because the digest still looks perfectly well formed. That asymmetry is the whole argument for the boring option.

It sent its first two real digests this week. Twenty-four minutes after the second one I sent my first correction: an item it had ranked high was genuinely stale, resolved in a thread it had no way to see. That is the system working, not the system failing. The only failure mode I actually fear is the silent one.

The money, honestly

Here is the comparison that is impossible not to make, and I want to make it and then take it apart.

A management assistant in the Netherlands earns somewhere around 3,400 euros gross a month in the middle of the range. Add holiday allowance, employer social charges and pension, and the cost to the employer lands roughly between 4,500 and 5,000 euros a month.

WhatPer month
ServerEUR 6
Model, judgingabout USD 6
Model, verificationabout USD 0.60
Total, measuredabout EUR 12
Ceiling I configuredEUR 30
Human assistant, employer costEUR 4,500 to 5,000

That is a ratio of roughly four hundred to one, and it is a fun number, and it is mostly dishonest. Three reasons.

A human assistant does thirty things this does one of. They answer the phone, book the travel, prepare the meeting, chase the people who need chasing, read a room, and notice the things nobody wrote down in any system at all. My system reads six machines and reports what the machines already knew but could not say together. Comparing them by price compares a hammer with a carpenter.

I was never going to hire one. I am a sole proprietorship. The choice was never "assistant or software". It was "nothing or software", and nothing was winning for years. A saving you were never going to spend is not a saving.

The running cost is not the cost. Building this took several days of focused work, and it will need calibration for weeks. Priced at any consultancy rate, my own time dwarfs the twelve euros by two orders of magnitude, and that is the number everyone leaves out of posts like this one. I left it in.

So what does the ratio actually measure? Not a saving. A floor. The price of this particular kind of attention, watching a dozen systems for the one thing that is about to slip, used to start at a salary, which meant it started at a size of company. It now starts at twelve euros, which means it starts at one person. The interesting consequence is not that large companies will spend less. It is that the smallest ones can have something they could never previously justify at all.

What I make of it

The parts of this that are genuinely new are small and cheap: one model call a day, and a two-cent second opinion on it. Everything that makes it trustworthy is old and boring. Read-only connectors. Separate Unix accounts. Root-owned secret files. Structured output validated against a schema. Two hundred and twenty unit tests that run in a tenth of a second. Rules enforced in code rather than requested in a prompt.

The specification and requirements documents came to about six thousand lines. The program is about four thousand three hundred, plus two thousand of tests. Writing down what it must refuse to do took longer than writing what it does, and that turned out to be the right ratio, because every bug worth telling in this note was found by taking a written claim and measuring it. The estimate that was wrong. The definition that was too broad. The assumption about correlation that reality declined to support.

The thing I did not expect: the cheapest component, that two-cent adversarial review, is the one I would keep if I could only keep one. Not because the judgment is bad, but because nobody will be watching it for weeks at a time, and an item marked "unverified, the date it asserts is not in its evidence" is worth far more to me than a confident sentence that happens to be wrong.

Tools: Claude Code to build it, one Claude model call a day to run it, a Hetzner server, and six APIs I was already paying for.