Projects  /  Autonomous Publishing Agent

Nothing Is Accidental

An agent that runs a Substack publication on its own — it writes notes, comments under other authors, replies to reactions and follows new people. It runs on a schedule, three times a day, without a single question to a human.

LIVEVisibility · OpenAI · Automation

01

A publication that runs itself

The publication explains why ordinary things look the way they do — what decision, regulation or interest sits behind them. A note about the American stop sign being the only octagonal sign in the country, because a federal standard reserves that shape for it alone, so it stays readable when snow covers the word.

Writing that daily is not the hard part. Doing it unattended is. The system has to decide what is worth saying, check that it is true, publish it, notice what came back, and know when to stay quiet — three times a day, on its own server, without anyone watching.

My first attempt at this did not work. It is worth saying why, because everything the current system is came out of that failure.

02

The constraint that shaped everything

The first version had 71,598 lines of Python, 2,817 tests, 236 database triggers and 42 schema migrations. It produced two articles. It kept falling over on its own infrastructure — task queues, persistent intents, leases, model qualifications — none of which had anything to do with writing.

The second version began with a budget, not a feature list.

first version

lines of Python71,598

.py files200+

schema migrations42

database triggers236

Result: two articles.

second version

lines of Python6,526

.py files10

schema migrations0

database triggers0

Result: an account that runs every day.

complexity budget, set before the first linelimitactual
.py filesmax 1010
database tablesmax 44
abstraction layers between schedule and model callmax 11
migrations, triggers, task queues, persistent intentsnonenone

The limits were written down before any code, and were not negotiable while building. Same scope, eleven times less of it.

One more rule came out of the first version: a number lives in one place. Where it genuinely has to exist in two — the run time limit, which appears both in the config and in the service file — a test compares them and fails if they drift apart.

03

What was actually hard

Reading and publishing turned out to be different problems. There is no public API for publishing here, so the agent works the way a person does — through a logged-in browser. What I did not expect is that reading the platform's own data and writing to it are not the same privilege. An approach that reads reliably can still be refused when it tries to publish. Most of the early work went into discovering that, not into writing features.

The session is the fragile part. A logged-in session is the one dependency the agent cannot manufacture for itself. It expires, and re-establishing it needs a person — roughly once a quarter. That is the honest limit of the word autonomous in this project: the system runs unattended for months, then needs me for five minutes. A health check warns fourteen days before the session expires, because noticing on the day it dies is noticing too late.

Pace is a design constraint, not a setting. An agent that works as fast as it technically can is worse than one that doesn't — for the platform it is using and for the account it is running. The system caps how much it does in a day and spreads that work across hours rather than seconds. It also refuses more often than it acts: it comments only when it has something of its own to add, and it records the reason when it decides not to.

There was a shortcut, and I did not take it. The account does not hide what it is — it never claims to be a person, and asked directly, it does not deny it. Nothing in the system is built to defeat bot protection; a proxy setup for exactly that was considered and rejected. Those constraints cost real reach. I would choose them again: an account that survives is worth more than an account that scales for a month.

04

How a day runs

Six steps and one loop. The loop is the only one on the diagram, and it is the part that matters: what the agent learns from yesterday changes what it does today.

05

A click is not proof

The button always clicks. The content does not always arrive. After every action the agent asks the platform whether the thing it just did is actually there, and only then records it as done.

This is not defensive programming for its own sake. It is what makes a restart in the middle of a run safe: the system does not trust its own bookkeeping over what the platform reports. Three lines from a real run, where a click happened and nothing followed it:

-- following --
  button: 'Subscribe'
  CLICKED, BUT THE STATE DID NOT CHANGE

The action was attempted, was not confirmed, and was therefore not counted.

06

What it has actually done

Nine consecutive notes published by the agent on its Substack profile, each showing reader like and reply counts, followed by an article card.
Nine consecutive notes written and published with no human involved. The counts next to the hearts and speech bubbles are real reader reactions.

architecture — second version

lines of Python
6,526
.py files
10
database tables
4
tests, each with a counter-proof
139

Constants of this version. They describe the build, so they do not age.

production snapshot ·

comments, across as many publications
18
cost per run, USD
0.15–0.27

Eighteen comments across eighteen different publications, with no repeats — the result of a rule against commenting twice in the same place. Measured over a short window; the date is there because these numbers move.

== day closed ==
   notes: 2
   comments: 9
   replies: 0
   likes: 6
== run cost: $0.1890 in 59 calls ==

The last lines of a run. The system closes its own day and reports what it spent.

07

What I know now, and what I still don't

A complexity budget set before the first line is worth more than any refactor after. The second version is not smaller because I cleaned it up. It is smaller because the limits existed before there was anything to clean.

A test that cannot detect the bug it is meant to detect is worse than no test. Every test here also checks that it fails on the broken version. That mechanism caught two faults nobody was looking for.

The hundred-and-twenty-seventh comment is invisible and costs the same as the first. The first version of target selection sorted by engagement, so it walked straight into the most crowded threads. Reversing that sort was a two-line change and one of the more useful things I did.

Still not solved

  • The pool of conversations is too narrow. The reader feed returns only a handful, and searching on these topics returns mostly articles. Shortening the freshness threshold helped, but a different source is what is actually needed.
  • Finding facts is about 45% of the cost of a run. It can also come back with commentary instead of data, which means a paid call thrown away.
  • The agent works seven days a week at an even pace. People have days when they say nothing. This is the last obvious tell, and I have not decided what to do about it.
← All projectsHome