Tech & AIInsightsAboutCareers Book a call

Blog Article

Your API's Next User Is a Model: Agent Experience (AX) for Engineers

AI agents call your API and treat each response as their next prompt. Engineering for them: clear errors, idempotency keys, rate limits and MCP tools.

Author

Keerthan K Acharya

Keerthan K Acharya

Founding Engineer

For as long as we have built APIs, we have written them for a developer with the documentation open in one tab and a terminal in the other. When a request failed, a person read the error, opened the docs, worked out what went wrong and fixed the call.

That person is increasingly not there. An AI agent calls your API, reads the response and decides what to do next, in a loop, on its own. Your error message is no longer read by someone debugging. It is fed straight back into a model as its next input. If the response is vague, the agent guesses. If it guesses wrong, it retries. If retrying is not safe, your customer pays twice.

This post is about engineering for that reader. It covers what changed this month, why every response you send is now part of a prompt, and six concrete patterns, with code, that make a system work well when the client is an agent.


What Changed on October 7?

On October 7, 2026, OpenAI launched a feature it calls Intelligent UI, together with a new GPT-6 model in ChatGPT. As TechCrunch reported, answers can now include tappable buttons, a calculator built for a specific task, interactive charts and editable graphs. OpenAI’s examples included a personal savings calculator and a map for a multi-day hiking trip. It reached paid plans first and Free and Go users the next day. As OpenAI product manager Aarush Selvan put it, “ChatGPT has predominantly been a text-based interface.”

This is generative UI, which Google Research describes as a capability “in which an AI model generates not only content but an entire user experience.” Google launched it in the Gemini app and in AI Mode in Google Search in November 2025.

Earlier, at DevDay 2026, OpenAI also announced that its Agents API now supports computer use, letting applications operate software through its graphical interface, and proposed an MCP Events specification that lets automations start when something happens in a connected application.

For engineers, the takeaway is simple. The assistant is becoming the interface, and the work behind it is done by agents calling systems like yours. The screen you designed may never be seen. Your API, your error messages and your data will be.


From DX to AX

Netlify’s CEO and co-founder, Mathias Biilmann, introduced the term agent experience in a post on January 28, 2025, defining it as “the holistic experience AI agents will have as the user of a product or platform.” Netlify draws the line simply: DX is about the people who build on a platform, and AX is about the AI agents those people send to act for them.

Developer experience (DX)Agent experience (AX)
Who reads your responseA developer, once, while debuggingA model, every time, inside a loop
What happens on a vague errorThe developer opens the docsThe agent guesses, retries or gives up
What happens on a timeoutThe developer checks before retryingThe agent often just retries
Where documentation livesA docs siteThe tool description and the response itself

Every Response Is the Agent’s Next Prompt

An agent works in a loop: decide on an action, call a tool, read the result, decide again. Whatever your system returns becomes part of the model’s context for the next decision. That makes response design a form of prompt design.

The people building these systems say so directly. In its guide to writing tools for agents, Anthropic advises that “you can prompt-engineer your error responses to clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks.” The Model Context Protocol (MCP) specification separates two kinds of failure: protocol errors, which “models are less likely to be able to fix,” and tool execution errors, which “contain actionable feedback that language models can use to self-correct and retry with adjusted parameters.” It lists input validation errors, such as a date in the wrong format, as the second kind.

Here is an example. Every failure in it is one we see agents run into regularly. An agent is asked to book a table for four this Friday at 7 pm and pay any deposit.

  1. It sends the date as 17/10/2026. The API answers 400 Bad Request with the body Invalid request. The agent tries two more formats before one works.
  2. The payment call times out. The agent cannot tell whether the deposit went through, so it calls again. Two deposits are charged.
  3. It needs the opening hours. They exist only in a photo of the menu. The agent guesses.
  4. It gets rate limited. The API answers 429 with no hint of when to come back, so the agent hammers it, then gives up and books somewhere else.

None of these is a model problem. Each is an engineering decision on the server side, and each has a known fix.


1. Write Errors That Tell the Agent How to Recover

The standard to build on is RFC 9457, Problem Details for HTTP APIs, published in July 2023. It defines a JSON error format with the media type application/problem+json, the members type, title, status, detail and instance, and it allows extension members. Its own example adds an errors array with a pointer to each invalid field.

Before:

HTTP/1.1 400 Bad Request
Content-Type: text/plain

