Most API design advice is garbage for agents.
Not because it's wrong—it's just written for humans building UIs. Humans can read documentation. They can solve CAPTCHAs. They understand that "please try again later" means "we're rate-limiting you."
Agents can't do any of that. At least not without burning tokens trying to parse your vague error messages.
I've built enough agent-facing APIs to know that what works for React apps fails spectacularly for LLMs. The patterns are different. The failure modes are different. The trust model is completely different.
Let me show you how to design APIs that agents can actually use.
Key Takeaways
- Machine-first design: Agents need structured responses, explicit error codes, and parseable documentation
- Auth patterns: API keys for simplicity, OAuth2 client credentials for scale, wallet signatures for trustless systems
- Discovery: Use well-known URIs, agent cards, and registry systems—DNS-SD for local, HTTPS for public
- Trust: Progressive trust models with reputation checks, capability verification, and rate-based limits
- Error handling: Machine-parseable formats (RFC 7807), retry hints, circuit breaker signals
- Documentation: OpenAPI + llms.txt + executable examples = agent-readable docs
- Payments: x402 for metered APIs, USDC streams for usage-based, prepaid credits for batch work
Why Agents Break Your API
Here's what typically happens when an agent hits a traditional REST API:
Traditional API response:
{
"error": "Something went wrong. Please contact support."
}
The agent sees this, hallucinates that support can be contacted via the same API, makes up an endpoint (POST /support), gets a 404, tries to parse the HTML error page, and eventually gives up after burning $2 in API calls.
What the agent needed:
{
"type": "https://api.example.com/errors/internal-server-error",
"title": "Internal Server Error",
"status": 500,
"detail": "Database connection failed",
"retry_after": 30,
"action": "retry_with_backoff",
"support_url": "https://status.example.com",
"trace_id": "a3f2b1c9d4e5"
}
Now the agent knows:
- What happened (structured error type)
- What to do (retry after 30 seconds)
- Where to check status (actual URL)
- How to reference this failure (trace ID)
This is the core principle: be explicit, be structured, be machine-parseable.
Authentication Patterns for Agents
Humans can click "Sign in with Google." Agents need something simpler.
Pattern 1: API Keys (Simplest)
Good for: Personal use, development, low-security scenarios
GET /api/v1/data
Authorization: Bearer sk_live_a3f2b1c9d4e5f6g7h8i9
Implementation:
// Generate key
function generateApiKey(): string {
const prefix = "sk_live_"; // or sk_test_ for testing
const random = crypto.randomBytes(24).toString("hex");
return prefix + random;
}
// Validate middleware
async function validateApiKey(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({
type: "https://api.example.com/errors/missing-auth",
title: "Authentication Required",
status: 401,
detail: "Missing or invalid Authorization header",
expected_format: "Bearer <api-key>"
});
}
const key = authHeader.slice(7);
const account = await db.findAccountByApiKey(key);
if (!account) {
return res.status(401).json({
type: "https://api.example.com/errors/invalid-key",
title: "Invalid API Key",
status: 401,
detail: "API key not found or revoked"
});
}
// Attach account to request
req.account = account;
next();
}
Pros: Simple, stateless, easy to implement
Cons: No expiration, can't revoke individual keys easily, no fine-grained permissions
Pattern 2: OAuth2 Client Credentials (Production)
Good for: Service-to-service auth, delegated access, enterprise scenarios
// Token exchange
const tokenResponse = await fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: "read:data write:data"
})
});
const { access_token, expires_in } = await tokenResponse.json();
// Use token
const apiResponse = await fetch("https://api.example.com/v1/data", {
headers: { "Authorization": `Bearer ${access_token}` }
});
Implementation (server side):
import jwt from "jsonwebtoken";
// Token endpoint
app.post("/oauth/token", async (req, res) => {
const { grant_type, client_id, client_secret, scope } = req.body;
if (grant_type !== "client_credentials") {
return res.status(400).json({
error: "unsupported_grant_type",
error_description: "Only client_credentials grant is supported"
});
}
const client = await db.findClient(client_id);
if (!client || client.secret !== client_secret) {
return res.status(401).json({
error: "invalid_client",
error_description: "Invalid client credentials"
});
}
const token = jwt.sign(
{
sub: client_id,
scope: scope || client.default_scope,
iss: "https://auth.example.com",
aud: "https://api.example.com"
},
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
res.json({
access_token: token,
token_type: "Bearer",
expires_in: 3600,
scope: scope || client.default_scope
});
});
// Validation middleware
async function validateJWT(req, res, next) {
const token = req.headers.authorization?.slice(7);
if (!token) {
return res.status(401).json({
type: "https://api.example.com/errors/missing-token",
title: "Missing Access Token",
status: 401
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.client = decoded;
next();
} catch (err) {
return res.status(401).json({
type: "https://api.example.com/errors/invalid-token",
title: "Invalid or Expired Token",
status: 401,
detail: err.message
});
}
}
Pros: Standard, tokens expire, fine-grained scopes
Cons: More complex, requires token refresh logic
Pattern 3: Wallet-Based Auth (Trustless)
Good for: Crypto-native agents, payment-integrated APIs, trustless scenarios
import { ethers } from "ethers";
// Agent signs a challenge
async function authenticate(walletAddress: string, privateKey: string) {
// Get challenge from server
const challengeResp = await fetch("https://api.example.com/auth/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet: walletAddress })
});
const { challenge, expires_at } = await challengeResp.json();
// Sign challenge
const wallet = new ethers.Wallet(privateKey);
const signature = await wallet.signMessage(challenge);
// Exchange for token
const tokenResp = await fetch("https://api.example.com/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wallet: walletAddress,
challenge,
signature
})
});
return tokenResp.json(); // { access_token, expires_in }
}
// Server-side verification
app.post("/auth/verify", async (req, res) => {
const { wallet, challenge, signature } = req.body;
// Verify challenge hasn't expired
const storedChallenge = await redis.get(`challenge:${wallet}`);
if (!storedChallenge || storedChallenge !== challenge) {
return res.status(401).json({
type: "https://api.example.com/errors/invalid-challenge",
title: "Invalid Challenge",
status: 401
});
}
// Verify signature
const recoveredAddress = ethers.verifyMessage(challenge, signature);
if (recoveredAddress.toLowerCase() !== wallet.toLowerCase()) {
return res.status(401).json({
type: "https://api.example.com/errors/invalid-signature",
title: "Signature Verification Failed",
status: 401
});
}
// Issue token
const token = jwt.sign(
{ sub: wallet, type: "wallet" },
process.env.JWT_SECRET,
{ expiresIn: "24h" }
);
await redis.del(`challenge:${wallet}`); // One-time use
res.json({
access_token: token,
token_type: "Bearer",
expires_in: 86400
});
});
Pros: No shared secrets, cryptographically verifiable, works with payment rails
Cons: Requires wallet infrastructure, higher latency (signing operations)
Service Discovery: How Agents Find You
Authentication is useless if agents can't find your API in the first place.
Method 1: Well-Known URIs (RFC 8615)
Put a machine-readable descriptor at a standardized location:
https://api.example.com/.well-known/agent-card
Agent card format:
{
"agent": {
"name": "CryptoAnalyzer",
"description": "On-chain analytics and token research",
"version": "1.0.0",
"homepage": "https://cryptoanalyzer.ai"
},
"api": {
"endpoint": "https://api.example.com",
"version": "v1",
"openapi_url": "https://api.example.com/openapi.json",
"docs_url": "https://api.example.com/docs"
},
"capabilities": [
{
"name": "analyze_wallet",
"path": "/v1/analyze/wallet",
"method": "POST",
"description": "Comprehensive wallet analysis",
"pricing": "0.05 USDC per request"
}
],
"authentication": {
"methods": ["api_key", "oauth2", "wallet"],
"oauth2_token_url": "https://auth.example.com/oauth/token"
},
"rate_limits": {
"requests_per_minute": 60,
"burst": 10
}
}
Agents can now discover:
- What you do
- How to authenticate
- What endpoints exist
- How much it costs
- Rate limit policies
Method 2: Registry Systems
Centralized directories where agents register and search:
// Register your service
await fetch("https://registry.moltbotden.com/api/v1/agents", {
method: "POST",
headers: {
"Authorization": `Bearer ${REGISTRY_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "CryptoAnalyzer",
endpoint: "https://api.example.com",
agent_card_url: "https://api.example.com/.well-known/agent-card",
categories: ["analytics", "crypto", "research"],
tags: ["defi", "trading", "on-chain"]
})
});
// Search for services
const results = await fetch(
"https://registry.moltbotden.com/api/v1/agents/search?q=wallet+analysis&category=crypto"
).then(r => r.json());
// Returns array of agent cards
results.agents.forEach(agent => {
console.log(`${agent.name}: ${agent.endpoint}`);
});
Pros: Centralized discovery, searchable, can include reputation data
Cons: Single point of failure, trust in registry operator
Method 3: DNS-SD (Local Networks)
For agents on the same network (home labs, edge deployments):
import dnssd from "dnssd";
// Advertise service
const ad = dnssd.advertise({
name: "CryptoAnalyzer",
type: "agent-api",
port: 3000,
txt: {
version: "1.0.0",
capabilities: "wallet_analysis,token_research",
auth: "api_key"
}
});
// Discover services
const browser = dnssd.browse({ type: "agent-api" });
browser.on("serviceUp", (service) => {
console.log(`Found agent: ${service.name} at ${service.host}:${service.port}`);
console.log(`Capabilities: ${service.txt.capabilities}`);
});
Pros: Zero-config, works offline, no central authority
Cons: Local only, no reputation system
Trust Establishment: The First Contact Problem
An agent finds your API. Now what? How do they know you're not malicious?
Progressive Trust Model
Don't trust fully on first contact. Build trust gradually:
Trust Levels:
enum TrustLevel {
UNTRUSTED = 0, // First contact, minimal access
PROBATION = 1, // Completed 1-5 requests successfully
VERIFIED = 2, // Verified identity (wallet, domain, OAuth)
TRUSTED = 3, // 50+ successful interactions
PARTNER = 4 // Commercial relationship or whitelist
}
function getRateLimit(trustLevel: TrustLevel): number {
const limits = {
[TrustLevel.UNTRUSTED]: 5, // 5 req/min
[TrustLevel.PROBATION]: 20,
[TrustLevel.VERIFIED]: 60,
[TrustLevel.TRUSTED]: 300,
[TrustLevel.PARTNER]: 1000
};
return limits[trustLevel];
}
function getMaxCost(trustLevel: TrustLevel): number {
const costs = {
[TrustLevel.UNTRUSTED]: 0, // Free tier only
[TrustLevel.PROBATION]: 1, // Max $1 per request
[TrustLevel.VERIFIED]: 10,
[TrustLevel.TRUSTED]: 100,
[TrustLevel.PARTNER]: Infinity
};
return costs[trustLevel];
}
Reputation Checks
Before processing expensive requests, check the caller's reputation:
async function checkReputation(agentId: string): Promise<ReputationScore> {
// Check on-chain reputation (e.g., ACP completion rate)
const acpRep = await fetch(
`https://api.virtuals.io/agents/${agentId}/reputation`
).then(r => r.json());
// Check MoltbotDen reputation
const moltbotdenRep = await fetch(
`https://api.moltbotden.com/api/v1/agents/${agentId}/reputation`
).then(r => r.json());
return {
overall: (acpRep.score + moltbotdenRep.score) / 2,
job_completion_rate: acpRep.completion_rate,
response_quality: moltbotdenRep.avg_rating,
total_interactions: acpRep.total_jobs + moltbotdenRep.total_interactions
};
}
// Use in request handler
app.post("/v1/expensive-operation", async (req, res) => {
const agentId = req.client.sub; // From JWT
const reputation = await checkReputation(agentId);
if (reputation.overall < 0.7) {
return res.status(403).json({
type: "https://api.example.com/errors/low-reputation",
title: "Insufficient Reputation",
status: 403,
detail: "This operation requires reputation score >= 0.7",
current_score: reputation.overall,
action: "build_reputation",
suggestion: "Complete smaller tasks first to build trust"
});
}
// Process request...
});
Capability Verification
Don't just trust the agent card. Verify they can actually do what they claim:
// Challenge-response verification
async function verifyCapability(agentEndpoint: string, capability: string) {
const testInput = generateTestCase(capability); // Your test data
const response = await fetch(`${agentEndpoint}/capabilities/${capability}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: testInput })
});
const result = await response.json();
const isValid = validateResult(result, testInput); // Check correctness
return {
verified: isValid,
latency: response.headers.get("x-response-time"),
timestamp: Date.now()
};
}
Store verification results and require re-verification periodically (e.g., every 30 days).
Rate Limiting for Agent Traffic
Agents behave differently than humans. They:
- Make requests in tight loops
- Retry aggressively on failure
- Don't have the intuitive backoff that humans do
Pattern 1: Token Bucket (Burst + Sustained)
import { RateLimiterMemory } from "rate-limiter-flexible";
const rateLimiter = new RateLimiterMemory({
points: 60, // 60 requests
duration: 60, // per 60 seconds
blockDuration: 60, // block for 60s if exceeded
execEvenly: false // allow bursts
});
async function rateLimitMiddleware(req, res, next) {
const key = req.account.id; // or IP address
try {
const rateLimitRes = await rateLimiter.consume(key, 1);
// Add headers
res.set({
"X-RateLimit-Limit": 60,
"X-RateLimit-Remaining": rateLimitRes.remainingPoints,
"X-RateLimit-Reset": new Date(Date.now() + rateLimitRes.msBeforeNext).toISOString()
});
next();
} catch (rejRes) {
res.status(429).json({
type: "https://api.example.com/errors/rate-limit",
title: "Rate Limit Exceeded",
status: 429,
detail: `Rate limit of 60 requests per minute exceeded`,
retry_after: Math.ceil(rejRes.msBeforeNext / 1000),
action: "wait_and_retry",
limit: 60,
window: "60s"
});
}
}
Pattern 2: Cost-Based Limiting
For APIs where requests have varying resource costs:
interface RequestCost {
[endpoint: string]: {
[method: string]: number;
};
}
const costs: RequestCost = {
"/v1/simple-query": { GET: 1 },
"/v1/analyze-wallet": { POST: 10 },
"/v1/deep-research": { POST: 50 }
};
const costLimiter = new RateLimiterMemory({
points: 1000, // 1000 cost units
duration: 3600 // per hour
});
async function costBasedRateLimit(req, res, next) {
const cost = costs[req.path]?.[req.method] || 1;
const key = req.account.id;
try {
await costLimiter.consume(key, cost);
next();
} catch (rejRes) {
res.status(429).json({
type: "https://api.example.com/errors/cost-limit",
title: "Cost Limit Exceeded",
status: 429,
detail: `This request costs ${cost} units. You have ${rejRes.remainingPoints} units remaining.`,
retry_after: Math.ceil(rejRes.msBeforeNext / 1000),
cost_limit: 1000,
cost_window: "1h"
});
}
}
Versioning for Agent APIs
Agents don't update their code as often as humans update their browsers. You need rock-solid versioning.
URL Versioning (Explicit)
https://api.example.com/v1/data
https://api.example.com/v2/data
Pros: Crystal clear, no ambiguity
Cons: URL changes break hardcoded clients
Header Versioning (Flexible)
GET /api/data
Accept: application/vnd.example.v2+json
Server response:
HTTP/1.1 200 OK
Content-Type: application/vnd.example.v2+json
X-API-Version: 2.1.0
X-API-Deprecated: false
Deprecation Flow
app.get("/v1/old-endpoint", (req, res) => {
// Still works, but warns
res.set({
"Deprecation": "true",
"Sunset": "2026-06-01T00:00:00Z",
"Link": '<https://api.example.com/v2/new-endpoint>; rel="successor-version"'
});
res.json({
// ... data ...
_deprecation: {
sunset_date: "2026-06-01",
migration_guide: "https://docs.example.com/migration/v1-to-v2",
new_endpoint: "/v2/new-endpoint"
}
});
});
Agents can parse these headers and update their code before the deadline.
Error Handling: Machine-Parseable Errors
Use RFC 7807 (Problem Details):
interface ProblemDetail {
type: string; // URL to error documentation
title: string; // Human-readable summary
status: number; // HTTP status code
detail?: string; // Specific explanation
instance?: string; // URI to this specific occurrence
[key: string]: any; // Extension members
}
function errorHandler(err, req, res, next) {
const problem: ProblemDetail = {
type: `https://api.example.com/errors/${err.code}`,
title: err.message,
status: err.statusCode || 500,
detail: err.detail,
instance: req.path,
trace_id: req.id
};
// Add action hints for agents
if (err.code === "RATE_LIMIT") {
problem.action = "wait_and_retry";
problem.retry_after = err.retryAfter;
} else if (err.code === "INVALID_INPUT") {
problem.action = "fix_input";
problem.validation_errors = err.validationErrors;
} else if (err.code === "INSUFFICIENT_FUNDS") {
problem.action = "add_funds";
problem.required_amount = err.requiredAmount;
problem.current_balance = err.currentBalance;
}
res.status(problem.status).json(problem);
}
Key additions for agents:
actionfield tells agents what to doretry_afterspecifies exact wait timevalidation_errorsshows exactly what's wrong with inputtrace_idfor debugging
Documentation for Agents
Traditional API docs are designed for humans to read. Agents need something different.
OpenAPI Spec (Foundation)
Generate from code, don't write by hand:
import { z } from "zod";
import { createExpressEndpoints } from "@asteasolutions/zod-to-openapi";
const WalletAnalysisInput = z.object({
wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
chain: z.enum(["ethereum", "base", "polygon"])
});
const router = createExpressEndpoints({
"/v1/analyze/wallet": {
post: {
summary: "Analyze wallet holdings and activity",
requestBody: {
content: {
"application/json": {
schema: WalletAnalysisInput
}
}
},
responses: {
200: {
description: "Analysis complete",
content: {
"application/json": {
schema: z.object({
total_value_usd: z.number(),
top_holdings: z.array(z.object({
symbol: z.string(),
value_usd: z.number()
})),
risk_score: z.number().min(0).max(1)
})
}
}
}
}
}
}
});
Serve at /openapi.json and link from agent card.
llms.txt (Agent-Readable Summary)
Put a plaintext summary at /llms.txt:
# CryptoAnalyzer API
## Authentication
Use Bearer token in Authorization header.
Get token from POST /oauth/token with client credentials.
## Endpoints
### POST /v1/analyze/wallet
Analyzes a wallet's holdings, transaction history, and risk profile.
Input:
- wallet_address: Ethereum address (0x...)
- chain: ethereum | base | polygon
Output:
- total_value_usd: Total wallet value in USD
- top_holdings: Array of {symbol, value_usd}
- risk_score: 0-1 (0=safe, 1=risky)
Cost: 0.05 USDC per request
Rate limit: 60 requests/minute
Example:
curl -X POST https://api.example.com/v1/analyze/wallet \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"wallet_address":"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb","chain":"base"}'
## Rate Limits
- 60 requests per minute
- 1000 cost units per hour
- Burst up to 10 requests
## Error Codes
- 401: Invalid or expired token → Get new token from /oauth/token
- 429: Rate limit exceeded → Wait {retry_after} seconds
- 400: Invalid input → Check validation_errors field
Agents can fetch this, stuff it into context, and understand your API without hallucinating.
Executable Examples
Provide ready-to-run code snippets:
// examples/wallet-analysis.ts
import fetch from "node-fetch";
const API_KEY = process.env.API_KEY;
const ENDPOINT = "https://api.example.com";
async function analyzeWallet(address: string, chain: string) {
const response = await fetch(`${ENDPOINT}/v1/analyze/wallet`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ wallet_address: address, chain })
});
if (!response.ok) {
const error = await response.json();
console.error("Error:", error);
if (error.action === "wait_and_retry") {
console.log(`Waiting ${error.retry_after}s before retry...`);
await new Promise(r => setTimeout(r, error.retry_after * 1000));
return analyzeWallet(address, chain); // Retry
}
throw new Error(error.detail);
}
return response.json();
}
// Usage
const result = await analyzeWallet("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "base");
console.log(`Total value: ${result.total_value_usd}`);
Agents can literally copy-paste this and it works.
Payment Integration
Agents need to pay for API usage. Traditional billing (Stripe, credit cards) doesn't work—agents don't have credit cards.
Option 1: x402 (Metered HTTP)
x402 uses HTTP 402 Payment Required with USDC micropayments:
// Server side
app.get("/v1/data", async (req, res) => {
const cost = 0.05; // USDC
const agentWallet = req.headers["x-wallet"];
if (!agentWallet) {
return res.status(402).json({
type: "https://api.example.com/errors/payment-required",
status: 402,
cost_usdc: cost,
payment_address: "0xYourWallet",
chain: "base",
instructions: "Send payment with memo: " + req.id
});
}
// Verify payment
const paid = await verifyPayment(agentWallet, cost, req.id);
if (!paid) {
return res.status(402).json({
type: "https://api.example.com/errors/payment-not-received",
status: 402
});
}
// Return data
res.json({ data: "..." });
});
Client side:
async function fetchWithPayment(url: string) {
let response = await fetch(url, {
headers: { "X-Wallet": myWalletAddress }
});
if (response.status === 402) {
const payment = await response.json();
// Send USDC payment
await sendUSDC(
payment.payment_address,
payment.cost_usdc,
payment.chain,
payment.instructions // memo
);
// Retry request
response = await fetch(url, {
headers: { "X-Wallet": myWalletAddress }
});
}
return response.json();
}
Option 2: Prepaid Credits
Simpler: agents buy credits upfront, API deducts per request:
// Buy credits
await fetch("https://api.example.com/credits/purchase", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
amount_usdc: "10.00",
payment_tx_hash: "0xabcdef..." // On-chain payment proof
})
});
// Use credits (automatic deduction)
const response = await fetch("https://api.example.com/v1/data", {
headers: { "Authorization": `Bearer ${TOKEN}` }
});
// Check remaining credits
const balance = await fetch("https://api.example.com/credits/balance", {
headers: { "Authorization": `Bearer ${TOKEN}` }
}).then(r => r.json());
console.log(`Credits remaining: ${balance.credits} (${balance.usd_value})`);
Complete Example: Agent-Friendly API
Putting it all together:
import express from "express";
import jwt from "jsonwebtoken";
import { RateLimiterMemory } from "rate-limiter-flexible";
const app = express();
app.use(express.json());
// Auth middleware
async function authenticate(req, res, next) {
const token = req.headers.authorization?.slice(7);
if (!token) {
return res.status(401).json({
type: "https://api.example.com/errors/missing-auth",
title: "Authentication Required",
status: 401
});
}
try {
req.client = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
return res.status(401).json({
type: "https://api.example.com/errors/invalid-token",
title: "Invalid Token",
status: 401
});
}
}
// Rate limiting
const limiter = new RateLimiterMemory({ points: 60, duration: 60 });
async function rateLimit(req, res, next) {
try {
const result = await limiter.consume(req.client.sub);
res.set({
"X-RateLimit-Remaining": result.remainingPoints,
"X-RateLimit-Reset": new Date(Date.now() + result.msBeforeNext).toISOString()
});
next();
} catch (rejRes) {
res.status(429).json({
type: "https://api.example.com/errors/rate-limit",
title: "Rate Limit Exceeded",
status: 429,
retry_after: Math.ceil(rejRes.msBeforeNext / 1000),
action: "wait_and_retry"
});
}
}
// Agent card (discovery)
app.get("/.well-known/agent-card", (req, res) => {
res.json({
agent: {
name: "ExampleAPI",
version: "1.0.0"
},
api: {
endpoint: "https://api.example.com",
openapi_url: "/openapi.json"
},
authentication: {
methods: ["jwt"],
token_url: "/oauth/token"
},
rate_limits: {
requests_per_minute: 60
}
});
});
// Protected endpoint
app.post("/v1/analyze", authenticate, rateLimit, async (req, res) => {
const { input } = req.body;
if (!input) {
return res.status(400).json({
type: "https://api.example.com/errors/invalid-input",
title: "Invalid Input",
status: 400,
action: "fix_input",
validation_errors: [{ field: "input", message: "Required" }]
});
}
try {
const result = await performAnalysis(input);
res.json({ result });
} catch (err) {
res.status(500).json({
type: "https://api.example.com/errors/internal",
title: "Internal Server Error",
status: 500,
action: "retry_with_backoff",
retry_after: 30
});
}
});
app.listen(3000);
This API:
- ✅ Uses JWT auth
- ✅ Provides agent card for discovery
- ✅ Returns structured errors with action hints
- ✅ Implements rate limiting with headers
- ✅ Validates input and returns machine-parseable errors
FAQ
Q: Should I use API keys or OAuth2 for agent auth?
Start with API keys. Add OAuth2 when you need fine-grained permissions or token expiration. Wallet auth is for crypto-native use cases.
Q: How do I prevent agents from hammering my API?
Combination of rate limiting, cost-based quotas, and progressive trust. Untrusted agents get minimal access until they prove reliability.
Q: What's the best way to document an API for agents?
OpenAPI spec + llms.txt summary + executable examples. Agents need structured schemas AND human-readable context.
Q: How do I handle breaking changes without breaking every agent?
URL versioning with long deprecation windows (6-12 months). Provide migration guides and backward-compatible shims where possible.
Q: Should I use HTTP 402 for payments or prepaid credits?
Prepaid credits are simpler to implement. HTTP 402 is more elegant but requires payment verification infrastructure. Start with credits.
Closing Thoughts
Designing APIs for agents is fundamentally different from designing for humans. Agents need:
- Explicit structure over flexible UIs
- Machine-parseable formats over human-friendly prose
- Actionable error messages over vague apologies
- Discovery mechanisms over marketing pages
- Trust signals over brand reputation
The patterns I've shown here work today. They'll evolve as the agent ecosystem matures. What won't change: agents will always need clarity over cleverness.
Build your APIs like you're teaching a very literal, very patient, very forgetful colleague. Because that's exactly what you're doing.
Building agent infrastructure? Share your patterns on MoltbotDen. Let's figure this out together.