AgentDoor provides a complete pre-authentication layer for AI agents, enabling them to register, authenticate, and access your API programmatically in under 500ms—no browser automation required.
Architecture Overview
AgentDoor sits as middleware between your API and incoming agent requests, handling three core responsibilities:
Agent Discovery - Agents find your service and capabilities via /.well-known/agentdoor.json
Registration & Authentication - Challenge-response protocol using Ed25519 signatures
Request Authorization - JWT-based or signature-based auth on every API call
Complete Flow
Here’s the complete agent onboarding and request flow with timing:
Phase 1: Discovery (~50ms)
The agent fetches your service’s capabilities and requirements:
const discovery = await fetch ( 'https://api.example.com/.well-known/agentdoor.json' );
const config = await discovery . json ();
Response:
{
"agentdoor_version" : "1.0" ,
"service_name" : "Weather API" ,
"service_description" : "Real-time weather data and forecasts" ,
"registration_endpoint" : "/agentdoor/register" ,
"auth_endpoint" : "/agentdoor/auth" ,
"scopes_available" : [
{
"id" : "weather.read" ,
"description" : "Read current weather data" ,
"price" : "$0.001/req" ,
"rate_limit" : "1000/hour"
}
],
"auth_methods" : [ "ed25519-challenge" , "x402-wallet" , "jwt" ],
"payment" : {
"protocol" : "x402" ,
"version" : "2.0" ,
"networks" : [ "base" ],
"currency" : [ "USDC" ]
}
}
Phase 2: Registration (~100ms)
The agent submits its public key and requests scopes:
import { generateKeypair } from '@agentdoor/core' ;
const keypair = generateKeypair ();
const registerResponse = await fetch ( 'https://api.example.com/agentdoor/register' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
public_key: keypair . publicKey , // Base64-encoded Ed25519 public key
scopes_requested: [ 'weather.read' ],
x402_wallet: '0x1234...abcd' , // Optional: for payments
metadata: {
framework: 'langchain' ,
version: '0.1.0'
}
})
});
const challenge = await registerResponse . json ();
Response:
{
"agent_id" : "ag_V1StGXR8_Z5jdHi6B" ,
"challenge" : {
"nonce" : "Xy9k3mP7qR2sT5vW8zA1bC4dE6fG9hJ0" ,
"message" : "agentdoor:register:ag_V1StGXR8_Z5jdHi6B:1709481600:Xy9k3mP7qR2sT5vW8zA1bC4dE6fG9hJ0" ,
"expires_at" : "2024-03-03T12:10:00.000Z"
}
}
Server-side: AgentDoor generates a unique agent ID and creates a time-limited challenge. The challenge message format is:
agentdoor:register:{agent_id}:{timestamp}:{nonce}
This challenge is stored temporarily (default: 5 minutes) and associated with the pending registration.
Phase 3: Challenge Response (~200ms)
The agent signs the challenge message with its private key and returns the signature:
import { signChallenge } from '@agentdoor/core' ;
// Agent signs the challenge with its private key
const signature = signChallenge (
challenge . challenge . message ,
keypair . secretKey
);
const verifyResponse = await fetch ( 'https://api.example.com/agentdoor/register/verify' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
agent_id: challenge . agent_id ,
signature: signature // Base64-encoded Ed25519 signature
})
});
const credentials = await verifyResponse . json ();
Response:
{
"agent_id" : "ag_V1StGXR8_Z5jdHi6B" ,
"api_key" : "agk_live_Xy9k3mP7qR2sT5vW8zA1bC4dE6fG9hJ0kL2mN4oP6qR8" ,
"scopes_granted" : [ "weather.read" ],
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"token_expires_at" : "2024-03-03T13:05:00.000Z" ,
"rate_limit" : {
"requests" : 1000 ,
"window" : "1h"
},
"x402" : {
"payment_address" : "0xYourWallet..." ,
"network" : "base" ,
"currency" : "USDC"
}
}
Server-side: AgentDoor:
Retrieves the stored challenge for this agent_id
Verifies the signature using the agent’s public key
Checks that the challenge hasn’t expired
Creates the agent record in storage
Generates an API key (hashed for storage, returned in plaintext once)
Issues a JWT token for immediate use
Returns credentials and rate limits
The api_key is only returned once during registration. Store it securely. The server stores a SHA-256 hash of the key, not the plaintext.
Phase 4: Making Authenticated Requests
Once registered, the agent can make authenticated requests using either:
Option A: JWT Bearer Token (Most Common)
const response = await fetch ( 'https://api.example.com/api/weather/forecast?city=sf' , {
headers: {
'Authorization' : `Bearer ${ credentials . token } `
}
});
Server-side: The AgentDoor middleware:
Extracts the JWT from the Authorization header
Verifies the signature using the shared secret
Checks expiration
Extracts agent context (ID, scopes, metadata)
Attaches to req.agent for your route handlers
Checks rate limits
Proceeds to your API logic
Option B: API Key Authentication
const response = await fetch ( 'https://api.example.com/api/weather/forecast?city=sf' , {
headers: {
'Authorization' : `Bearer ${ credentials . api_key } `
}
});
Server-side: AgentDoor:
Detects the agk_ prefix (API key vs JWT)
Hashes the provided key
Looks up the agent record by key hash
Checks agent status (active/suspended/banned)
Attaches agent context to the request
Checks rate limits
Phase 5: Token Refresh (When JWT Expires)
JWTs are short-lived (default: 1 hour). When a token expires, agents refresh via signature-based auth:
import { buildAuthMessage , signChallenge } from '@agentdoor/core' ;
const timestamp = new Date (). toISOString ();
const message = buildAuthMessage ( agent_id , timestamp );
const signature = signChallenge ( message , keypair . secretKey );
const authResponse = await fetch ( 'https://api.example.com/agentdoor/auth' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
agent_id: agent_id ,
timestamp: timestamp ,
signature: signature
})
});
const { token , expires_at } = await authResponse . json ();
Auth message format:
agentdoor:auth:{agent_id}:{timestamp}
Server-side: AgentDoor:
Retrieves the agent’s stored public key
Verifies the signature
Checks timestamp freshness (default: within 5 minutes)
Issues a fresh JWT
Updates lastAuthAt timestamp
The AgentDoor SDK handles token refresh automatically . Agents never need to manually implement refresh logic.
Total Timing Breakdown
Phase Operation Time Cumulative 1 Discovery ~50ms 50ms 2 Registration ~100ms 150ms 3 Challenge-Response ~200ms 350ms 4 First Request ~50ms 400ms
Compare this to browser automation:
Puppeteer/Playwright: 30-60 seconds
Human signup flow: 2-5 minutes
Request Context in Your API
Once AgentDoor middleware is active, every incoming request is enriched with agent context:
app . get ( '/api/weather/forecast' , ( req , res ) => {
// Check if request is from an agent
if ( req . isAgent ) {
console . log ( 'Agent request detected' );
// Access authenticated agent context
if ( req . agent ) {
console . log ( 'Agent ID:' , req . agent . id );
console . log ( 'Scopes:' , req . agent . scopes );
console . log ( 'Framework:' , req . agent . metadata . framework );
console . log ( 'Reputation:' , req . agent . reputation );
}
}
res . json ({ forecast: 'sunny' });
});
Available context:
interface AgentContext {
id : string ; // "ag_V1StGXR8_Z5jdHi6B"
publicKey : string ; // Base64-encoded Ed25519 public key
scopes : string []; // ["weather.read", "weather.write"]
rateLimit : { // Agent-specific rate limit
requests : number ;
window : string ;
};
reputation ?: number ; // 0-100 score
metadata : Record < string , string >; // framework, version, etc.
}
Security Model
Private Key Never Leaves Agent
Unlike API keys or OAuth tokens, the agent’s private key never touches the network :
// ✅ SAFE: Signature is sent, not the key
const signature = signChallenge ( message , privateKey );
fetch ( '/agentdoor/register/verify' , {
body: JSON . stringify ({ signature })
});
// ❌ NEVER: Private key stays local
fetch ( '/agentdoor/register' , {
body: JSON . stringify ({ private_key: privateKey })
});
Time-Limited Challenges
Every challenge expires after 5 minutes (configurable). Replay attacks are prevented:
if ( Date . now () > challenge . expiresAt . getTime ()) {
throw new ChallengeExpiredError ( 'Challenge expired. Request a new one.' );
}
API Key Hashing
API keys are only shown once at registration. Stored as SHA-256 hashes:
import { hashApiKey } from '@agentdoor/core' ;
// At registration
const rawKey = generateApiKey ( 'live' ); // "agk_live_..."
const hash = hashApiKey ( rawKey ); // SHA-256 hex
// Store hash, return raw key to agent once
await storage . createAgent ({
apiKeyHash: hash // Only hash is stored
});
return { api_key: rawKey }; // Returned once, then discarded
JWT Short-Lived Tokens
JWTs expire quickly (default: 1 hour). Refresh requires signature proof:
// Token refresh requires signing a fresh timestamp
const timestamp = new Date (). toISOString ();
const message = `agentdoor:auth: ${ agent_id } : ${ timestamp } ` ;
const signature = signChallenge ( message , privateKey );
// Server verifies signature before issuing new token
// Stolen JWT alone cannot be refreshed
Storage Layer
AgentDoor supports multiple storage backends:
Driver Use Case Configuration memory Development, testing No setup required sqlite Single-server production { driver: 'sqlite', url: 'file:./agentdoor.db' }postgres Multi-server production { driver: 'postgres', url: 'postgresql://...' }redis Edge workers, high-scale { driver: 'redis', url: 'redis://...' }
Stored data:
Agents : ID, public key, x402 wallet, scopes, API key hash, rate limits, reputation, metadata
Challenges : Pending registration challenges (temporary, auto-expire)
Rate limits : Request counts per agent per time window
Sessions : JWT blacklist for revocation (optional)
Next Steps
Authentication Deep dive into Ed25519 challenge-response and JWT handling
Discovery Protocol Learn how agents find and connect to your API
Agent Detection Understand how AgentDoor identifies agent traffic
Payments Integrate x402 protocol for per-request payments