PostHog published five golden rules for agent-first product engineering in April. I read them a week after shipping a product built for agents, which is the worst possible time: too late to use them as a checklist, exactly right to use them as an audit.
Four of them I had followed without knowing they had names. The fifth I had broken so thoroughly that the documentation said so out loud, and I had not noticed.

The four that held
The product is a price index for GPU rental, LLM tokens and wholesale electricity. It has no account and no login wall, which is not generosity: an index nobody can pull into a spreadsheet is a screenshot.
Let agents do everything users can. Every number on the page comes from a public JSON endpoint, and every table has a CSV export. There is no capability behind a human-only door, because there was never a door.
Meet agents at their level of abstraction. PostHog's answer here is SQL, and for an analytics product that is right: the question space is open, so the interface has to be a query language. A price index is not that. Its whole question space is three nouns, a market, an asset and a range, and one row per asset per day. Handing an agent SQL would be more power than the domain contains, and every extra degree of freedom is another way to ask something the data cannot answer.
Front-load universal context. That is what llms.txt and llms-full.txt are for. They state the units, the update cadence, and the thing most sites leave out: what the numbers do not say. Power is reported trades, published weekly, eight days behind. Missing days stay missing.
Front-loaded context also rots faster than anything else on a site, because nothing points at it. The API section said "all prices are USD per 1M tokens" and stayed true until the day GPU rental landed, at which point it was a confident, prominent, machine-readable lie.
Writing skills is a human skill. The version of this that survives is a NOTICE file and a docs page carrying what an agent cannot derive: that one source quotes per instance rather than per GPU, that a historical dataset flags 38,620 of 46,589 rows as CPU-only, and that without that filter the median is zero.
The fifth one, which is the expensive one
Treat agents like real users.
I had treated them like real users in the documentation and like nobody at all in the infrastructure. The docs invited agents to poll the endpoints without a key, and said, in as many words, that there was no rate limit worth mentioning.
There was no rate limit at all.
Measured from one laptop:
120 requests in 5 seconds
120 responses with status 200
0 rejected
Each of those opened its own Postgres connection, on a shared server with max_connections = 100 that also carries two other projects. Nothing broke, and that is the part worth sitting with. The failure mode of rule five is not an outage you notice. It is an invitation you published, a bill you have not received yet, and two neighbours who would have gone down with you.
The fix is two layers, and the cache is the one that matters.
The responses already carried s-maxage=300 and it did nothing, because Cloudflare does not put a Worker's own response through the cache on the way out. Asking for the same URL twice returned no cf-cache-status either time. Storing it explicitly means a flood now costs Worker invocations at $0.30 a million and never reaches the database at all.
The limit is 600 a minute per client, and 60 for the CSV export, which is the one large answer and the one unbounded query. Over it, 429 with Retry-After, so a client that reads headers backs off instead of retrying into the wall. Keyed on CF-Connecting-IP, which Cloudflare sets and the caller cannot, rather than X-Forwarded-For, which a flood could use to invent as many buckets as it liked.
Two things no rule would have told me
Both of these appeared between writing the fix and trusting it.
Hono's cors() sets its headers before calling the next middleware rather than while unwinding. So the copy that goes into the cache carries the allowed origin, and the fresh Response constructed on a hit discards it. Every cache hit would have failed in a browser while curl kept working perfectly, which is the shape of bug that survives a whole afternoon of testing by the wrong method.
And with no max-age of my own, Cloudflare filled the gap with the zone default and served max-age=14400. Four hours of browser cache on a price the application treats as stale after sixty seconds, and it appeared at the exact moment those responses started being cached at all. Only production could show that one, because it is a property of the zone rather than of the code.
What the rule is actually asking
Read as a slogan, "treat agents like real users" sounds like empathy. Read as an operational instruction, it is much narrower and much more useful:
- Real users are rate limited. Agents poll, and they poll at machine speed the moment you tell them they may.
- Real users get a status code they can act on.
429withRetry-Afteris the difference between a client that backs off and a client that hammers. - Real users hit a cache. If your platform will not cache a response for you, storing it yourself is the difference between a flood costing pennies and a flood reaching your database.
- Real users are counted per identity you control. Key on something the caller cannot set, or your limiter is decorative.
None of that is agent-specific. It is the ordinary hygiene of a public API, which is exactly the point: the moment your documentation says an agent may poll you without a key, you have a public API, whether or not you decided to have one.
Next
The four rules that held cost nothing, because the product was already shaped that way. The one that broke cost a rate limiter, an edge cache, two subtle bugs and an afternoon. That ratio is worth remembering when a set of principles reads as though all of its items weigh the same.
The next one in this series takes Datadog's Golden Paths for agents, which ask that every run produce evidence, and holds it against a test suite of mine that produced evidence while executing nothing. I wrote about that suite in the post about building this thing from one prompt.
If you want one thing from this: open your own docs and look for a sentence that grants access. Then check whether the thing it grants access to has a limiter, a cache, and a status code worth reading. Mine had a sentence and none of the three, and it took someone else's blog post to make me look.
PostHog's five rules are quoted from Jina Yoon's newsletter of 8 April 2026. Every number here was read out of the repository or measured against the deployed API.
Building something similar?
I write about setups I actually use. If you're working on something comparable, I'd be curious what your workflow looks like.