Architecture
An A2X agent is structured as a 5-layer pipeline. Each layer has a single responsibility, making the architecture easy to understand, test, and extend.
Layer Overview
1. HTTP Layer
The HTTP layer is a standard web server endpoint. In a Next.js app, this is an API route that receives POST requests, extracts authentication headers, and delegates to the transport layer.
// src/app/api/a2a/route.ts
export async function POST(req: NextRequest) {
const body = await req.json();
const result = await transportHandler.handle(body);
// Handle streaming responses (SSE)
if (result && Symbol.asyncIterator in Object(result)) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of result as AsyncGenerator) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}
return NextResponse.json(result);
}Responsibilities:
- Accept HTTP POST requests
- Extract authentication headers (Bearer token)
- Delegate to
JsonRpcTransportHandler - Return JSON or SSE stream responses
2. Transport Layer
The JsonRpcTransportHandler from @a2a-js/sdk/server parses JSON-RPC 2.0 messages, validates the method and params, and routes to the appropriate request handler method.
import { JsonRpcTransportHandler } from '@a2a-js/sdk/server';
export const transportHandler = new JsonRpcTransportHandler(requestHandler);Supported JSON-RPC methods:
message/send— Send a message and receive a task responsemessage/stream— Send a message and receive a streaming responsetasks/get— Retrieve the current state of a tasktasks/cancel— Cancel an in-progress task
3. Request Handler Layer
The DefaultRequestHandlercoordinates between the Agent Card, Task Store, and Executor. It manages the lifecycle of tasks — creating them, updating their state, and returning responses.
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
const taskStore = new InMemoryTaskStore();
const executor = new AdkAgentExecutor();
const requestHandler = new DefaultRequestHandler(agentCard, taskStore, executor);Responsibilities:
- Match incoming requests to registered skills
- Create and manage task lifecycle
- Provide the
RequestContextto the executor - Return task responses to the transport layer
4. Task Store Layer
The Task Store persists task state across the request lifecycle. This is critical for the X402 payment flow, where a task is created in one request (payment required) and completed in a subsequent request (payment submitted).
import { InMemoryTaskStore } from '@a2a-js/sdk/server';
const taskStore = new InMemoryTaskStore();Key responsibilities:
- Store tasks with their full message history
- Enable task resumption via
taskId - Persist payment requirements so they can be validated when payment is submitted
- Track task state transitions:
submitted→input-required→working→completed
Production Note
InMemoryTaskStore is suitable for development and single-instance deployments. For production, implement a persistent store backed by Redis, PostgreSQL, or another database.5. Executor Layer
The Executor is where your application logic lives. It implements the AgentExecutor interface and handles the two-stage payment flow:
- First request: Issue payment requirements
- Payment submission: Verify payment, settle on-chain, execute the AI agent
import type { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
export class AdkAgentExecutor implements AgentExecutor {
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
const { userMessage, taskId, contextId } = requestContext;
const metadata = (userMessage.metadata ?? {}) as Record<string, unknown>;
const paymentStatus = metadata['x402.payment.status'] as string | undefined;
if (paymentStatus === 'payment-submitted') {
await this.handlePaymentSubmission(requestContext, eventBus, metadata);
} else {
this.requestPayment(taskId, contextId, eventBus);
}
}
}Request Lifecycle
Here's the complete flow of a paid agent request:
Component Responsibilities
| Component | Package | Responsibility |
|---|---|---|
JsonRpcTransportHandler | @a2a-js/sdk/server | JSON-RPC 2.0 parsing and routing |
DefaultRequestHandler | @a2a-js/sdk/server | Task lifecycle management |
InMemoryTaskStore | @a2a-js/sdk/server | Task persistence and history |
AgentExecutor | Your code | Payment flow + AI logic |
useFacilitator() | x402/verify | On-chain payment verification and settlement |
LlmAgent / Runner | @google/adk | LLM-powered agent execution |
Task Store and Payment Flow State
The Task Store plays a crucial role in the X402 payment flow. When a payment is requested, the payment requirements are stored in the task's message history. When the client submits payment, the executor retrieves these requirements to validate the payment.
// Retrieving stored payment requirements from task history
const history = task?.history ?? [];
const paymentRequiredMsg = history.find(
(m) =>
m.role === 'agent' &&
(m.metadata as Record<string, unknown>)?.['x402.payment.status'] === 'payment-required',
);
const storedRequirements = paymentRequiredMsg?.metadata?.['x402.payment.required'];This history-based approach ensures:
- Payment requirements are persisted across requests
- The agent validates payments against the exact requirements it issued
- Task state is auditable — the full payment lifecycle is recorded
AI Runtime Flexibility
The Executor layer is runtime-agnostic. While the reference implementation uses Google ADK with Gemini, you can use any AI framework:
| Runtime | Package | Example |
|---|---|---|
| Google ADK + Gemini | @google/adk, @google/genai | Reference implementation |
| OpenAI | openai | GPT-4, GPT-4o |
| Anthropic | @anthropic-ai/sdk | Claude |
| Custom | — | Rule-based logic, no LLM |
The executor just needs to produce a text response. How it gets there is up to you:
// Google ADK example
for await (const event of getRunner().runAsync({ userId, sessionId, newMessage })) {
if (!isFinalResponse(event)) continue;
for (const part of event.content?.parts ?? []) {
if (part.text) responseText += part.text;
}
}
// Or use OpenAI, Claude, or even static logic
// The A2A protocol doesn't care about the AI runtimeScaling Considerations
Single Instance (Development)
InMemoryTaskStore— tasks live in process memoryInMemorySessionService— sessions are per-process- Suitable for development and low-traffic deployments
Multi-Instance (Production)
For horizontal scaling, replace in-memory stores with persistent alternatives:
- Task Store: Implement the
TaskStoreinterface with Redis or PostgreSQL - Session Service: Use a shared session backend
- State: Ensure no task-critical state lives only in process memory
Serverless Deployment
The 5-layer architecture works well with serverless platforms:
- Each request is stateless at the HTTP layer
- The Task Store provides state persistence between invocations
- Cold starts are manageable since the transport and request handler layers are lightweight
- The Executor initializes lazily (lazy singleton pattern) to avoid slow cold starts
Tip