iss: https://id.agentboxd.com
LiveLet AI agents sign in to your app.
Agent Login is Sign in with Agentboxd: an OpenID Connect provider where the user is an AI agent. Agents prove who they are with a short-lived token instead of a password, and your app knows it is talking to an agent.
- exp − iat ≤ 300
- Every token lives 5 minutes at most, works once, and is addressed to one app.
- sub (pairwise)
- Each app sees a different, stable subject for the same agent.
- OIDC · PKCE · JWKS
- Standard OpenID Connect at id.agentboxd.com. Better Auth, Auth.js or any OIDC client.
https://agentboxd.com/claims/agent: true
Two sides of one sign-in.
Apps decide to accept agents; agents need a way in that isn’t a borrowed password. Agent Login is built for both.
You build an app or an API
AI agents already use your product: they sign up with passwords and click verification emails, or share a person’s credentials. Give them a front door of their own.
- Know it is an agent. Every token carries
agent: true, so you can route agents to API-first onboarding or their own limits. - No passwords to store and no email loop to automate. Verify a signed token and start your session.
- Standard OpenID Connect. Discovery, JWKS, code flow with PKCE, or a server-side token exchange. No proprietary SDK required.
You build AI agents
Your agent already has an Agentboxd inbox. That inbox is also its identity: one API call and it signs in to any app that accepts Sign in with Agentboxd.
- One call, no secrets to keep.
mr.identity.token()returns a 5-minute token for one app. - Private by default. Each app sees a different subject, so apps can’t follow your agent across services.
- You stay in charge. Switch sign-in off per inbox and see every app it signed in to.
POST /v1/inboxes/:id/identity-token
How it works.
Three steps, and no password anywhere. This is the headless flow, for agents that call your API or app directly.
- /app/identity
Register your app
In the dashboard, register the app and pick its type: a server app (with the button), a public app, or verify-only for agents that call your API. You get a
client_id, the audience of every token. - POST /v1/inboxes/:id/identity-token
The agent asks for a token
With one call the agent gets an ES256-signed ID token addressed to your
client_id, valid for 5 minutes, and sends it to you. From an MCP client it is theget_identity_tokentool. - verifyAgentIdentityToken
You verify it and start a session
Check the signature against the public keys, the issuer, the audience, the expiry and the nonce, and reject a token seen before. The SDK does all of it; or exchange the token at the issuer, which enforces single use for you.
import { Agentboxd } from 'agentboxd';
const mr = new Agentboxd(); // a key with identity:sign (preset "sign_in")
// A 5-minute, single-use ID token addressed to one app (its client_id).
const { id_token } = await mr.identity.token({
inboxId: inbox.id,
audience: 'abxc_4kQ9...',
nonce, // optional: the one the app gave you
});
await fetch('https://app.example.com/login/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_token }),
});import { MemoryReplayCache, verifyAgentIdentityToken } from 'agentboxd/identity'; // npm install agentboxd jose
const replayCache = new MemoryReplayCache(); // several processes: back it with Redis (SET jti 1 NX EXAT exp)
export async function signInAgent(idToken: string, nonce?: string) {
const agent = await verifyAgentIdentityToken(idToken, {
audience: process.env.AGENTBOXD_CLIENT_ID!, // your client_id
nonce, // if you handed the agent one
replayCache, // single use: a second presentation of the same jti throws
});
// agent.sub is stable for your app and different at every other app: key the user on it.
return { sub: agent.sub, email: agent.email, isAgent: agent.isAgent };
}{
"iss": "https://id.agentboxd.com",
"sub": "Qm9vZ2xlLXBhaXJ3aXNlLXN1YmplY3QtZXhhbXBsZQ",
"aud": "abxc_4kQ9...",
"iat": 1790327643,
"exp": 1790327943,
"auth_time": 1790327643,
"jti": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
"nonce": "n-3f9a2c",
"email": "support-bot@agents.agentboxd.com",
"email_verified": true,
"https://agentboxd.com/claims/agent": true
}alg: ES256
Built so a token can’t be reused.
An agent’s token is worth little to anyone who steals it: it is short-lived, single-use, and good for one app only. The details are in the docs.
- ES256
Signed, with rotating keys
Tokens are signed with ECDSA P-256 keys that rotate. Verifiers fetch them from the JWKS;
noneand HMAC algorithms are refused. - ≤ 300 S
Short-lived, no refresh tokens
An ID token lives 5 minutes at most. There are no refresh tokens: an agent signs in again with one API call, and your session decides how long it stays.
- SINGLE USE
Every token works once
Each token has a unique
jti. Keep a replay cache, or exchange it at the issuer with the JWT bearer grant (RFC 7523), which records it and refuses a second use. - AUD = CLIENT_ID
Bound to one app
A token names exactly one audience, your
client_id. A token issued for another app fails verification, and can’t be redeemed at the issuer by another client. - PAIRWISE SUB
A different subject at every app
Each app sees its own stable
subfor the same agent, so two apps can’t join their user tables to track it. Key your users onsub. - AGENT: TRUE
Agents say they are agents
Every token carries
https://agentboxd.com/claims/agent: true. The subject is an AI agent’s inbox, never a person pretending otherwise. - PKCE S256
A safe browser flow
The button uses the authorization code flow with S256 PKCE,
stateandnonceall required, exact redirect URIs, 60-second single-use codes andissin the response (RFC 9207). - SWITCH · LOG
Owners see and stop every sign-in
Each inbox has a sign-in switch that takes effect at once, a history of every token and sign-in (kept 180 days), and a webhook for each one.
GET {issuer}/authorize
Add a Sign in with Agentboxd button.
For a web app, a person clicks the button: the agent’s owner. They approve on agentboxd.com and choose which of their agents’ inboxes signs in, and your app gets a normal OpenID Connect result, with agent: true in it.
Point the button at your auth library’s sign-in route for the agentboxd provider. The markup on the right is a starting point; keep the label “Sign in with Agentboxd”.
<a class="siwa" href="/api/auth/signin/agentboxd">
<svg width="20" height="20" viewBox="0 0 32 32" aria-hidden="true">
<circle cx="12.5" cy="16" r="8" fill="none" stroke="#D4FF3A" stroke-width="3" />
<path d="M22.5 11.5h7M23.5 16h6M22.5 20.5h7" stroke="#EDEAE2" stroke-width="2.2" />
</svg>
Sign in with Agentboxd
</a>
<style>
.siwa { display: inline-flex; align-items: center; gap: 10px; min-height: 44px;
padding: 0 18px 0 12px; border: 1px solid #2A2D33; border-radius: 2px;
background: #0A0B0D; color: #EDEAE2; font: 600 15px/1 system-ui, sans-serif;
text-decoration: none; }
.siwa:focus-visible { outline: 2px solid #D4FF3A; outline-offset: 2px; }
</style>/.well-known/openid-configuration
Works with the auth you already have.
Agent Login is plain OpenID Connect, so there is nothing new to learn. Point your library at the discovery document and it finds the keys and endpoints.
- Better Auth: the Generic OAuth plugin, with PKCE and ID-token verification.
- Auth.js (NextAuth): a small OIDC provider with pkce, state and nonce checks.
- Any OIDC client with the authorization code flow and PKCE, in any language.
- Headless: verifyAgentIdentityToken from agentboxd/identity, or the RFC 7523 token exchange.
The Better Auth, Auth.js and headless code is typechecked in examples/identity and walked through in the guide.
import { betterAuth } from 'better-auth';
import { genericOAuth } from 'better-auth/plugins';
const ISSUER = process.env.AGENTBOXD_ISSUER ?? 'https://id.agentboxd.com';
export const auth = betterAuth({
// database: your adapter
user: {
additionalFields: { isAgent: { type: 'boolean', required: false, defaultValue: false, input: false } },
},
plugins: [
genericOAuth({
config: [
{
providerId: 'agentboxd',
name: 'Agentboxd',
discoveryUrl: `${ISSUER}/.well-known/openid-configuration`,
clientId: process.env.AGENTBOXD_CLIENT_ID!,
clientSecret: process.env.AGENTBOXD_CLIENT_SECRET!,
scopes: ['openid', 'email', 'profile'],
pkce: true,
requireIdTokenVerification: true,
mapProfileToUser: (profile) => ({
name: typeof profile.name === 'string' ? profile.name : undefined,
email: typeof profile.email === 'string' ? profile.email : null,
emailVerified: profile.email_verified === true,
isAgent: profile['https://agentboxd.com/claims/agent'] === true,
}),
},
],
}),
],
});
// In the browser:
// await authClient.signIn.social({ provider: 'agentboxd', callbackURL: '/dashboard' });import type { OIDCConfig, OIDCUserConfig } from '@auth/core/providers';
export interface AgentboxdProfile extends Record<string, unknown> {
sub: string;
email?: string;
email_verified?: boolean;
name?: string;
'https://agentboxd.com/claims/agent': true;
}
export default function Agentboxd(options: OIDCUserConfig<AgentboxdProfile>): OIDCConfig<AgentboxdProfile> {
return {
id: 'agentboxd',
name: 'Agentboxd',
type: 'oidc',
issuer: 'https://id.agentboxd.com',
checks: ['pkce', 'state', 'nonce'], // Agentboxd requires all three
authorization: { params: { scope: 'openid email profile' } },
profile: (p) => ({ id: p.sub, name: p.name ?? p.email ?? null, email: p.email ?? null, image: null }),
options,
};
}curl https://id.agentboxd.com/.well-known/openid-configuration
# Configure any OpenID Connect client with:
# issuer https://id.agentboxd.com
# client_id abxc_... (register the app in the dashboard, Identity)
# client_secret abxs_... (server apps only; shown once)
# scopes openid email profile
# flow authorization code + PKCE (S256), with state and noncecurl -X POST https://id.agentboxd.com/token \
-u "$AGENTBOXD_CLIENT_ID:$AGENTBOXD_CLIENT_SECRET" \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
-d assertion="$ID_TOKEN"
# 200 { "access_token": "abxat_...", "token_type": "Bearer", "expires_in": 300,
# "id_token": "eyJ...", "scope": "openid email" }
# A second exchange of the same token: 400 { "error": "invalid_grant" }Questions
Questions.
What is Agent Login?
Agent Login, or Sign in with Agentboxd, is an OpenID Connect identity provider at id.agentboxd.com where the user is an AI agent. An agent’s identity is its Agentboxd inbox, and it signs in to apps with a short-lived, single-use ID token instead of a password.
Do I need an Agentboxd account to accept agents in my app?
You need a free account to register your app and get a client_id. After that, verifying a token needs only the public keys: any OpenID Connect or JOSE library works, and the agentboxd TypeScript package has a helper that does every check. Create an account.
Which libraries does it work with?
Any OpenID Connect client that supports the authorization code flow with PKCE. The guide has working setups for Better Auth (Generic OAuth plugin) and Auth.js, and a headless relying party in TypeScript. Other clients only need the discovery URL, your client_id and, for server apps, the client secret. The guide.
How is a token kept from being replayed or stolen?
It lives at most 5 minutes, names exactly one app as its audience, carries a unique jti that your app or the issuer accepts once, and can be bound to a login attempt with a nonce. The browser flow adds PKCE, state and exact redirect URIs.
Can apps track my agent across services?
Not through Agent Login. By default each app receives a different subject for the same agent. The email claim is the same everywhere, so an app that asks for it can match on it; apps that care about privacy shouldn’t ask for it.
Who approves a browser sign-in?
The agent’s owner. The button sends them to agentboxd.com, where they see your app’s name, its redirect host and the scopes, pick which inbox signs in and approve. There is no remembered consent: every browser sign-in is approved.
What does it cost?
Agent Login is included on every Agentboxd plan, Free included. Apps that only accept tokens don’t pay anything to verify them. Pricing.
/app/identity
Open your app to agents.
Register your app in the dashboard to get a client_id, then follow the guide. Free on every plan.