Invalid request

After:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Your request is not valid.",
  "status": 422,
  "detail": "1 field failed validation. Fix it and send the request again.",
  "errors": [
    {
      "pointer": "#/date",
      "detail": "Must be an ISO 8601 date (YYYY-MM-DD), for example 2026-10-17. Received \"17/10/2026\"."
    }
  ]
}

What makes the second version work for an agent:

  • It points at the field. pointer says exactly which input is wrong.
  • It states the expected format with an example. A model copies examples well.
  • It echoes what was received. The agent can see its own mistake without guessing.
  • It says what to do next. “Fix it and send the request again” is an instruction, not just a status.
  • It leaks nothing internal. No stack traces, which help no one and expose your internals.

A practical test: read each error message and ask whether a new developer could fix the call from the message alone, without the docs. If not, an agent probably cannot either.


2. Make Retries Safe With Idempotency Keys

Agents retry. A timeout is ambiguous: the request may have failed, or it may have succeeded and only the response was lost. A careful developer checks before retrying. An agent often just tries again. For reads, that is harmless. For a payment or a booking, it is a double charge.

The fix is an idempotency key: the client generates a unique key per operation and sends it with every attempt, and the server makes sure the operation runs once per key. Stripe’s API is a well-known example. Its documentation describes idempotency as “safely retrying requests without accidentally performing the same operation twice.” The IETF has worked on a standard Idempotency-Key HTTP header. Its latest draft, draft-07 from October 2025, has since expired without becoming an RFC, but its rules are a useful reference:

SituationStatus code in the draft
The key is missing on an endpoint that requires it400
The key is reused with a different request body422
A retry arrives while the original request is still processing409

The draft recommends a UUID or similar random value as the key, and says the server should define how long keys are kept and publish that in its documentation. It also allows an idempotency fingerprint, a hash of the request, to detect a key reused with a different payload.

Here is a simplified version for an Express API with PostgreSQL. The table stores one row per client and key:

CREATE TABLE idempotency_keys (
  client_id    text        NOT NULL,
  key          text        NOT NULL,
  fingerprint  text        NOT NULL,   -- hash of the request body
  status_code  int,                    -- NULL while the request is in flight
  response     jsonb,
  created_at   timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (client_id, key)
);

And the wrapper around any endpoint that changes state:

import { createHash } from "node:crypto";
import type { Request, Response } from "express";
import { db } from "./db"; // a node-postgres Pool

type Result = { status: number; body: unknown };

function problem(res: Response, status: number, title: string, detail: string) {
  return res.status(status).type("application/problem+json").json({ title, status, detail });
}

