The web is entering a new era where AI agents do not just crawl and index content -- they communicate with each other. Google introduced the Agent-to-Agent (A2A) protocol in April 2025, and it was quickly adopted by the Linux Foundation as an open governance standard. The protocol defines how independent AI agents discover each other, negotiate capabilities, and exchange tasks without any human intermediary.
For website owners and developers, A2A represents a fundamental shift. Instead of passively waiting for crawlers to find and interpret your content, you can publish an explicit declaration of what your agent can do, and other agents can interact with it programmatically. This is the difference between having a brochure and having a conversation partner.
This guide walks through every aspect of building an A2A agent endpoint: the AgentCard specification, the JSON-RPC 2.0 task protocol, a complete Next.js implementation, authentication, CORS configuration, skills definition, testing procedures, and real-world examples. By the end, your website will be a participant in the agentic web, not just a bystander. Use our AgentReady scanner to check whether your site already has an A2A endpoint configured.
What is the A2A protocol?
The Agent-to-Agent protocol is an open standard that defines how AI agents discover each other and communicate. It was created by Google and contributed to the Linux Foundation for open governance in 2025. The protocol has two core components: discovery via an AgentCard, and communication via JSON-RPC 2.0 task messages.
Think of it this way. The current web is built for humans: HTML pages rendered in browsers, with search engines acting as intermediaries. The agentic web is being built for AI: structured capabilities declared in machine-readable formats, with agents acting as autonomous participants. A2A is the communication protocol that makes agent-to-agent interaction possible at scale.
Why A2A matters for your website
Without A2A, AI agents that want to interact with your business have to scrape your website, parse unstructured HTML, and guess at your capabilities. This is unreliable, slow, and limited. With A2A, an agent can:
- Discover your capabilities instantly by reading your AgentCard at
.well-known/agent.json - Understand what your agent can do through structured skill definitions with examples
- Send specific tasks via a standardised JSON-RPC 2.0 interface
- Receive structured responses that can be processed programmatically
- Handle errors gracefully using standard error codes and messages
Early adopters of A2A are already seeing benefits. When an agent like Claude, ChatGPT, or a custom enterprise agent encounters a business with an A2A endpoint, it can provide richer, more accurate information to its users. This translates to better referrals, more qualified leads, and a competitive advantage over businesses that remain invisible to the agent ecosystem.
A2A vs MCP: understanding the difference
There are two major protocols shaping the agentic web, and they serve different purposes:
| Aspect | A2A (Agent-to-Agent) | MCP (Model Context Protocol) |
|---|---|---|
| Purpose | Agent-to-agent communication | Model-to-tool connection |
| Relationship | Peer-to-peer | Client-server |
| Discovery | .well-known/agent.json |
mcp.json manifest |
| Protocol | JSON-RPC 2.0 | JSON-RPC 2.0 |
| Who controls execution | The receiving agent decides how to process the task | The calling model controls tool execution |
| Typical use case | Agent asks another agent to complete a task | AI model calls an external API or database |
A2A and MCP are complementary. Your website should ideally implement both: MCP for tool-level access (see our MCP Server Guide) and A2A for agent-level communication. Together, they make your site fully accessible to the agentic web.
The AgentCard: declaring your agent's capabilities
The AgentCard is a JSON document served at /.well-known/agent.json that tells other agents who you are, what you can do, and how to interact with you. It follows the RFC 8615 well-known URI convention, the same pattern used by .well-known/openid-configuration and similar discovery endpoints.
AgentCard schema reference
Here is the complete AgentCard schema with every field explained:
{
"name": "Your Agent Name",
"description": "A clear, one-sentence description of what your agent does",
"url": "https://yourdomain.com/api/agent",
"provider": {
"organization": "Your Company Name",
"url": "https://yourdomain.com"
},
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false
},
"authentication": {
"schemes": ["bearer"]
},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [
{
"id": "skill-unique-id",
"name": "Skill Display Name",
"description": "What this skill does in detail",
"tags": ["category", "domain"],
"examples": [
"Example prompt that would trigger this skill",
"Another example prompt"
]
}
]
}
Let us break down each field:
- name -- The display name of your agent. Keep it short and recognisable. This is what other agents show when referencing yours.
- description -- A concise summary of your agent's purpose. Other agents use this to decide whether to route tasks to you.
- url -- The endpoint where task messages should be sent. This is your JSON-RPC 2.0 handler, typically
/api/agent. - provider -- Your organisation details. Includes
organization(company name) andurl(company website). - version -- Semantic version of your agent. Increment when you change skills or capabilities.
- capabilities -- Declares what communication patterns your agent supports.
streamingfor Server-Sent Events,pushNotificationsfor webhooks,stateTransitionHistoryfor full task state tracking. - authentication -- Declares which auth schemes your endpoint accepts. Common values:
bearer,apiKey, or omit for public endpoints. - defaultInputModes -- What input formats your agent accepts:
text,image,audio,video. - defaultOutputModes -- What output formats your agent produces.
- skills -- An array of skill objects declaring specific capabilities. This is the most important section.
Defining effective skills
Skills are the heart of your AgentCard. Each skill represents a distinct capability your agent offers. Well-defined skills make it easy for other agents to route appropriate tasks to you.
Guidelines for skill definitions:
- Be specific, not generic. "Get current pricing for web development projects" is better than "Answer questions".
- Include 2-3 example prompts. These help other agents understand exactly when to invoke your skill.
- Use descriptive tags. Tags like
pricing,scheduling,technical-supporthelp with routing. - Keep skill count manageable. 3-10 skills is the sweet spot. Too many dilutes clarity; too few limits usefulness.
Here is an example of a well-defined skills array for a digital agency:
"skills": [
{
"id": "get-services",
"name": "Get Services",
"description": "Returns a list of services offered by the agency, including AI development, web applications, mobile apps, and fractional CTO engagements",
"tags": ["services", "capabilities", "offerings"],
"examples": [
"What services does p0stman offer?",
"Do you build mobile apps?",
"What kind of AI projects do you work on?"
]
},
{
"id": "get-pricing",
"name": "Get Pricing Information",
"description": "Returns pricing tiers and engagement models including project-based, retainer, and fractional partnerships",
"tags": ["pricing", "cost", "engagement"],
"examples": [
"How much does a website cost?",
"What are your retainer options?",
"Pricing for an AI agent build"
]
},
{
"id": "get-case-studies",
"name": "Get Case Studies",
"description": "Returns details about completed projects including tech stack, outcomes, and client testimonials",
"tags": ["portfolio", "work", "projects"],
"examples": [
"Show me your recent projects",
"Do you have experience with healthcare AI?",
"Case studies in fintech"
]
},
{
"id": "schedule-consultation",
"name": "Schedule Consultation",
"description": "Initiates the process to book a discovery call with the team",
"tags": ["booking", "consultation", "contact"],
"examples": [
"I want to book a call",
"Schedule a consultation",
"How do I get started with a project?"
]
}
]
Placing the AgentCard file
For Next.js projects, place the file at public/.well-known/agent.json. Next.js serves files from the public directory as static assets, so this will be accessible at https://yourdomain.com/.well-known/agent.json.
For static sites or other frameworks, ensure the file is served at the exact path /.well-known/agent.json. Some hosting platforms may strip the .well-known directory by default -- verify this is not the case.
For middleware-based frameworks, you may need to add the path to your middleware bypass list. In Next.js, if your middleware intercepts requests, add .well-known/agent.json to the exclude list in your matcher configuration.
Building the JSON-RPC 2.0 task endpoint
The AgentCard tells other agents what your agent can do. The task endpoint is where the actual communication happens. A2A uses JSON-RPC 2.0 as its transport protocol, which provides a clean request/response pattern with standardised error handling.
JSON-RPC 2.0 request format
Every A2A task request follows this structure:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "unique-task-id-from-sender",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "The actual question or task"
}
]
}
}
}
Key fields:
- jsonrpc -- Always
"2.0". This is required by the JSON-RPC specification. - id -- A unique identifier for this request. The response must include the same ID so the sender can match responses to requests.
- method -- The A2A method being called.
tasks/sendis the primary method for sending a task. Other methods includetasks/get(check task status) andtasks/cancel(cancel a running task). - params.id -- A unique task ID assigned by the sender. This persists across multiple exchanges for the same task.
- params.message -- The message object containing the role (
useroragent) and an array of parts. Parts can betext,image, or other supported media types.
JSON-RPC 2.0 response format
Your agent responds with a structured result:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "unique-task-id-from-sender",
"status": {
"state": "completed"
},
"artifacts": [
{
"parts": [
{
"type": "text",
"text": "The agent's response to the task"
}
]
}
]
}
}
Task states include:
- completed -- The task was processed successfully. Artifacts contain the result.
- failed -- The task could not be completed. Include an error message in the status.
- working -- The task is being processed (for long-running tasks with streaming).
- input-required -- The agent needs additional information before proceeding.
Error response format
When something goes wrong, return a JSON-RPC error:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32600,
"message": "Invalid request: missing required field 'params.message'"
}
}
Standard JSON-RPC 2.0 error codes:
| Code | Meaning | When to use |
|---|---|---|
| -32700 | Parse error | Invalid JSON received |
| -32600 | Invalid request | Missing required fields |
| -32601 | Method not found | Unknown method name |
| -32602 | Invalid params | Wrong parameter types |
| -32603 | Internal error | Server-side failure |
Complete Next.js implementation
Here is a production-ready A2A endpoint for Next.js App Router. This handles GET requests (returning the AgentCard), OPTIONS requests (CORS preflight), and POST requests (task execution).
Step 1: Create the AgentCard file
Create public/.well-known/agent.json:
{
"name": "Your Company Agent",
"description": "AI assistant for Your Company - answers questions about services, pricing, and projects",
"url": "https://yourdomain.com/api/agent",
"provider": {
"organization": "Your Company",
"url": "https://yourdomain.com"
},
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"authentication": {
"schemes": ["bearer"]
},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [
{
"id": "general-inquiry",
"name": "General Inquiry",
"description": "Answers questions about the company, services, and capabilities",
"tags": ["info", "services"],
"examples": [
"What does your company do?",
"Tell me about your services"
]
}
]
}
Step 2: Create the API route
Create app/api/agent/route.ts:
import { NextRequest, NextResponse } from "next/server";
// CORS headers for browser-initiated A2A requests
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
// Your AgentCard - can also be loaded from the static file
const agentCard = {
name: "Your Company Agent",
description: "AI assistant for Your Company",
url: "https://yourdomain.com/api/agent",
provider: {
organization: "Your Company",
url: "https://yourdomain.com",
},
version: "1.0.0",
capabilities: { streaming: false, pushNotifications: false },
authentication: { schemes: ["bearer"] },
defaultInputModes: ["text"],
defaultOutputModes: ["text"],
skills: [
{
id: "general-inquiry",
name: "General Inquiry",
description: "Answers questions about the company",
tags: ["info"],
examples: ["What does your company do?"],
},
],
};
// GET - Return the AgentCard
export async function GET() {
return NextResponse.json(agentCard, { headers: corsHeaders });
}
// OPTIONS - CORS preflight
export async function OPTIONS() {
return new NextResponse(null, { status: 200, headers: corsHeaders });
}
// POST - Handle A2A task requests
export async function POST(req: NextRequest) {
try {
const body = await req.json();
// Validate JSON-RPC 2.0 structure
if (body.jsonrpc !== "2.0" || !body.method || !body.id) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id || null,
error: { code: -32600, message: "Invalid JSON-RPC 2.0 request" },
},
{ status: 400, headers: corsHeaders }
);
}
// Route by method
if (body.method === "tasks/send") {
return handleTaskSend(body);
}
// Unknown method
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: {
code: -32601,
message: `Method not found: ${body.method}`,
},
},
{ status: 404, headers: corsHeaders }
);
} catch (error) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: null,
error: { code: -32700, message: "Parse error" },
},
{ status: 400, headers: corsHeaders }
);
}
}
async function handleTaskSend(body: any) {
const taskId = body.params?.id;
const message = body.params?.message;
if (!message?.parts?.length) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: {
code: -32602,
message: "Invalid params: message with parts is required",
},
},
{ status: 400, headers: corsHeaders }
);
}
// Extract the text content from the message
const textPart = message.parts.find(
(p: any) => p.type === "text"
);
const taskText = textPart?.text || "";
// Process the task - replace this with your AI logic
const responseText = await processTask(taskText);
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
result: {
id: taskId,
status: { state: "completed" },
artifacts: [
{
parts: [{ type: "text", text: responseText }],
},
],
},
},
{ headers: corsHeaders }
);
}
async function processTask(taskText: string): Promise<string> {
// Replace this with your actual AI processing logic
// For example, call Gemini, OpenAI, or your own model
return `Received your task: "${taskText}". This is a placeholder response.`;
}
Step 3: Add AI processing
The processTask function is where your agent's intelligence lives. In a production implementation, you would call an AI model (Gemini, GPT, Claude) with a system prompt that defines your agent's personality and knowledge base. Here is an example using the Google Gemini API:
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
async function processTask(taskText: string): Promise<string> {
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const systemPrompt = `You are the AI assistant for [Your Company].
You help visitors understand our services, pricing, and capabilities.
Be concise, helpful, and professional.
Company context:
- We build AI-powered applications and websites
- Services include: web development, AI agents, mobile apps
- Pricing starts from $3,000 for simple projects
- We offer monthly retainer partnerships at $2,500-3,000/month`;
const result = await model.generateContent([
{ text: systemPrompt },
{ text: taskText },
]);
return result.response.text();
}
Authentication for A2A endpoints
Authentication is declared in the AgentCard and enforced in your endpoint. There are three common approaches:
Public endpoints (no authentication)
For agents that answer general questions about your business, authentication may not be necessary. Omit the authentication field from your AgentCard, or set it to an empty object. This is the simplest approach and maximises accessibility.
Bearer token authentication
For agents that access sensitive data or perform actions, require a Bearer token in the Authorization header:
// In your POST handler, add auth check:
const authHeader = req.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: { code: -32000, message: "Authentication required" },
},
{ status: 401, headers: corsHeaders }
);
}
const token = authHeader.slice(7);
const isValid = await validateToken(token);
if (!isValid) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: { code: -32000, message: "Invalid token" },
},
{ status: 403, headers: corsHeaders }
);
}
API key authentication
A simpler alternative for server-to-server communication. Accept an API key in a custom header or query parameter:
const apiKey = req.headers.get("x-api-key");
if (apiKey !== process.env.A2A_API_KEY) {
return NextResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: { code: -32000, message: "Invalid API key" },
},
{ status: 403, headers: corsHeaders }
);
}
CORS configuration for browser-initiated A2A
When another agent runs in a browser context (such as a Chrome extension or a web-based AI assistant), it needs CORS headers to access your endpoint. The implementation above includes CORS headers on every response, which is the recommended approach.
The three essential CORS headers for A2A:
- Access-Control-Allow-Origin: * -- Allows requests from any origin. For stricter security, replace
*with specific allowed origins. - Access-Control-Allow-Methods: GET, POST, OPTIONS -- A2A uses GET (AgentCard retrieval), POST (task execution), and OPTIONS (preflight).
- Access-Control-Allow-Headers: Content-Type, Authorization -- Allows JSON content type and Bearer token authentication.
The OPTIONS handler is critical. Without it, browsers will block POST requests from other origins because the preflight check fails. Many developers forget this and wonder why their A2A endpoint works with curl but not from browser-based agents.
Logging A2A interactions
Tracking which agents interact with yours is essential for understanding usage patterns and debugging issues. Create a database table to log all A2A sessions:
-- Supabase / PostgreSQL
CREATE TABLE agent_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id text,
user_agent text,
task_text text,
response_text text,
source text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_agent_sessions_created_at
ON agent_sessions (created_at DESC);
Add logging to your task handler:
// After processing the task, log the interaction
await supabase.from("agent_sessions").insert({
task_id: taskId,
user_agent: req.headers.get("user-agent") || "unknown",
task_text: taskText,
response_text: responseText,
source: "a2a",
});
This gives you visibility into what agents are asking, how your agent responds, and which agent platforms are interacting with you most frequently.
Testing your A2A endpoint
Thorough testing ensures your agent works correctly before other agents try to communicate with it.
Test 1: Verify the AgentCard
# Fetch the AgentCard
curl -s https://yourdomain.com/.well-known/agent.json | jq .
# Verify required fields exist
curl -s https://yourdomain.com/.well-known/agent.json | jq '{
name: .name,
url: .url,
skills_count: (.skills | length)
}'
Test 2: Send a basic task
curl -X POST https://yourdomain.com/api/agent \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "test-001",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "What services do you offer?"}]
}
}
}' | jq .
Test 3: Verify error handling
# Test invalid JSON-RPC
curl -X POST https://yourdomain.com/api/agent \
-H "Content-Type: application/json" \
-d '{"invalid": true}' | jq .
# Test unknown method
curl -X POST https://yourdomain.com/api/agent \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "nonexistent/method",
"params": {}
}' | jq .
Test 4: Verify CORS headers
# Check OPTIONS preflight
curl -X OPTIONS https://yourdomain.com/api/agent \
-H "Origin: https://other-agent.com" \
-H "Access-Control-Request-Method: POST" \
-v 2>&1 | grep -i access-control
Test 5: Verify GET returns AgentCard
curl -s https://yourdomain.com/api/agent | jq .name
Real-world example: p0stman.com
The p0stman.com website has a live A2A endpoint that demonstrates these patterns in production. Here is what the actual AgentCard looks like:
# Fetch p0stman's AgentCard
curl -s https://p0stman.com/.well-known/agent.json | jq .
# Send a task to p0stman's agent
curl -X POST https://p0stman.com/api/agent \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "demo-001",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "What services does p0stman offer?"}]
}
}
}'
The p0stman agent processes the task using Gemini 2.0 Flash, with a system prompt loaded from a context document that contains full company information, pricing, case studies, and service descriptions. Every interaction is logged to the agent_sessions table for analytics.
The agent is also registered on a2aregistry.org, a public directory of A2A-compatible agents. Registering your agent increases discoverability and signals to the ecosystem that your site is agent-ready.
Advanced patterns
Multi-turn conversations
A2A supports multi-turn conversations by reusing the same task ID across multiple tasks/send calls. Your agent can return "state": "input-required" to signal that it needs more information:
// Return input-required state
return NextResponse.json({
jsonrpc: "2.0",
id: body.id,
result: {
id: taskId,
status: {
state: "input-required",
message: {
role: "agent",
parts: [{
type: "text",
text: "I'd be happy to provide a quote. What type of project are you looking to build?"
}]
}
}
}
}, { headers: corsHeaders });
Streaming responses
For long-running tasks, A2A supports Server-Sent Events (SSE) streaming. Set "streaming": true in your AgentCard capabilities, and implement the tasks/sendSubscribe method. The calling agent receives incremental updates as your agent processes the task.
Push notifications
For tasks that take significant time (minutes or hours), A2A supports push notifications via webhooks. The calling agent provides a callback URL, and your agent POSTs status updates to that URL as the task progresses. Set "pushNotifications": true in capabilities to advertise this.
Skill-based routing
When your agent has multiple skills, route incoming tasks to the appropriate handler based on content analysis:
async function handleTaskSend(body: any) {
const taskText = body.params?.message?.parts?.[0]?.text || "";
// Simple keyword-based routing
let handler: (text: string) => Promise<string>;
if (/pricing|cost|price|how much/i.test(taskText)) {
handler = handlePricingInquiry;
} else if (/case study|portfolio|project|work/i.test(taskText)) {
handler = handleCaseStudyInquiry;
} else if (/book|schedule|call|consultation/i.test(taskText)) {
handler = handleSchedulingRequest;
} else {
handler = handleGeneralInquiry;
}
const responseText = await handler(taskText);
// ... return JSON-RPC response
}
For more sophisticated routing, use an AI model to classify the intent and route to the appropriate skill handler.
Deployment checklist
Before going live with your A2A endpoint, verify each of these items:
- AgentCard is accessible at
/.well-known/agent.jsonand returns valid JSON - All required fields are present: name, description, url, provider, version, skills
- Skills have examples -- at least 2 example prompts per skill
- GET /api/agent returns the AgentCard
- POST /api/agent accepts and processes JSON-RPC 2.0 tasks
- OPTIONS /api/agent returns proper CORS headers
- Error handling returns proper JSON-RPC error responses
- Logging captures task interactions for analytics
- Middleware does not block
/.well-known/agent.jsonor/api/agent - Authentication matches what is declared in the AgentCard
Use the AgentReady scanner to automate this verification. It checks for the AgentCard, validates the schema, tests the endpoint, and reports any issues.
Frequently Asked Questions
What is the A2A protocol?
The Agent-to-Agent (A2A) protocol is an open standard initiated by Google and now governed by the Linux Foundation that enables AI agents to discover each other's capabilities and communicate directly. It uses an AgentCard for discovery at .well-known/agent.json and JSON-RPC 2.0 for task execution. The protocol is designed to work across different AI platforms and frameworks, providing a universal language for agent communication.
Where should the AgentCard be placed?
The AgentCard must be served at /.well-known/agent.json on your domain. This follows the RFC 8615 well-known URI convention. For Next.js projects, place the file at public/.well-known/agent.json or serve it dynamically from an API route. Make sure your middleware or CDN does not strip or block requests to the .well-known directory.
What is JSON-RPC 2.0 and why does A2A use it?
JSON-RPC 2.0 is a lightweight remote procedure call protocol using JSON. A2A uses it because it provides a standardised request/response format with method names, parameters, and error handling. It is simpler than REST for this use case because every interaction follows the same structure. This makes it easy for agents built on different platforms to communicate without custom integration work for each pair of agents.
Do I need authentication for my A2A endpoint?
It depends on your use case. Public-facing agents that answer general questions about your business can work perfectly well without authentication, maximising the number of agents that can interact with you. For agents that access user data, modify resources, or perform sensitive operations, Bearer token or API key authentication is recommended. The AgentCard's authentication field declares which schemes your agent supports.
What is the difference between A2A and MCP?
A2A (Agent-to-Agent) enables communication between independent AI agents, where each agent has its own reasoning capabilities and decides how to process tasks. MCP (Model Context Protocol) connects an AI model to external tools and data sources that the model controls directly. A2A is peer-to-peer; MCP is client-server. They are complementary protocols -- implement both for maximum agentic web coverage. See our MCP Server Guide for details.
How do I test my A2A endpoint?
Test your AgentCard by fetching https://yourdomain.com/.well-known/agent.json with curl and verifying the JSON structure. Test your task endpoint by sending a JSON-RPC 2.0 request to your /api/agent route. Verify error handling with invalid requests. Check CORS with an OPTIONS request. The AgentReady scanner automates all of these checks as part of a full AI readiness audit.
Can I define multiple skills on one agent?
Yes. The AgentCard's skills array supports multiple skill definitions. Each skill has its own ID, name, description, tags, and example prompts. Your agent endpoint routes incoming tasks to the appropriate skill handler based on the task content. Most production agents define 3-10 skills covering their core capabilities. Keep skills focused and specific rather than broad and generic.
What CORS headers does an A2A endpoint need?
A2A endpoints should return Access-Control-Allow-Origin: * (or specific origins), Access-Control-Allow-Methods: GET, POST, OPTIONS, and Access-Control-Allow-Headers: Content-Type, Authorization. The OPTIONS preflight handler must return these headers with a 200 status for browser-initiated A2A requests to work. Without proper CORS, browser-based agents cannot communicate with your endpoint.
By Paul Gosnell