AgentOnboard Docs

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:

app/api/notes/route.ts
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:

app/api/signup/route.ts
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.


On this page