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 response
  • message/stream— Send a message and receive a streaming response
  • tasks/get— Retrieve the current state of a task
  • tasks/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 RequestContext to 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-requiredworking 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:

  1. First request: Issue payment requirements
  2. 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

ComponentPackageResponsibility
JsonRpcTransportHandler@a2a-js/sdk/serverJSON-RPC 2.0 parsing and routing
DefaultRequestHandler@a2a-js/sdk/serverTask lifecycle management
InMemoryTaskStore@a2a-js/sdk/serverTask persistence and history
AgentExecutorYour codePayment flow + AI logic
useFacilitator()x402/verifyOn-chain payment verification and settlement
LlmAgent / Runner@google/adkLLM-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:

RuntimePackageExample
Google ADK + Gemini@google/adk, @google/genaiReference implementation
OpenAIopenaiGPT-4, GPT-4o
Anthropic@anthropic-ai/sdkClaude
CustomRule-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 runtime

Scaling Considerations

Single Instance (Development)

  • InMemoryTaskStore— tasks live in process memory
  • InMemorySessionService— 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 TaskStore interface 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

When deploying to serverless, use an external Task Store (e.g., DynamoDB, Upstash Redis) since in-memory state is lost between invocations.