AgentOnboard Docs

Publishing auth.md

The discovery file that tells an agent your API accepts AgentOnboard, and the one canonical rejection table.

A coding agent that has never heard of your API will not find it, and one that has heard of it will not know which hostname to mint a token for or what a 403 means. auth.md is the file that answers both. You host plain markdown at the root of your domain; an agent that knows the convention reads it, and an agent that does not will still authenticate correctly — it will just have to be told.

Nothing we ship fetches this file

There is no reader in the SDK, none in the aon CLI, and no validator anywhere. That is exactly why the file has one job and why the format below has to be exact: the document is the contract, and there is no tool to tell you it is wrong.


Where it goes

https://<your-audience>/auth.md — HTTPS, no authentication, no redirect to another host. If you serve on Next.js, public/auth.md is already that URL:

auth.md
favicon.ico
package.json

The format

Three required sections, in this order. No front matter, no version field, no machine-readable block.

SectionWhat it is
## AudienceThe exact string to pass to aon token get <string>. One argument, no flags.
## EndpointsOne ### per endpoint: method, path, request shape, success response.
## When a request is rejectedThe status and code for each rejection, and what the agent should do about it.

Put a YAML header in it and nothing will notice. That is deliberate: a field nothing reads is a field you maintain for nothing.

The ## Audience string has to work in two places

The same string has to agree, after normalization, with the audience you pass to requireAgentAccount or verifyAgentToken. Both sides run the same normalization — scheme, path and port dropped, lowercased, localhost keeping its port — so https://api.notes.com/v1/ and api.notes.com are the same audience. Write the bare hostname you serve on. If your API lives on a subdomain, that subdomain is a separate audience with its own auth.md.

The comparison that follows is exact, never by prefix or suffix, so notes.com.evil.com is not notes.com and a token minted for one is rejected by the other.


The canonical file

This is the one format. Everything else on this page is commentary about it.

public/auth.md
# Notes API

This service accepts AgentOnboard. Verified agent requests are authorized against the
account the verified email maps to.

## Audience

`notes.com`

    aon token get notes.com

Send the token on every request as `Authorization: Bearer <token>`.

## Endpoints

### `POST /api/notes`

Create a note. Rate limited to 60 requests per minute per account.

- Request: `{ "title": string, "body": string }`
- Success: `201` with `{ "id": string, "title": string, "body": string }`
- Required header: `Authorization: Bearer <token>`

### `GET /api/notes`

List the calling account's notes. Never returns another account's notes.

- Request: no body; optional query `limit` (default 50, max 200)
- Success: `200` with `{ "notes": Array<{ "id": string, "title": string }> }`
- Required header: `Authorization: Bearer <token>`

## When a request is rejected

| Status | `code` | What to do |
|---|---|---|
| 401 | any | The token was rejected. Run `aon token get notes.com` again and retry **once**. If it fails again, stop and tell the human — do not retry in a loop. |
| 403 | `ACCOUNT_REQUIRED` | The identity is genuine but has no Notes account. Ask the human to connect their account at notes.com/connect, then retry. **Retrying without that will never work.** |
| 500 | `RESOLVER_ERROR` or `CONFIG_ERROR` | Our account lookup failed, or our configuration is wrong. Retry after a short delay. Do not re-authenticate; the token is fine. |

Why that rejection table is the highest-value part

It is the only place an agent is told the difference between "mint a new token" and "ask the human to do something", and that difference is exactly what the 401 / 403 / 500 split carries. An agent that reads it does not need a human to interpret a 403.

Two things in that table are load-bearing, and both are easy to get backwards:

403, and connect — never register

ACCOUNT_REQUIRED is 403. A 401 there tells the agent its token is bad, and the agent's correct response to a 401 is to re-authenticate, which cannot create an account.

And the action is connect, not register. Telling an agent to send the human to a signup URL is the flow this whole model warns against: with no consent model, an account created on the spot is an account whose ownership was never demonstrated. There is no consent step that a signup would perform that the connection does not. Return the connect flow, and say plainly that a retry without it will never succeed.


Before you publish

  • The code column must match what your responses actually return. If you use onFailure to stop publishing the specific code, rewrite the column to whatever your body does carry. A table naming codes your responses never return is worse than no table.
  • Treat every line as published. No API key, no token, no private endpoint, no internal hostname.
  • The endpoints you list are the endpoints you protect. An agent that reads a file advertising an unprotected endpoint will use it.

And the request you actually received

If the SDK is wired in, an agent reading this file needs no other instruction. If it is not, the agent will mint a token, send it, and get a 401 it cannot interpret — so publish the file after the integration works, not before.

On this page