Building Anton · Chapter 3 of 14

Anton, chapter 3: Building the substrate

March 13, 2026 · 5 min read

The week began with unfinished business.

Anton could search Doctolib, but he could not reliably open the appointment pages and tell us what was actually available.

Every attempted fix exposed another failure. Clicking the result produced stale browser references. Direct navigation timed out. More logging revealed that Cloudflare was identifying the browser as automated and blocking it entirely.

Eventually, the answer became clear: stop trying to force the same approach.

A browser with a memory

The useful question was not, "How do I bypass the block?"

It was, "What does a real browser have that Anton's browser does not?"

The answer was continuity.

A normal browser accumulates cookies, history, authenticated sessions, preferences, and countless other small traces of use. Anton was arriving every time as a completely new and anonymous browser, which was itself suspicious.

I replaced the bundled headless browser with a real Chromium instance using persistent profiles. Each website could now retain its own identity between sessions.

Anti-detection settings helped, but they were not the real solution. The important change was allowing the browser to behave like a returning browser rather than a stranger appearing from nowhere.

Once it did, Cloudflare stopped blocking it.

The browser then moved into its own isolated service, with the persistent profiles stored in mounted volumes. Every domain that needed browser access (Doctolib, the building management portal, the consulate, or the general web) could use the same hardened infrastructure.

One browser service. One profile per site. One place to fix things.

Letting the model read

Fixing browser identity solved the access problem, but there was still a better way to extract the appointment information.

I had been treating the page as a structure to navigate: locate each card, click it, inspect the details, and translate the result into data.

But most of the information was already present on the list page as text.

Instead of traversing every element deterministically, I gave the page text to the model and asked it to return the fields I needed in a structured format.

One model call replaced a complicated browser workflow.

It was faster, cheaper, and less fragile because it did not fight the design of the website.

This is a pattern I keep rediscovering: when deterministic extraction becomes disproportionately expensive, move the problem up one level and let the model interpret the material.

The LLM is often the cheapest unit of work available. I should use it where it pays.

Movie night

Meanwhile, the media domain was becoming something the family might genuinely use.

The collection of individual media tools became a smaller intent-driven interface: check the status, search the library, watch something, or search for something new.

The logic also became more grounded in the actual state of the system. Rather than searching for episodes within an arbitrary fourteen-day window, Anton could look at the last episode already present in Plex and work forward from there.

Reason from state, not from an invented window.

Then came movie night.

Every Friday evening, Anton would choose two or three films and post them to the family group.

It was his first proactive message.

Until then, Anton had always been responding to someone. Movie night required him to initiate an interaction at the right moment, without being asked.

Getting the tone right took more work than the feature itself. Early versions ended by asking whether anyone wanted him to download one of the films. It was technically helpful, but it made Anton sound like a salesman trying to prolong the conversation.

That led to a new rule for scheduled messages: no filler, no unnecessary questions, and no attempt to manufacture engagement.

A proactive message should justify its interruption and then get out of the way.

Pluggable domains

The most important structural change that week was making Anton's domains pluggable.

Until then, the parent agent knew about each domain individually. Every new capability required another explicit integration.

I replaced that with a small registration interface. Each domain could describe itself and expose its tools, while the parent built its available toolset dynamically from the modules that were present.

The parent no longer needed to understand calendars, media, research, or building management.

It only needed to know that it had tools.

A few days later, I added the building management portal as a new domain in three lines.

That is usually how you know an abstraction has found the right boundary: the next feature becomes almost disappointingly easy.

When the marginal cost of adding a domain approaches zero, you have found the seam.

Collections

The same pattern appeared in Anton's memory.

Wine had begun as its own typed database table. But the household would eventually want to remember other things: restaurants, books, contacts, recipes, places, and whatever else emerged.

Building a separate data model and toolset for every kind of collection would create endless special cases.

So collections became a shared substrate.

A generic table stored items as JSON, with the same tools for adding, searching, and updating them. Each collection carried a schema describing the shape of its entries.

The schema was included directly in the tool description. The model could read it and understand that a wine has one set of fields while a restaurant has another.

The tools remained generic, but their behaviour adapted to the collection.

Wine became the first migration. The dedicated table disappeared.

One substrate, many collections.

Skills as data

Reusable prompts followed the same path.

There were already instructions I expected to use repeatedly: conduct a weekly review, create a Google document with a particular structure, or perform some other familiar workflow.

Copying the same instructions into chat each time made no sense.

I added skills as records in the database: named, reusable prompts that could be listed, edited, and invoked without changing the application.

The word "skill" unfortunately overlapped with another part of the codebase, but the underlying decision was right.

A useful instruction should be an artifact I can inspect and modify, not a string buried somewhere in the code.

As with Anton's identity, the important move was from code to data.

Different people, different Anton

A few smaller capabilities also found their place.

Weather gained a direct integration because a simple forecast should not require a general web search. Anton could create Google Docs and share them automatically because the family already worked in Drive.

Every new collection shipped with regression tests. The quality rule established the previous weekend was becoming habitual rather than aspirational.

Anton also gained his first user-specific behaviour.

For Clara, his responses became simpler, and he knew when to escalate something to me.

The same assistant should not speak identically to every member of a household. People have different ages, needs, permissions, and expectations.

Personalization should not mean only remembering preferences. It should also mean adapting how the system behaves.

By the end of the week, Anton had gained more capabilities, but the architecture itself had become smaller.

The browser was one service rather than several browser implementations. Domains registered themselves instead of being wired individually. Collections shared one substrate. Skills were data rather than embedded prompts.

The instinct underneath each change was the same:

When something is going to repeat, make it a substrate rather than a special case.

Every time I made that choice, the system became simpler and the next feature became cheaper.

That is the trade I want to keep making.