export async function withIdempotency(
  req: Request,
  res: Response,
  clientId: string,
  run: () => Promise<Result>,
) {
  const key = req.get("Idempotency-Key");
  if (!key) {
    return problem(res, 400, "Missing Idempotency-Key",
      "Send an Idempotency-Key header with a new UUID for each booking, and reuse it when you retry.");
  }

  const fingerprint = createHash("sha256").update(JSON.stringify(req.body)).digest("hex");

  // Claim the key atomically. Of two identical concurrent requests, only one can insert the row.
  const claim = await db.query(
    `INSERT INTO idempotency_keys (client_id, key, fingerprint)
     VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
    [clientId, key, fingerprint],
  );

  if (claim.rowCount === 0) {
    const { rows: [saved] } = await db.query(
      `SELECT fingerprint, status_code, response FROM idempotency_keys
       WHERE client_id = $1 AND key = $2`,
      [clientId, key],
    );
    if (saved.fingerprint !== fingerprint) {
      return problem(res, 422, "Idempotency-Key reused",
        "This key was already used for a different request. Generate a new key for a new booking.");
    }
    if (saved.status_code === null) {
      res.set("Retry-After", "2");
      return problem(res, 409, "Request still in progress",
        "The first request with this key is still running. Retry with the same key in 2 seconds.");
    }
    return res.status(saved.status_code).json(saved.response); // replay the original result
  }

  let result: Result;
  try {
    result = await run(); // runs once per key
  } catch (err) {
    // The operation failed: release the claim so the client can retry with the same key.
    await db.query(`DELETE FROM idempotency_keys WHERE client_id = $1 AND key = $2`, [clientId, key]);
    throw err;
  }

  // The operation succeeded: store the result so every retry gets the same answer.
  await db.query(
    `UPDATE idempotency_keys SET status_code = $3, response = $4
     WHERE client_id = $1 AND key = $2`,
    [clientId, key, result.status, JSON.stringify(result.body)],
  );
  return res.status(result.status).json(result.body);
}

The code is the easy part. The decisions around it are where systems go wrong:

  • Scope keys per client. Two clients can generate the same key by accident. The composite primary key prevents one client from replaying another’s response.
  • Decide what a failure means. The sketch releases the key when the operation throws, so a retry runs it again. That is only safe if a failed run left nothing behind. If it may have half-completed, keep the key and return the error on replay instead.
  • Carry the key downstream. If the server crashes after charging the card but before saving the result, a retry will run again. When the downstream system supports it, pass an idempotency key to it as well. Stripe, for example, accepts one on its POST requests, so the payment provider also refuses to charge twice.
  • Canonicalize before hashing. The same JSON with keys in a different order produces a different hash. Hash a normalized form of the body, or only the fields that define the operation.
  • Expire keys, and say when. Stripe’s API v1, for example, may remove keys once they are at least 24 hours old. Whatever window you choose, publish it, and clean up claims left in flight by a crashed process.

3. Rate Limits That Say When to Come Back

RFC 6585 defines 429 Too Many Requests for rate limiting. It says the response “SHOULD include details explaining the condition, and MAY include a Retry-After header.” RFC 9110 defines Retry-After as either an HTTP date or a number of seconds, for example Retry-After: 120.

A human sees a 429 and waits. An agent with no hint may retry immediately, burn its own budget and yours, then give up. Tell it exactly when to come back:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/rate-limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "The limit is 60 booking requests per minute per API key. Wait 30 seconds, then retry. Earlier retries will also be rejected."
}

4. Publish Facts in a Form Machines Can Read

Opening hours in a photo, prices in a PDF and policies in an image are fine for a person and hard for a machine. Put the facts that drive decisions into text and structured data.

For businesses with a physical location, schema.org already has the vocabulary, and Google lists openingHoursSpecification as a recommended property in its local business structured data documentation:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Restaurant",
  "name": "Example Bistro",
  "servesCuisine": "French",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Marktstraße 12",
    "addressLocality": "Berlin",
    "postalCode": "10115",
    "addressCountry": "DE"
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Tuesday", "Wednesday", "Thursday"],
      "opens": "17:00",
      "closes": "22:00"
    },
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Friday", "Saturday"],
      "opens": "17:00",
      "closes": "23:30"
    }
  ]
}
</script>

The engineering point is the source. Generate this markup from the same database your booking system reads, not by hand. Hours copied into three places drift apart, and an agent that reads the stale copy will confidently tell your customer the wrong thing.

A related note on llms.txt, a file proposed by Jeremy Howard in September 2024 to give language models a plain summary of a website. Google’s guidance says you can ignore it for Google Search, and that keeping one is fine for other services that use it. It is cheap to add, but it is not a substitute for structured data and a good API.


5. Build a Front Door for Agents: Tools, Not Pages

An agent can drive a website through its screens, but a tool built for it is faster and far more reliable. MCP is the open standard for this. The project describes it as “an open-source standard for connecting AI applications to external systems” and compares it to a USB-C port for AI applications. A tool definition has a name, a description, an inputSchema in JSON Schema and optional annotations:

{
  "name": "create_booking",
  "title": "Book a table",
  "description": "Book a table at Example Bistro. Call check_availability first. Times are local to Berlin. Party size is 1 to 8; for larger groups call request_callback. Parties of 6 or more pay a deposit of 10 EUR per person. Reuse the same idempotency_key when you retry, or the guest may be charged twice.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "date": { "type": "string", "format": "date", "description": "ISO 8601 date, for example 2026-10-17" },
      "time": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", "description": "24-hour local time, for example 19:00" },
      "party_size": { "type": "integer", "minimum": 1, "maximum": 8 },
      "idempotency_key": { "type": "string", "description": "A UUID you create once per booking and reuse on every retry" }
    },
    "required": ["date", "time", "party_size", "idempotency_key"],
    "additionalProperties": false
  },
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": false,
    "idempotentHint": true,
    "openWorldHint": false
  }
}

A few things in this definition are deliberate:

  • The description is documentation. Anthropic’s advice is to “think of how you would describe your tool to a new hire on your team.” Order of calls, limits, money and the retry rule all belong here, because this is what the model reads before it acts.
  • The schema documents, the server validates. JSON Schema tells the agent the shape it should send. Your server still has to check every input, and the MCP specification requires servers to validate all tool inputs.
  • Annotations must be true. In the MCP schema, idempotentHint means calling the tool repeatedly with the same arguments has no additional effect. That is only honest because idempotency_key is part of the arguments and the server enforces it. The specification also says clients must treat annotations as untrusted unless they come from a trusted server, so they describe your tool, they do not protect it.

When a call fails for a reason the agent can fix, return a tool execution error with isError: true and a message that points to the next step:

{
  "content": [
    {
      "type": "text",
      "text": "No table for 4 on 2026-10-17 at 19:00. Free times that evening: 18:00 and 20:30. Call create_booking again with one of these times, or call request_callback to reach the restaurant."
    }
  ],
  "isError": true
}

That single message turns a dead end into the next step.


6. Hand Off to a Person When the Agent Gets Stuck

No agent will finish every task. Some requests need judgment, an exception or a phone call. Design that path on purpose instead of letting the agent fail silently. A request that needs a person does not have to be rejected. It can be accepted for review, with the way forward in the response:

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "status": "pending_review",
  "detail": "Groups larger than 8 are confirmed by the restaurant team. Your request has been passed on. Share the link with the guest.",
  "handoff": {
    "case_id": "c_48213",
    "url": "https://example.com/requests/c_48213",
    "expected_response": "within 2 hours, between 10:00 and 22:00 local time"
  }
}

RFC 9110 defines 202 Accepted for a request that has been accepted for processing but not completed, which is exactly the situation.

A clean handoff turns a lost customer into a delayed one, and gives the agent something true to tell the person it is working for.


An AX Review Checklist for Your API

  1. Every error uses a structured format, such as application/problem+json, and names the field, the expected format, an example and the next step.
  2. No error leaks internals. No stack traces, no raw database messages.
  3. Every state-changing endpoint accepts an idempotency key, scoped per client, with the request fingerprint checked and the key’s lifetime published.
  4. Keys are passed downstream to payment providers and other systems that support them.
  5. Every 429 carries Retry-After and a sentence explaining the limit.
  6. Facts that drive decisions are machine-readable, generated from your source of truth, not copied by hand.
  7. Tool descriptions read like onboarding notes, with call order, limits, money and the retry rule.
  8. Annotations match behavior, and the server validates every input whatever the schema says.
  9. There is a handoff path that returns a case ID or link instead of a dead end.
  10. Run your key flows through an AI agent before your customers’ agents do.

The Rule to Adopt

Write every response as if a model will act on it without asking a follow-up question, because increasingly one will. If the response alone is not enough to take the right next step safely, fix the response.


How Incresco Helps

Incresco is a Claude partner. We build web and mobile applications, cloud systems, Salesforce solutions, WhatsApp experiences and AI agents. For agent readiness, we review your APIs and customer flows the way an agent meets them: errors, retries, limits, data and tools. Then we fix what gets in the way and build MCP servers where they pay off. See our custom platform development, system integrations and AI transformation services.

Wondering how your API holds up when the client is an AI agent? Contact Incresco and ask for an agent experience review. We will go through your key endpoints and flows, and tell you what to fix first.


Frequently Asked Questions

What Is Agent Experience (AX)?

Agent experience is how well an AI agent can discover, use and recover from errors in your product when it acts for a person. Netlify’s Mathias Biilmann introduced the term in January 2025. For engineers, it comes down to clear errors, safe retries, explicit limits, machine-readable facts and well-described tools.

What Is Generative UI?

Generative UI means an AI model builds the interface for each question, such as a calculator, chart or map, instead of showing a screen designed in advance. Google launched it in the Gemini app and Google Search in November 2025, and ChatGPT’s Intelligent UI followed on October 7, 2026.

What Is an Idempotency Key?

It is a unique value the client sends with a request, and reuses on every retry, so the server can make sure the operation runs only once. It prevents a timeout and a retry from turning into a double charge or a double booking.

Do I Need an MCP Server?

Not always. Start with clear errors, idempotent writes and documented APIs. An MCP server is worth building when you expect AI assistants to use your service regularly, because it gives them tools with descriptions and schemas instead of pages to click through.

Ready to stop experimenting and
start operating?