Setting Up Agent Signup
The explicit two-step wiring end to end — guard every route, add one signup handler, publish it in auth.md.
This is the whole integration for a service that lets agents sign strangers up. Two handlers, one file published, and a test pass before it goes live. It assumes mapping an identity to an account already reads clearly — that page is the reference, this page is the walkthrough.
Guard every route with verifyAgentAccount
Nothing changes here. Unknown identities get ACCOUNT_REQUIRED, exactly as a reject-only service answers:
import { verifyAgentAccount } from "@agentonboard/sdk";
export async function POST(request: Request) {
const outcome = await verifyAgentAccount({
token: request.headers.get("Authorization") ?? "",
audience: "notes.com",
resolveAccount: async ({ email }) => {
const user = await db.user.findUnique({ where: { email: normalizeEmail(email) } });
return user?.id ?? null;
},
});
if (!outcome.ok) {
return Response.json(
{ code: outcome.code, error: outcome.error },
{ status: outcome.code === "ACCOUNT_REQUIRED" ? 403 : 401 },
);
}
return Response.json({ created_by: outcome.user });
}Add one signup handler with createAgentAccount
This is the only route that may mint an account. The function verifies the token first, resolves, and creates on a miss — a hit signs in idempotently, so a double registration cannot duplicate:
import { createAgentAccount } from "@agentonboard/sdk";
export async function POST(request: Request) {
const outcome = await createAgentAccount({
token: request.headers.get("Authorization") ?? "",
audience: "notes.com",
resolveAccount: async ({ email }) => {
const user = await db.user.findUnique({ where: { email: normalizeEmail(email) } });
return user?.id ?? null;
},
createAccount: async ({ email }) => {
const user = await db.user.create({ data: { email: normalizeEmail(email) } });
return user.id;
},
});
if (!outcome.ok) {
return Response.json(
{ code: outcome.code, error: outcome.error },
{ status: outcome.code === "RESOLVER_ERROR" || outcome.code === "CONFIG_ERROR" ? 500 : 401 },
);
}
return Response.json({ user: outcome.user, created: outcome.created }, { status: 201 });
}One handler, not every route
Wrapping every route in createAgentAccount mints accounts as a side effect of requests that were never trying to create one — including reads. The signup handler is the whole opt-in: greppable in your repository, visible in a diff, and the only place an account comes into being.
Publish it in auth.md
The agent learns the two steps from your file: the 403 row names the signup endpoint, and the endpoint list documents it. The canonical wording lives on the auth.md page — copy it, don't paraphrase it:
### `POST /api/signup`
Create the calling account. An agent that got `403 ACCOUNT_REQUIRED`
elsewhere calls this with the same token, then retries its original request.
| Status | `code` | What to do |
|---|---|---|
| 403 | `ACCOUNT_REQUIRED` | The identity is genuine but has no Notes account. Create one at `POST /api/signup` with the same token, then retry the original request **once**. |Prove it before you publish it
Run the integration test pass: unknown email → 403, signup → 201 with created: true, retry the original → served, signup again → created: false with the same user, forged token → 401 without touching either callback. If any of those behaves differently, the wiring is wrong — not the agent.
Handling Unknown Users (Reject vs Create)
Deciding what happens when an agent presents a valid token not in your database — reject with ACCOUNT_REQUIRED or enable explicit signup.
Testing Your Integration with curl
Six checks against your own deployment that prove the wiring — run them, don't trust this page.