The Agent Can Deploy. Credentials Were the Hard Part.

I have a coding agent that can put a website on the internet. Not a preview URL, not a tunnel to my laptop. A real domain, with a real certificate, that a stranger can open. It does this from a terminal, unattended, without me clicking anything in a dashboard.

People assume the interesting part is the deploy. It is not. The deploy is one command and it has been one command for years. The interesting part, the part that took real thought, is that an agent holding a credential is a different security object than a person holding the same credential, and almost none of the tooling is designed around that.

The setup, in full

Static sites go to Cloudflare Pages. Free tier, TLS handled, global, and importantly for this purpose it has an API that does not require a browser at any step. The whole deploy is:

export CLOUDFLARE_API_TOKEN="$PAGES_DEPLOY_TOKEN"
export CLOUDFLARE_ACCOUNT_ID="<account id>"
npx wrangler pages deploy ./site \
  --project-name=my-project --branch=main --commit-dirty=true

That is the entire mechanism. Forty-odd projects have gone out that way, and I have not opened the Cloudflare dashboard to ship one in months. The dashboard is now a read-only view I use to check my own work.

The first thing that goes wrong

Leave out CLOUDFLARE_ACCOUNT_ID and the deploy fails. That is fine. What is not fine is the error, which talks about being unable to list your accounts.

The cause is that wrangler, given no account id, tries to enumerate the accounts the token can see so it can pick one. A properly scoped token cannot enumerate accounts, because enumerating accounts is not the job you gave it. So the more correctly you scope the token, the more confusing the failure becomes. The fix is one environment variable, and the reason it is worth writing down is that the error message points away from it.

This is the shape of most of what follows. Nothing here is difficult. All of it is invisible from the outside, and each one costs an hour the first time.

One token cannot do the job

A static site needs a hostname, and a hostname needs a DNS record. My first instinct was one token for the agent, covering both. That instinct is wrong twice over.

It is wrong practically, because wrangler's OAuth login is Pages-scoped and simply cannot edit DNS. You will go looking for the flag. There is no flag. DNS is a separate API surface and needs a token minted with Zone, DNS, Edit.

It is wrong as a design. The deploy token pushes files into a project that already exists. Its worst day is a bad version of a site I control, fixed by deploying again. The DNS token rewrites where a name points. Its worst day is mail stopping, or a domain answering as someone else. Those two failure modes do not belong in the same credential, and the fact that a human would use one login for both is a habit, not a requirement.

So there are two tokens. The Pages token cannot touch DNS. The DNS token cannot deploy. An agent that goes wrong while deploying cannot take the domain with it.

A related trap while you are minting these: an account-scoped token verifies at the account endpoint, not the user one. Checking the wrong one tells you the token is invalid when it is fine, and you will go and mint another perfectly good token to replace it.

# account-scoped token
curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/tokens/verify

The proxy toggle that quietly breaks certificates

For anything with a backend I run small boxes behind a reverse proxy that requests its own certificates over HTTP-01. Point a subdomain at the box, register the route, done.

Except when the DNS record is created proxied, which is Cloudflare's default and the one their own UI nudges you toward. Then the ACME challenge never reaches your origin, because Cloudflare is answering on its behalf. Issuance fails, and it fails in a way that reads like a problem with your proxy rather than a problem with a checkbox.

So the record goes in DNS-only, the certificate issues, and only then is proxying a decision worth having. If you have an agent creating DNS records, this is not a preference, it is a default it has to be given, because it will not diagnose it.

The part I did not expect to matter most

All of the above is mechanical. Get it right once, write it down, never think about it again. The thing that actually caused pain was more boring than any of it.

When it costs nothing to put something on the internet, you put a great deal on the internet. Every experiment gets a subdomain, because why not. And then a year passes and nobody, including you, can say what half of those hostnames are, whether anything depends on them, or whether the thing behind them still runs.

I went through a cull and found something like eighteen deployments that existed for no current reason. That is not a disk problem. It is worse than that, because among things you have forgotten about there is usually at least one that someone still clicks, and you cannot tell which one without looking. Deleting in that state is guesswork.

The fix is not clever. It is a file, in a repo, with one row per deployment: what it is, where it answers, why it exists, and whether it should keep existing. The rule is that anything that changes hosting updates that file in the same change. An agent is perfectly happy to comply with that rule, which is more than I can say for myself before I wrote it down.

The corollary is a routing rule for new work: use the cheapest surface that still proves the point. Static or client-side goes to Pages and costs nothing and consumes no memory anywhere. Something that genuinely needs a database goes on a box, and only then. A one-off demo that nobody will click twice gets a public repository and no host at all. Most things turn out not to need a server, once someone makes you say out loud why they do.

What this is actually about

The reason to do any of this is not that deploying is hard. It is that a deploy which requires a person is a deploy that happens when that person is available, and the times you most want to ship are exactly the times you are least able to sit down and do it carefully. Automating it is not about saving the ninety seconds. It is about the change being possible at all at an inconvenient hour.

Handing that to an agent just makes the requirement explicit. A process is only delegatable if every step has a defined outcome, and going through a deploy asking which steps need judgement is a useful exercise even if you never delegate it. In my case the answer was: none of them, once the credentials were shaped correctly. The judgement had all moved earlier, into deciding what to deploy and where it belongs.

Which is roughly where you want judgement to live anyway.