A2X

Archived · v0.7.0 · Advanced

x402 Payments

Charge per call with on-chain cryptocurrency payments. A2X implements the a2a-x402 v0.2 extension, which layers the x402 payment protocol on top of A2A tasks.

The flow: the merchant agent responds to an unpaid request with input-required + x402.payment.required. The client signs a PaymentPayload with its wallet and resubmits the same task. The merchant verifies + settles the payment via an x402 facilitator, runs the agent, and attaches the settlement receipt to the completed task.

Installation

pnpm add @a2x/sdk x402 viem

x402 and viem are optional peer dependencies — only install them if you actually enable x402 on your agent or client.

Server

import { A2XAgent, AgentExecutor, StreamingMode, InMemoryTaskStore } from '@a2x/sdk';
import { X402PaymentExecutor, X402_EXTENSION_URI } from '@a2x/sdk/x402';
 
const inner = new AgentExecutor({
  runner,
  runConfig: { streamingMode: StreamingMode.SSE },
});
 
const executor = new X402PaymentExecutor(inner, {
  accepts: [{
    network: 'base-sepolia',
    amount: '10000',                                   // 0.01 USDC (6 decimals)
    asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // USDC on Base Sepolia
    payTo: process.env.MERCHANT_ADDRESS!,
    description: 'Premium agent access',
  }],
  // Facilitator defaults to https://x402.org/facilitator.
  // Override if you run your own: facilitator: { url: 'https://…' }
});
 
const agent = new A2XAgent({ taskStore: new InMemoryTaskStore(), executor })
  .setName('Paid Agent')
  .setDescription('Charges per call')
  .setCapabilities({
    extensions: [{ uri: X402_EXTENSION_URI, required: true }],
  });

Multiple payment options

Put more than one entry in accepts[] and the client picks one. Use this for multi-network support:

accepts: [
  { network: 'base-sepolia', amount: '10000', asset: USDC_BASE_SEPOLIA, payTo, description: 'Testnet' },
  { network: 'base',         amount: '10000', asset: USDC_BASE,         payTo, description: 'Mainnet' },
],

Conditional pricing

requiresPayment is a predicate over the incoming message. Return false to pass the message straight through to the inner executor without charging:

new X402PaymentExecutor(inner, {
  accepts: [...],
  requiresPayment: (message) => {
    // Free tier: short health-check messages
    const text = message.parts.find((p) => 'text' in p)?.text ?? '';
    return text.length > 10;
  },
});

Custom facilitator

For self-hosted facilitators or tests, pass a { verify, settle } pair instead of a URL:

new X402PaymentExecutor(inner, {
  accepts: [...],
  facilitator: {
    async verify(payload, requirements) { /* … */ return { isValid: true, payer: '0x…' }; },
    async settle(payload, requirements) { /* … */ return { success: true, transaction: '0x…', network: 'base-sepolia', payer: '0x…' }; },
  },
});

What gets emitted

On an unpaid request, the task transitions to input-required and the status message carries:

{
  "x402.payment.status": "payment-required",
  "x402.payment.required": {
    "x402Version": 1,
    "accepts": [ /* PaymentRequirements[] */ ]
  }
}

On a successful payment, the completed task's status message carries:

{
  "x402.payment.status": "payment-completed",
  "x402.payment.receipts": [
    { "success": true, "transaction": "0x…", "network": "base-sepolia" }
  ]
}

Failures surface under x402.payment.error with one of INVALID_PAYLOAD, NETWORK_MISMATCH, INVALID_PAY_TO, AMOUNT_EXCEEDED, VERIFY_FAILED, SETTLE_FAILED.

Client

High-level: X402Client

import { A2XClient } from '@a2x/sdk/client';
import { X402Client } from '@a2x/sdk/x402';
import { privateKeyToAccount } from 'viem/accounts';
 
const x402 = new X402Client(new A2XClient('https://agent.example.com'), {
  signer: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
  onPaymentRequired: (required) => {
    console.log('Merchant asks for', required.accepts);
  },
});
 
const task = await x402.sendMessage({
  message: {
    messageId: crypto.randomUUID(),
    role: 'user',
    parts: [{ text: 'hello' }],
  },
});
 
console.log(task.status.state); // "completed"

If the merchant rejects the payment (verify or settle failed), sendMessage throws X402PaymentFailedError with the on-chain reason attached.

Low-level: signX402Payment

Use this when you need to inspect the payment-required task before signing — e.g. to show the user a confirmation modal, to fetch the signer's balance, or to route across multiple wallets.

import {
  signX402Payment,
  getX402PaymentRequirements,
} from '@a2x/sdk/x402';
 
const first = await client.sendMessage({ message: { … } });
const required = getX402PaymentRequirements(first);
if (!required) {
  return first; // merchant didn't charge
}
 
// Show the cost to the user, wait for confirmation, etc.
const signed = await signX402Payment(first, { signer });
 
const final = await client.sendMessage({
  message: {
    messageId: crypto.randomUUID(),
    role: 'user',
    taskId: first.id,
    parts: [{ text: 'hello' }],
    metadata: signed.metadata,
  },
});

Picking a specific requirement when the merchant offers several:

const signed = await signX402Payment(first, {
  signer,
  selectRequirement: (accepts) =>
    accepts.find((r) => r.network === 'base-sepolia'),
});

Reading receipts

import { getX402Receipts } from '@a2x/sdk/x402';
 
const receipts = getX402Receipts(task);
for (const receipt of receipts) {
  console.log(receipt.success, receipt.transaction, receipt.network);
}

Supported scope

  • Standalone Flow from a2a-x402 v0.2. Embedded Flow (AP2 CartMandate etc.) isn't yet wired up — the extension spec notes the embedded flow "supports" nesting but implementing it is separate work.
  • exact scheme, EVM networks (base, base-sepolia, polygon, avalanche, …). The x402 npm package powers signing; adding Solana/SVM support means passing a Solana-compatible signer in a later release.
  • The base protocol is x402 v1 (x402Version: 1). a2a-x402 v0.2 pins to this version; x402 v2 exists as a forward-looking draft but a2a-x402 has not adopted it yet.

Reference