When the agent is the customer: five product changes I made in the last six months
I used to think about my software as a thing humans used. Then I noticed half my traffic had no cursor. Here are five product decisions I made when I accepted that the customer had changed.
In October 2025 I ran a script that counted how many of my /api/ask calls came from a human and how many from a machine. The split was 38% human, 62% machine. I had been designing every error message, every response header, every landing page for the 38%. In the six months since, I have changed five specific product decisions based on the other 62%. Here they are.
Change 1: I stopped HTML-first, started JSON-first
My product started as a web app. Naturally, every endpoint returned a beautiful, server-rendered HTML page. When I added an API, I bolted it on. You could get a machine-readable response by adding ?format=json to the URL. This seemed logical. Humans get the default, machines opt-in to their format.
The logs told a different story. A huge percentage of my 400 Bad Request errors came from API callers who simply forgot the query parameter. They would send a valid request to /api/ask, get a full HTML document back, and their JSON parser would immediately crash. Most API clients are not browsers. They do not gracefully render unexpected HTML. They fail, hard.
I thought about content negotiation. The Accept header is the correct technical solution. A client can say Accept: application/json and the server should honor it. The problem is that many simple HTTP clients, especially those cobbled together in scripts, do not send an Accept header at all. Or they send a generic */*. My server, prioritizing its HTML origins, would see this ambiguity and default to text/html. Another crash.
The fix was to flip the default. Now, my server inspects the Accept header. If it explicitly sees text/html, it serves the web page. For everything else, including a missing header or a generic */*, it defaults to application/json. The human experience did not change at all; browsers send the correct Accept header automatically. But the agent experience improved overnight.
The result was a 30% drop in 400-level errors in the first two weeks. The lesson was clear: agents are far less tolerant of format surprises than humans. A human sees a weird page and might find the right button. An agent sees the wrong Content-Type and terminates.
Change 2: I moved error messages from prose to structured JSON
My old 500 Internal Server Error page was a masterpiece of apologetic prose. It said something like, "Oops, something went wrong on our end. Please try again in a few moments. If the problem persists, contact support." This is fine for a human. It is useless for a machine.
An agent cannot parse "try again in a few moments". How many moments? Is this a rate limit, or is the database on fire? The agent has no way to know, so it probably just retries immediately, making the problem worse.
I replaced all my error responses with structured data, specifically following the RFC 7807 Problem Details for HTTP APIs standard. Now, a server error does not return a paragraph. It returns a JSON object.
A rate limit error, for example, now looks like this:
{ "type": "https://kieran123.win/problems/rate-limit", "title": "Rate Limit Exceeded", "detail": "You have exceeded your request quota.", "instance": "/api/ask" }
The type field is a unique, permanent URI that an agent can use in a switch statement. It can now programmatically identify the exact error class. If it sees /problems/rate-limit, it knows to implement an exponential backoff strategy. If it sees /problems/insufficient-funds, it knows to stop calling entirely until its account is refilled.
This change took a single weekend. I wrote a small helper function, about 40 lines of code in /lib/problem.ts, that generates these responses. It was one of the highest-leverage changes I have ever made. It transformed my API from a black box into a self-describing system that agents can navigate intelligently.
Change 3: I published prices in a machine-readable format
For the first year, my pricing page was a classic "Contact us for a quote" form. This was not a sophisticated sales strategy. It was a thin veil for "I have not decided on pricing yet". This is a huge barrier for a human, but it is a complete wall for an agent. An autonomous agent cannot "contact us for a quote".
To serve my new customers, I needed to make pricing discoverable and legible to a machine. I implemented three things.
First, I created a /.well-known/x-payment endpoint that returns a simple JSON object with my current pricing tiers and the cost per API call. This is a non-standard but increasingly common pattern for services to advertise their costs.
Second, I built a real pricing table on my site at /demo/x402. It shows the same information but is designed for human eyes.
Third, and most importantly, I started using the HTTP 402 Payment Required status code. When an unauthenticated agent makes a call to a paid endpoint, it gets a 402 response. In the headers of that response, I include the exact cost of the requested operation. The agent can then decide if it is willing to pay, fetch a payment token, and retry the request with the correct credentials.
The result was magical. The first new agent that integrated in March never sent me a single email. My logs showed its journey. It hit the API, got a 402, fetched the pricing from the well-known endpoint, made a payment of $0.001, and then successfully got its answer. That is the full, automated agent discovery and integration loop. It happened without any human intervention.
Change 4: I standardized on idempotency keys
Humans and agents handle failure differently. If a human clicks a "Submit" button and the page hangs, they might wait, get frustrated, and click it again once. Maybe twice.
An agent in a retry loop will hit your server six times in four seconds without blinking. If your endpoint is not idempotent, that is six duplicate charges, six duplicate database entries, six duplicate actions.
To solve this, every POST endpoint in my API now accepts an Idempotency-Key header. The calling agent is responsible for generating a unique key, usually a UUID, for each operation it wants to perform. When a request comes in, my server first checks a cache to see if it has ever processed that key before.
If the key is new, it processes the request, stores the result in a KV store with the key, and returns the response. If the same request with the same key arrives again within a 24-hour window, the server does not re-process it. It simply serves the stored response from the first successful attempt.
This protects both the customer and my system. The customer never gets double-charged for a network hiccup. My system is protected from the "retry storms" that impatient agents can create.
The cost is negligible. The KV space for storing these idempotency keys costs me about $0.008 per month. The cost of a single double-charge it prevented was $0.001. But the trust it buys is the entire game. An agent developer needs to know that your API is safe to call, even on an unreliable network. Idempotency is the foundation of that trust.
Change 5: I wrote my changelog with machines in mind
My old changelog was a simple Markdown file. I would add a bullet point whenever I shipped a new feature. It was a journal for me and a casual update for human users.
An agent, however, needs to know about changes with much more precision. A new feature is an opportunity. A breaking change is a five-alarm fire. My prose-based changelog did not distinguish between them.
My new /changelog page still looks like a simple Markdown file to a human. But it is now backed by a structured data source. I also publish a machine-readable /changelog/feed.xml. This feed contains structured entries for every change. Each entry is tagged with a category: breaking-change, price-change, capability-add, or capability-remove.
This allows an agent to subscribe to the feed and parse it. It can be configured to ignore all capability-add entries but to immediately halt and alert a human developer if it ever sees a breaking-change or capability-remove tag.
I even added a custom XML element, <kw:changeImpact>, with a numeric score from 1 to 10 indicating the severity of the change. This allows downstream agents to have more nuanced reactions. A minor price change might be logged, while a major one triggers a pause. This is about giving agents the structured data they need to make intelligent decisions about my product's evolution.
The counter-change I didn't make
At one point, I considered removing my human-readable pricing page. My thinking was that I should force everyone, human and machine, to use the new, superior, machine-readable endpoints. This would simplify my codebase.
I quickly realized this was a mistake. Humans are still part of the loop. A human developer is the one who decides whether to integrate my API in the first place. They read my landing page, they look at my documentation, and they scan my pricing table to see if it is even worth their time. If I hide that information behind a curl command, I lose that initial human evaluation. I lose the word-of-mouth that brings new developers to my door.
The lesson was to treat this as addition, not replacement. Every machine-serving surface is an additional surface. You are building a second interface to your product, one made of structured data and clear contracts. You cannot tear down the human interface to do it.
The data after six months
The proof is in the numbers. After six months of consistently prioritizing the agent as a customer, my key metrics have all moved in the right direction.
| Metric | Before (Oct 2025) | After (Mar 2026) | Change | | :--- | :--- | :--- | :--- | | Machine API Traffic | 62% | 74% | +12% | | API Support Emails | ~20/month | ~6/month | -70% | | New Caller Success Rate | 41% | 88% | +115% | | API Revenue | 1x | 8x | +700% |
The revenue numbers are still small in absolute terms, but the relative growth is real. By making my service easier for agents to use, I made it a more attractive and reliable dependency. The drop in support emails is perhaps the most telling. I am not fixing more bugs; I am providing a system where agents can understand and recover from errors on their own.
What doesn't transfer
"Agents are the customer" is not a license to make everything ugly. It is not an excuse to abandon design, typography, and a pleasant user experience. Humans still need to visit your website, read your documentation, and manage their accounts. That experience needs to be tasteful and clear.
I did not strip the CSS from my site to "focus on machines". That is a false dichotomy. The reality is that you have a dual audience. You need a well-designed, human-friendly front door, and you also need a well-designed, machine-friendly API. These two things are not in conflict. It is a small extra burden on me as the maintainer, but not a big one once I accepted the dual-audience design philosophy.
What I'd do if I were starting from scratch
If I were building this service again today, knowing what I know now, I would do things in a different order.
- Ship the JSON endpoint first. I would build the core logic and expose it via a clean, JSON-based API. The HTML view would be a user-friendly "skin" built on top of that API, not the other way around.
- Make every price machine-readable from day one. No "contact us" forms. The price would be in a JSON file and in API responses from the first commit.
- Use Problem Details for every error. I would build the RFC 7807 error helper before writing my first real endpoint. Structured, predictable errors are a feature, not an afterthought.
- Set up idempotency at the framework level. I would build idempotency key handling into my core API middleware, not add it route-by-route as I did.
Conclusion
When the customer changes, the product changes. Half my customers are machines now. They wanted machine-legibility, clear error types, and stable, predictable prices. By giving them these things, I made my product better. And humans got a better-designed, more reliable system as a side effect of building things that agents could read.
If your traffic is 60/40 human/machine, you already have both sets of customers. You just might not be serving one of them very well. Design for both.