A2X

Archived · v0.9.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')
  .addExtension({ 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 the codes below. The seven names listed first come straight from spec §9.1; the remaining three are SDK-specific codes covering failure modes the SDK detects outside the facilitator's purview.

Code Source Meaning
INSUFFICIENT_FUNDS spec §9.1 Wallet can't cover the payment.
INVALID_SIGNATURE spec §9.1 Authorization signature failed verification.
EXPIRED_PAYMENT spec §9.1 Authorization was submitted after its validity window.
DUPLICATE_NONCE spec §9.1 Nonce has already been spent.
NETWORK_MISMATCH spec §9.1 Payload's network doesn't match any advertised accepts.
INVALID_AMOUNT spec §9.1 Authorization value doesn't match the required amount.
SETTLEMENT_FAILED spec §9.1 On-chain settle call failed.
INVALID_PAYLOAD SDK Payment payload is missing or structurally invalid.
INVALID_PAY_TO SDK Authorization target address doesn't match payTo.
VERIFY_FAILED SDK Facilitator rejected the signature, but the reason string didn't match any of the spec codes above.

The SDK uses the facilitator's invalidReason string to dispatch into the spec codes (mapVerifyFailureToCode() exports the same logic if you need it client-side). Clients SHOULD branch on the spec codes first and treat VERIFY_FAILED as an unmapped fallback.

Payment lifecycle

Every paid task runs through the same state machine (spec §7.1):

PAYMENT_REQUIRED → PAYMENT_REJECTED           (client declined the challenge)
PAYMENT_REQUIRED → PAYMENT_SUBMITTED          (client signed and resubmitted)
PAYMENT_SUBMITTED → PAYMENT_VERIFIED          (facilitator verified the signature)
PAYMENT_VERIFIED → PAYMENT_COMPLETED          (on-chain settlement succeeded)
PAYMENT_VERIFIED → PAYMENT_FAILED             (settlement failed on-chain)

The SDK emits payment-verified as a transient working-state event between submit and completion when streaming, so clients can surface a "settling on-chain…" indicator. In the blocking execute() path the state is recorded on task.status but clients only observe the final value.

Retry-on-failure (opt-in)

By default, verify/settle failures terminate the task with state failed and an error code. Spec §9 also allows the merchant to "request a payment requirement with input-required again"; set retryOnFailure: true on the executor to pick that strategy — failures will re-publish payment-required on the same task with the prior failure reason carried in X402PaymentRequiredResponse.error, letting the client top up the wallet (or refresh the nonce) and resubmit without creating a new task.

new X402PaymentExecutor(inner, {
  accepts: [...],
  retryOnFailure: true,
});

x402.payment.receipts accumulates every settle attempt (success or failure) across the task's lifetime per spec §7's "complete history" requirement.

Rejection handling

When a client decides not to pay it can respond with x402.payment.status: payment-rejected (spec §5.4.2). The executor terminates the task with state failed and status payment-rejected — no further challenges are published, the loop ends.

Client

High-level: A2XClient with x402

A2XClient itself runs the x402 dance when you pass an x402 option. Whether the agent gates on x402 is a property of its AgentCard, not the caller — so you don't have to pick between two client classes. If the agent never asks for payment, the client behaves as a plain A2A client.

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

The same call site works for streaming. The dance happens in-band — the generator yields the merchant's payment-required event and then continues with the followup stream's payment-verified → working → artifacts → payment-completed events:

for await (const event of client.sendMessageStream({ message: { … } })) {
  console.log(event);
}

If the merchant rejects the payment (verify or settle failed), the call throws X402PaymentFailedError with the on-chain reason attached. If every requirement in accepts[] exceeds maxAmount, signing throws X402NoSupportedRequirementError before any authorization is created.

The full option surface:

Field Default Purpose
signer required viem LocalAccount used to produce the EIP-3009 authorization.
maxAmount no cap Atomic-unit ceiling. Filters accepts[] before the selector runs, so a custom selectRequirement only sees the affordable subset.
selectRequirement first scheme === 'exact' Predicate over the (already filtered) requirements. Return undefined to abort.
onPaymentRequired none Hook that fires after payment-required and before signing. Throw to abort the dance — the caller observes the unmodified payment-required task (blocking) or a stream that closes after the payment-required event (streaming).

Extension activation header

Setting the x402 option auto-registers X402_EXTENSION_URI so every JSON-RPC request carries the X-A2A-Extensions activation header (spec §8). You don't need to pass extensions separately for x402.

To activate other extensions, list them in extensions or call client.registerExtension(uri) at runtime:

new A2XClient(url, {
  extensions: ['https://example.org/some-extension'],
  x402: { signer },                              // X402_EXTENSION_URI auto-added
});

Low-level: signX402Payment

When you need to inspect the payment-required task before signing — e.g. to show the user a confirmation modal, fetch the signer's balance, or route across multiple wallets — drive the dance manually using the primitives. A2XClient without the x402 option will hand you the raw payment-required task, and signX402Payment produces the metadata block to attach to the followup message/send call.

import { A2XClient } from '@a2x/sdk/client';
import {
  signX402Payment,
  getX402PaymentRequirements,
} from '@a2x/sdk/x402';
 
const client = new A2XClient(url, {
  extensions: [X402_EXTENSION_URI], // still required for header activation
});
 
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);
}

Server-side enforcement

Per spec §8, x402-capable clients MUST include the extension URI in the X-A2A-Extensions HTTP header on every JSON-RPC request:

X-A2A-Extensions: https://github.com/google-agentic-commerce/a2a-x402/blob/main/spec/v0.2

When the merchant agent declares the extension with required: true on its AgentCard, DefaultRequestHandler rejects requests whose header doesn't list that URI (error -32600). The check only runs when a RequestContext is provided to handler.handle() — pure in-process invocations skip it.

The client side is covered for you when you set A2XClientOptions.x402 — see the section above. If you drive the dance manually via signX402Payment, pass extensions: [X402_EXTENSION_URI] to the A2XClient constructor (or call client.registerExtension(X402_EXTENSION_URI)) so the header gets emitted on every request.

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