From 9bc424e021b57ab4ccef9a53db3df8fe20e519a5 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Thu, 18 Dec 2025 21:47:29 +0100
Subject: [PATCH 1/6] update to x402 v2
---
typescript/agentkit/package.json | 6 +-
.../src/action-providers/x402/README.md | 88 ++--
.../src/action-providers/x402/schemas.ts | 61 ++-
.../src/action-providers/x402/utils.ts | 454 ++++++++++++++++--
.../x402/x402ActionProvider.ts | 369 +++++++-------
typescript/pnpm-lock.yaml | 250 ++++++----
6 files changed, 868 insertions(+), 360 deletions(-)
diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json
index 69ac8a251..3a9ebceb0 100644
--- a/typescript/agentkit/package.json
+++ b/typescript/agentkit/package.json
@@ -53,12 +53,14 @@
"@solana/spl-token": "^0.4.12",
"@solana/web3.js": "^1.98.1",
"@vaultsfyi/sdk": "^2.1.9",
+ "@x402/evm": "^2.0.0",
+ "@x402/svm": "^2.0.0",
+ "@x402/fetch": "^2.0.0",
"@zerodev/ecdsa-validator": "^5.4.5",
"@zerodev/intent": "^0.0.24",
"@zerodev/sdk": "^5.4.28",
"@zoralabs/coins-sdk": "0.2.8",
"@zoralabs/protocol-deployments": "0.6.1",
- "axios": "^1.9.0",
"bs58": "^4.0.1",
"canonicalize": "^2.1.0",
"clanker-sdk": "^4.1.18",
@@ -71,8 +73,6 @@
"sushi": "6.2.1",
"twitter-api-v2": "^1.18.2",
"viem": "2.38.3",
- "x402": "^0.6.0",
- "x402-axios": "^0.6.0",
"zod": "^3.23.8"
},
"devDependencies": {
diff --git a/typescript/agentkit/src/action-providers/x402/README.md b/typescript/agentkit/src/action-providers/x402/README.md
index 27ffec4a7..7087d5cca 100644
--- a/typescript/agentkit/src/action-providers/x402/README.md
+++ b/typescript/agentkit/src/action-providers/x402/README.md
@@ -20,15 +20,17 @@ x402/
1. `make_http_request`: Make initial HTTP request and handle 402 responses
2. `retry_http_request_with_x402`: Retry a request with payment after receiving payment details
-### Alternative Action
+### Alternative Actions
- `make_http_request_with_x402`: Direct payment-enabled requests (skips confirmation flow)
-- `discover_x402_services`: Discover available x402 services (optionally filter by asset and price)
+- `discover_x402_services`: Discover available x402 services (optionally filter by price)
## Overview
The x402 protocol enables APIs to require micropayments for access. When a client makes a request to a protected endpoint, the server responds with a `402 Payment Required` status code along with payment instructions.
+This provider supports **both v1 and v2 x402 endpoints** automatically.
+
### Recommended Two-Step Flow
1. Initial Request:
@@ -64,21 +66,33 @@ Makes initial request and handles 402 responses:
### `retry_http_request_with_x402` Action
-Retries request with payment after 402:
+Retries request with payment after 402. Supports both v1 and v2 payment option formats:
```typescript
+// v1 format (legacy endpoints)
{
url: "https://api.example.com/data",
- method: "GET", // Optional, defaults to GET
- headers: { "Accept": "..." }, // Optional
- body: { ... }, // Optional
- selectedPaymentOption: { // Payment details from 402 response
+ method: "GET",
+ selectedPaymentOption: {
scheme: "exact",
- network: "base-sepolia",
+ network: "base-sepolia", // v1 network identifier
maxAmountRequired: "1000",
asset: "0x..."
}
}
+
+// v2 format (CAIP-2 network identifiers)
+{
+ url: "https://api.example.com/data",
+ method: "GET",
+ selectedPaymentOption: {
+ scheme: "exact",
+ network: "eip155:84532", // v2 CAIP-2 identifier
+ amount: "1000",
+ asset: "0x...",
+ payTo: "0x..."
+ }
+}
```
### `make_http_request_with_x402` Action
@@ -96,22 +110,31 @@ Direct payment-enabled requests (use with caution):
### `discover_x402_services` Action
-Fetches available services and optionally filters them by maximum price in USDC whole units. The action defaults to USDC on the current network:
+Fetches all available services from the x402 Bazaar with full pagination support. Returns simplified output with url, price, and description for each service.
```typescript
{
- maxUsdcPrice: 0.1 // optional (e.g., 0.1 for $0.10 USDC)
+ discoveryUrl: "https://...", // Optional, defaults to CDP discovery endpoint
+ maxUsdcPrice: 0.1 // Optional, filter by max price in USDC
}
```
-Example filtering for USDC services under $0.10:
-
-```ts
-const maxUsdcPrice = 0.1;
-
-const services = await discover_x402_services({ maxUsdcPrice });
-
+Example response:
+```json
+{
+ "success": true,
+ "walletNetworks": ["base-sepolia", "eip155:84532"],
+ "total": 150,
+ "returned": 25,
+ "services": [
+ {
+ "url": "https://api.example.com/weather",
+ "price": "0.001 USDC on base-sepolia",
+ "description": "Get current weather data"
+ }
+ ]
+}
```
## Response Format
@@ -132,21 +155,32 @@ Successful responses include payment proof when payment was made:
## Network Support
-The x402 provider currently supports the following networks:
-- `base-mainnet`
-- `base-sepolia`
-- `solana-mainnet`
-- `solana-devnet`
+The x402 provider supports the following networks:
+
+| Internal Network ID | v1 Identifier | v2 CAIP-2 Identifier |
+|---------------------|---------------|----------------------|
+| `base-mainnet` | `base` | `eip155:8453` |
+| `base-sepolia` | `base-sepolia` | `eip155:84532` |
+| `solana-mainnet` | `solana` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` |
+| `solana-devnet` | `solana-devnet` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` |
+
+The provider supports both EVM and SVM (Solana) wallets for signing payment transactions.
+
+## v1/v2 Compatibility
+
+This provider automatically handles both v1 and v2 x402 endpoints:
-The provider requires EVM-compatible networks where the wallet can sign payment transactions.
+- **Discovery**: Filters resources matching either v1 or v2 network identifiers
+- **Payment**: The `@x402/fetch` library handles protocol version detection automatically
+- **Headers**: Supports both `X-PAYMENT-RESPONSE` (v1) and `PAYMENT-RESPONSE` (v2) headers
## Dependencies
This action provider requires:
-- `axios` - For making HTTP requests
-- `x402-axios` - For handling x402 payment flows
-- `x402` - For payment requirement types and validation
+- `@x402/fetch` - For handling x402 payment flows
+- `@x402/evm` - For EVM payment scheme support
+- `@x402/svm` - For Solana payment scheme support
## Notes
-For more information on the **x402 protocol**, visit the [x402 documentation](https://x402.gitbook.io/x402/).
\ No newline at end of file
+For more information on the **x402 protocol**, visit the [x402 documentation](https://docs.cdp.coinbase.com/x402/overview).
diff --git a/typescript/agentkit/src/action-providers/x402/schemas.ts b/typescript/agentkit/src/action-providers/x402/schemas.ts
index 877cacec2..15d973e29 100644
--- a/typescript/agentkit/src/action-providers/x402/schemas.ts
+++ b/typescript/agentkit/src/action-providers/x402/schemas.ts
@@ -1,14 +1,37 @@
import { z } from "zod";
+// Default facilitator URL for x402 Bazaar
+export const DEFAULT_FACILITATOR_URL =
+ "https://api.cdp.coinbase.com/platform/v2/x402";
+
// Schema for listing x402 services
export const ListX402ServicesSchema = z
.object({
+ facilitatorUrl: z
+ .string()
+ .url()
+ .default(DEFAULT_FACILITATOR_URL)
+ .describe(
+ `Optional URL for the x402 facilitator service. Defaults to ${DEFAULT_FACILITATOR_URL}`,
+ ),
maxUsdcPrice: z
.number()
.optional()
.describe(
"Optional maximum price in USDC whole units (e.g., 0.1 for 0.10 USDC). Only USDC payment options will be considered when this filter is applied.",
),
+ x402Versions: z
+ .array(z.union([z.literal(1), z.literal(2)]))
+ .default([1, 2])
+ .describe(
+ "Filter by x402 protocol version (1 or 2). Defaults to accepting both versions.",
+ ),
+ keyword: z
+ .string()
+ .optional()
+ .describe(
+ "Optional keyword to filter services by description (case-insensitive). Example: 'weather' to find weather-related services.",
+ ),
})
.strip()
.describe("Parameters for listing x402 services with optional filtering");
@@ -39,6 +62,35 @@ export const HttpRequestSchema = z
.strip()
.describe("Instructions for making a basic HTTP request");
+// Payment option schema that supports both v1 and v2 formats
+const PaymentOptionSchema = z
+ .object({
+ scheme: z.string().describe("Payment scheme (e.g., 'exact')"),
+ network: z
+ .string()
+ .describe("Network identifier (v1: 'base-sepolia' or v2 CAIP-2: 'eip155:84532')"),
+ asset: z.string().describe("Asset address or identifier"),
+ // v1 format
+ maxAmountRequired: z
+ .string()
+ .optional()
+ .describe("Maximum amount required (v1 format)"),
+ // v2 format
+ amount: z
+ .string()
+ .optional()
+ .describe("Amount required (v2 format)"),
+ price: z
+ .string()
+ .optional()
+ .describe("Price (v2 format, e.g., '$0.01')"),
+ payTo: z
+ .string()
+ .optional()
+ .describe("Payment recipient address (v2 format)"),
+ })
+ .describe("Payment option supporting both v1 and v2 x402 formats");
+
// Schema for retrying a failed request with x402 payment
export const RetryWithX402Schema = z
.object({
@@ -53,14 +105,7 @@ export const RetryWithX402Schema = z
.describe("The HTTP method to use for the request"),
headers: z.record(z.string()).optional().describe("Optional headers to include in the request"),
body: z.any().optional().describe("Optional request body for POST/PUT/PATCH requests"),
- selectedPaymentOption: z
- .object({
- scheme: z.string(),
- network: z.string(),
- maxAmountRequired: z.string(),
- asset: z.string(),
- })
- .describe("The payment option to use for this request"),
+ selectedPaymentOption: PaymentOptionSchema.describe("The payment option to use for this request"),
})
.strip()
.describe("Instructions for retrying a request with x402 payment after receiving a 402 response");
diff --git a/typescript/agentkit/src/action-providers/x402/utils.ts b/typescript/agentkit/src/action-providers/x402/utils.ts
index 33ee9c0fe..aff080a73 100644
--- a/typescript/agentkit/src/action-providers/x402/utils.ts
+++ b/typescript/agentkit/src/action-providers/x402/utils.ts
@@ -1,14 +1,10 @@
import { Network } from "../../network";
-import { AxiosError } from "axios";
import { getTokenDetails } from "../erc20/utils";
import { TOKEN_ADDRESSES_BY_SYMBOLS } from "../erc20/constants";
-import { formatUnits, parseUnits } from "viem";
+import { formatUnits, parseUnits, LocalAccount } from "viem";
import { EvmWalletProvider, SvmWalletProvider, WalletProvider } from "../../wallet-providers";
-
-/**
- * Supported network types for x402 protocol
- */
-export type X402Network = "base" | "base-sepolia" | "solana" | "solana-devnet";
+import type { ClientEvmSigner } from "@x402/evm";
+import type { ClientSvmSigner } from "@x402/svm";
/**
* USDC token addresses for Solana networks
@@ -19,41 +15,426 @@ const SOLANA_USDC_ADDRESSES = {
} as const;
/**
- * Converts the internal network ID to the format expected by the x402 protocol.
- *
- * @param network - The network to convert
- * @returns The network ID in x402 format
- * @throws Error if the network is not supported
- */
-export function getX402Network(network: Network): X402Network | string | undefined {
- switch (network.networkId) {
- case "base-mainnet":
- return "base";
- case "base-sepolia":
- return "base-sepolia";
- case "solana-mainnet":
- return "solana";
- case "solana-devnet":
- return "solana-devnet";
- default:
- return network.networkId;
+ * Network mapping from internal network ID to both v1 and v2 (CAIP-2) formats.
+ * Used for filtering discovery results that may contain either format.
+ */
+const NETWORK_MAPPINGS: Record = {
+ "base-mainnet": ["base", "eip155:8453"],
+ "base-sepolia": ["base-sepolia", "eip155:84532"],
+ "solana-mainnet": ["solana", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
+ "solana-devnet": ["solana-devnet", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
+};
+
+/**
+ * Resource from discovery API
+ */
+export interface DiscoveryResource {
+ url?: string;
+ resource?: string;
+ type?: string;
+ metadata?: {
+ description?: string;
+ input?: Record;
+ output?: Record;
+ [key: string]: unknown;
+ };
+ accepts?: PaymentOption[];
+ x402Version?: number;
+ lastUpdated?: string;
+}
+
+/**
+ * Payment option from discovery API (supports both v1 and v2 formats)
+ */
+export interface PaymentOption {
+ scheme: string;
+ network: string;
+ asset: string;
+ // v1 format
+ maxAmountRequired?: string;
+ // v2 format
+ amount?: string;
+ price?: string;
+ payTo?: string;
+ description?: string;
+}
+
+/**
+ * Simplified resource output for LLM consumption
+ */
+export interface SimplifiedResource {
+ url: string;
+ price: string;
+ description: string;
+}
+
+/**
+ * x402 protocol version type
+ */
+export type X402Version = 1 | 2;
+
+/**
+ * Returns array of matching network identifiers (both v1 and v2 CAIP-2 formats).
+ * Used for filtering discovery results that may contain either format.
+ *
+ * @param network - The network object
+ * @returns Array of network identifiers that match the wallet's network
+ */
+export function getX402Networks(network: Network): string[] {
+ const networkId = network.networkId;
+ if (!networkId) {
+ return [];
+ }
+ return NETWORK_MAPPINGS[networkId] ?? [networkId];
+}
+
+/**
+ * Converts a viem LocalAccount to a ClientEvmSigner for x402Client.
+ *
+ * @param localAccount - The LocalAccount from EvmWalletProvider.toSigner()
+ * @returns A ClientEvmSigner compatible with @x402/evm
+ */
+export function toClientEvmSigner(localAccount: LocalAccount): ClientEvmSigner {
+ return {
+ address: localAccount.address,
+ signTypedData: async message => {
+ return localAccount.signTypedData(message);
+ },
+ };
+}
+
+/**
+ * Creates a client SVM signer from an SvmWalletProvider.
+ *
+ * @param walletProvider - The SVM wallet provider
+ * @returns A signer object compatible with @x402/svm
+ */
+export async function toClientSvmSigner(walletProvider: SvmWalletProvider): Promise {
+ return walletProvider.toSigner();
+}
+
+/**
+ * Fetches a URL with retry logic and exponential backoff for rate limiting.
+ *
+ * @param url - The URL to fetch
+ * @param maxRetries - Maximum number of retries (default 3)
+ * @param initialDelayMs - Initial delay in milliseconds (default 1000)
+ * @returns The fetch Response
+ */
+async function fetchWithRetry(
+ url: string,
+ maxRetries: number = 3,
+ initialDelayMs: number = 1000,
+): Promise {
+ let lastError: Error | null = null;
+
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
+ const response = await fetch(url);
+
+ if (response.ok) {
+ return response;
+ }
+
+ if (response.status === 429 && attempt < maxRetries) {
+ const delayMs = initialDelayMs * Math.pow(2, attempt);
+ await new Promise(resolve => setTimeout(resolve, delayMs));
+ continue;
+ }
+
+ lastError = new Error(`Discovery API error: ${response.status} ${response.statusText}`);
+ break;
+ }
+
+ throw lastError ?? new Error("Failed to fetch after retries");
+}
+
+/**
+ * Fetches all resources from the discovery API with pagination.
+ *
+ * @param discoveryUrl - The base URL for discovery
+ * @param pageSize - Number of resources per page (default 100)
+ * @returns Array of all discovered resources
+ */
+export async function fetchAllDiscoveryResources(
+ discoveryUrl: string,
+ pageSize: number = 1000,
+): Promise {
+ const allResources: DiscoveryResource[] = [];
+ let offset = 0;
+ let hasMore = true;
+
+ while (hasMore) {
+ const url = new URL(discoveryUrl);
+ url.searchParams.set("limit", pageSize.toString());
+ url.searchParams.set("offset", offset.toString());
+
+ const response = await fetchWithRetry(url.toString());
+
+ const data = await response.json();
+ const resources = data.resources ?? data.items ?? [];
+
+ if (resources.length === 0) {
+ hasMore = false;
+ } else {
+ allResources.push(...resources);
+ offset += resources.length;
+
+ // Stop when getting fewer resources than requested (last page)
+ if (resources.length < pageSize) {
+ hasMore = false;
+ }
+ }
}
+
+ return allResources;
+}
+
+/**
+ * Filters resources by network compatibility.
+ * Matches resources that accept any of the wallet's network identifiers (v1 or v2 format).
+ *
+ * @param resources - Array of discovery resources
+ * @param walletNetworks - Array of network identifiers to match
+ * @returns Filtered array of resources
+ */
+export function filterByNetwork(
+ resources: DiscoveryResource[],
+ walletNetworks: string[],
+): DiscoveryResource[] {
+ return resources.filter(resource => {
+ const accepts = resource.accepts ?? [];
+ return accepts.some(option => walletNetworks.includes(option.network));
+ });
+}
+
+/**
+ * Extracts description from a resource based on its x402 version.
+ * - v1: description is in accepts[].description
+ * - v2: description is in metadata.description
+ *
+ * @param resource - The discovery resource
+ * @returns The description string or empty string if not found
+ */
+function getResourceDescription(resource: DiscoveryResource): string {
+ if (resource.x402Version === 2) {
+ const metadataDesc = resource.metadata?.description;
+ return typeof metadataDesc === "string" ? metadataDesc : "";
+ }
+
+ // v1: look in accepts[].description
+ const accepts = resource.accepts ?? [];
+ for (const option of accepts) {
+ if (option.description?.trim()) {
+ return option.description;
+ }
+ }
+ return "";
+}
+
+/**
+ * Filters resources by having a valid description.
+ * Removes resources with empty or default descriptions.
+ * Supports both v1 (accepts[].description) and v2 (metadata.description) formats.
+ *
+ * @param resources - Array of discovery resources
+ * @returns Filtered array of resources with valid descriptions
+ */
+export function filterByDescription(resources: DiscoveryResource[]): DiscoveryResource[] {
+ return resources.filter(resource => {
+ const desc = getResourceDescription(resource).trim();
+ return desc && desc !== "" && desc !== "Access to protected content";
+ });
+}
+
+/**
+ * Filters resources by x402 protocol version.
+ * Uses the x402Version field on the resource.
+ *
+ * @param resources - Array of discovery resources
+ * @param allowedVersions - Array of allowed versions (default: [1, 2])
+ * @returns Filtered array of resources matching the allowed versions
+ */
+export function filterByX402Version(
+ resources: DiscoveryResource[],
+ allowedVersions: X402Version[] = [1, 2],
+): DiscoveryResource[] {
+ return resources.filter(resource => {
+ const version = resource.x402Version;
+ if (version === undefined) {
+ return true; // Include resources without version info
+ }
+ return allowedVersions.includes(version as X402Version);
+ });
+}
+
+/**
+ * Filters resources by keyword appearing in description or URL.
+ * Case-insensitive search.
+ * Supports both v1 (accepts[].description) and v2 (metadata.description) formats.
+ *
+ * @param resources - Array of discovery resources
+ * @param keyword - The keyword to search for in descriptions and URLs
+ * @returns Filtered array of resources with matching descriptions or URLs
+ */
+export function filterByKeyword(
+ resources: DiscoveryResource[],
+ keyword: string,
+): DiscoveryResource[] {
+ const lowerKeyword = keyword.toLowerCase();
+ return resources.filter(resource => {
+ // Check description (version-aware)
+ const desc = getResourceDescription(resource).toLowerCase();
+ if (desc.includes(lowerKeyword)) {
+ return true;
+ }
+
+ // Also check the URL for keyword matches
+ const url = (resource.resource ?? resource.url ?? "").toLowerCase();
+ if (url.includes(lowerKeyword)) {
+ return true;
+ }
+
+ return false;
+ });
+}
+
+/**
+ * Filters resources by maximum USDC price.
+ *
+ * @param resources - Array of discovery resources
+ * @param maxUsdcPrice - Maximum price in whole USDC units
+ * @param walletProvider - Wallet provider for asset identification
+ * @param walletNetworks - Array of network identifiers to match
+ * @returns Filtered array of resources within price limit
+ */
+export async function filterByMaxPrice(
+ resources: DiscoveryResource[],
+ maxUsdcPrice: number,
+ walletProvider: WalletProvider,
+ walletNetworks: string[],
+): Promise {
+ const filtered: DiscoveryResource[] = [];
+
+ for (const resource of resources) {
+ const accepts = resource.accepts ?? [];
+ let shouldInclude = false;
+
+ for (const option of accepts) {
+ if (!walletNetworks.includes(option.network)) {
+ continue;
+ }
+
+ if (!option.asset) {
+ continue;
+ }
+
+ // Check if this is a USDC asset
+ if (!isUsdcAsset(option.asset, walletProvider)) {
+ continue;
+ }
+
+ // Get the amount (supports both v1 maxAmountRequired and v2 amount/price)
+ const amountStr = option.maxAmountRequired ?? option.amount ?? option.price;
+ if (!amountStr) {
+ continue;
+ }
+
+ try {
+ const maxUsdcPriceAtomic = await convertWholeUnitsToAtomic(
+ maxUsdcPrice,
+ option.asset,
+ walletProvider,
+ );
+ if (maxUsdcPriceAtomic) {
+ const resourceAmount = BigInt(amountStr);
+ const maxAmount = BigInt(maxUsdcPriceAtomic);
+ if (resourceAmount <= maxAmount) {
+ shouldInclude = true;
+ break;
+ }
+ }
+ } catch {
+ // Skip if conversion fails
+ continue;
+ }
+ }
+
+ if (shouldInclude) {
+ filtered.push(resource);
+ }
+ }
+
+ return filtered;
+}
+
+/**
+ * Formats resources into simplified output for LLM consumption.
+ *
+ * @param resources - Array of discovery resources
+ * @param walletNetworks - Array of network identifiers to match for price extraction
+ * @param walletProvider - Wallet provider for formatting
+ * @returns Array of simplified resources with url, price, description
+ */
+export async function formatSimplifiedResources(
+ resources: DiscoveryResource[],
+ walletNetworks: string[],
+ walletProvider: WalletProvider,
+): Promise {
+ const simplified: SimplifiedResource[] = [];
+
+ for (const resource of resources) {
+ const accepts = resource.accepts ?? [];
+ const matchingOption = accepts.find(opt => walletNetworks.includes(opt.network));
+
+ if (!matchingOption) {
+ continue;
+ }
+
+ // Extract URL: v1 and v2 both use resource.resource, but v2 docs show resource.url
+ const url = resource.resource ?? resource.url ?? "";
+
+ // Extract description (version-aware via helper)
+ const description = getResourceDescription(resource);
+
+ let price = "Unknown";
+
+ // Get the amount (supports both v1 and v2 formats)
+ const amountStr = matchingOption.maxAmountRequired ?? matchingOption.amount ?? matchingOption.price;
+ if (amountStr && matchingOption.asset) {
+ price = await formatPaymentOption(
+ {
+ asset: matchingOption.asset,
+ maxAmountRequired: amountStr,
+ network: matchingOption.network,
+ },
+ walletProvider,
+ );
+ }
+
+ simplified.push({
+ url,
+ price,
+ description,
+ });
+ }
+
+ return simplified;
}
/**
* Helper method to handle HTTP errors consistently.
*
- * @param error - The axios error to handle
+ * @param error - The error to handle
* @param url - The URL that was being accessed when the error occurred
* @returns A JSON string containing formatted error details
*/
-export function handleHttpError(error: AxiosError, url: string): string {
- if (error.response) {
+export function handleHttpError(error: unknown, url: string): string {
+ if (error instanceof Response) {
return JSON.stringify(
{
error: true,
- message: `HTTP ${error.response.status} error when accessing ${url}`,
- details: (error.response.data as { error?: string })?.error || error.response.statusText,
+ message: `HTTP ${error.status} error when accessing ${url}`,
+ details: error.statusText,
suggestion: "Check if the URL is correct and the API is available.",
},
null,
@@ -61,7 +442,7 @@ export function handleHttpError(error: AxiosError, url: string): string {
);
}
- if (error.request) {
+ if (error instanceof TypeError && error.message.includes("fetch")) {
return JSON.stringify(
{
error: true,
@@ -74,11 +455,12 @@ export function handleHttpError(error: AxiosError, url: string): string {
);
}
+ const message = error instanceof Error ? error.message : String(error);
return JSON.stringify(
{
error: true,
message: `Error making request to ${url}`,
- details: error.message,
+ details: message,
suggestion: "Please check the request parameters and try again.",
},
null,
@@ -116,7 +498,7 @@ export async function formatPaymentOption(
if (asset.toLowerCase() === address.toLowerCase()) {
const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18;
const formattedAmount = formatUnits(BigInt(maxAmountRequired), decimals);
- return `${formattedAmount} ${symbol} on ${network} network`;
+ return `${formattedAmount} ${symbol} on ${network}`;
}
}
}
@@ -126,7 +508,7 @@ export async function formatPaymentOption(
const tokenDetails = await getTokenDetails(walletProvider, asset);
if (tokenDetails) {
const formattedAmount = formatUnits(BigInt(maxAmountRequired), tokenDetails.decimals);
- return `${formattedAmount} ${tokenDetails.name} on ${network} network`;
+ return `${formattedAmount} ${tokenDetails.name} on ${network}`;
}
} catch {
// If we can't get token details, fall back to raw format
@@ -141,12 +523,12 @@ export async function formatPaymentOption(
if (usdcAddress && asset === usdcAddress) {
// USDC has 6 decimals on Solana
const formattedAmount = formatUnits(BigInt(maxAmountRequired), 6);
- return `${formattedAmount} USDC on ${network} network`;
+ return `${formattedAmount} USDC on ${network}`;
}
}
// Fallback to original format for non-EVM/SVM networks or when token details can't be fetched
- return `${asset} ${maxAmountRequired} on ${network} network`;
+ return `${asset} ${maxAmountRequired} on ${network}`;
}
/**
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
index 3d9aaeaed..fb43a3d98 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
@@ -9,17 +9,22 @@ import {
ListX402ServicesSchema,
} from "./schemas";
import { EvmWalletProvider, WalletProvider, SvmWalletProvider } from "../../wallet-providers";
-import axios, { AxiosError } from "axios";
-import { withPaymentInterceptor, decodeXPaymentResponse } from "x402-axios";
-import { PaymentRequirements } from "x402/types";
-import { useFacilitator } from "x402/verify";
-import { facilitator } from "@coinbase/x402";
+import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
+import { registerExactEvmScheme } from "@x402/evm/exact/client";
+import { registerExactSvmScheme } from "@x402/svm/exact/client";
import {
- getX402Network,
+ getX402Networks,
handleHttpError,
formatPaymentOption,
- convertWholeUnitsToAtomic,
- isUsdcAsset,
+ fetchAllDiscoveryResources,
+ filterByNetwork,
+ filterByDescription,
+ filterByX402Version,
+ filterByKeyword,
+ filterByMaxPrice,
+ formatSimplifiedResources,
+ toClientEvmSigner,
+ toClientSvmSigner,
} from "./utils";
const SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia", "solana-mainnet", "solana-devnet"];
@@ -36,11 +41,32 @@ export class X402ActionProvider extends ActionProvider {
super("x402", []);
}
+ /**
+ * Creates an x402 client configured for the given wallet provider.
+ *
+ * @param walletProvider - The wallet provider to configure the client for
+ * @returns Configured x402Client
+ */
+ private async createX402Client(walletProvider: WalletProvider): Promise {
+ const client = new x402Client();
+
+ if (walletProvider instanceof EvmWalletProvider) {
+ const signer = toClientEvmSigner(walletProvider.toSigner());
+ registerExactEvmScheme(client, { signer });
+ } else if (walletProvider instanceof SvmWalletProvider) {
+ const signer = await toClientSvmSigner(walletProvider);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ registerExactSvmScheme(client, { signer: signer as any });
+ }
+
+ return client;
+ }
+
/**
* Discovers available x402 services with optional filtering.
*
* @param walletProvider - The wallet provider to use for network filtering
- * @param args - Optional filters: maxUsdcPrice
+ * @param args - Optional filters: discoveryUrl, maxUsdcPrice
* @returns JSON string with the list of services (filtered by network and description)
*/
@CreateAction({
@@ -54,121 +80,60 @@ export class X402ActionProvider extends ActionProvider {
args: z.infer,
): Promise {
try {
- const { list } = useFacilitator(facilitator);
- const services = await list();
- if (!services || !services.items) {
+ const discoveryUrl = args.facilitatorUrl + "/discovery/resources";
+
+ // Fetch all resources with pagination
+ const allResources = await fetchAllDiscoveryResources(discoveryUrl);
+
+ if (allResources.length === 0) {
return JSON.stringify({
error: true,
message: "No services found",
});
}
- // Get the current wallet network
- const walletNetwork = getX402Network(walletProvider.getNetwork());
+ // Get the wallet's network identifiers (both v1 and v2 formats)
+ const walletNetworks = getX402Networks(walletProvider.getNetwork());
- // Filter services by network, description, and optional USDC price
+ // Apply filter pipeline
+ let filteredResources = filterByNetwork(allResources, walletNetworks);
+ // filteredResources = filterByDescription(filteredResources);
+ filteredResources = filterByX402Version(filteredResources, args.x402Versions);
+
+ // Apply keyword filter if provided
+ if (args.keyword) {
+ filteredResources = filterByKeyword(filteredResources, args.keyword);
+ }
+
+ // Apply price filter if maxUsdcPrice is provided
const hasValidMaxUsdcPrice =
typeof args.maxUsdcPrice === "number" &&
Number.isFinite(args.maxUsdcPrice) &&
args.maxUsdcPrice > 0;
- const filteredServices = services.items.filter(item => {
- // Filter by network - only include services that accept the current wallet network
- const accepts = Array.isArray(item.accepts) ? item.accepts : [];
- const hasMatchingNetwork = accepts.some(req => req.network === walletNetwork);
-
- // Filter out services with empty or default descriptions
- const hasDescription = accepts.some(
- req =>
- req.description &&
- req.description.trim() !== "" &&
- req.description.trim() !== "Access to protected content",
- );
-
- return hasMatchingNetwork && hasDescription;
- });
-
- // Apply USDC price filtering if maxUsdcPrice is provided (only consider USDC assets)
- let priceFilteredServices = filteredServices;
if (hasValidMaxUsdcPrice) {
- priceFilteredServices = [];
- for (const item of filteredServices) {
- const accepts = Array.isArray(item.accepts) ? item.accepts : [];
- let shouldInclude = false;
-
- for (const req of accepts) {
- if (req.network === walletNetwork && req.asset && req.maxAmountRequired) {
- // Only consider USDC assets when maxUsdcPrice filter is applied
- if (isUsdcAsset(req.asset, walletProvider)) {
- try {
- const maxUsdcPriceAtomic = await convertWholeUnitsToAtomic(
- args.maxUsdcPrice as number,
- req.asset,
- walletProvider,
- );
- if (maxUsdcPriceAtomic) {
- const requirement = BigInt(req.maxAmountRequired);
- const maxUsdcPriceAtomicBigInt = BigInt(maxUsdcPriceAtomic);
- if (requirement <= maxUsdcPriceAtomicBigInt) {
- shouldInclude = true;
- break;
- }
- }
- } catch {
- // If conversion fails, skip this requirement
- continue;
- }
- }
- }
- }
-
- if (shouldInclude) {
- priceFilteredServices.push(item);
- }
- }
+ filteredResources = await filterByMaxPrice(
+ filteredResources,
+ args.maxUsdcPrice as number,
+ walletProvider,
+ walletNetworks,
+ );
}
- // Format the filtered services
- const filtered = await Promise.all(
- priceFilteredServices.map(async item => {
- const accepts = Array.isArray(item.accepts) ? item.accepts : [];
- const matchingAccept = accepts.find(req => req.network === walletNetwork);
-
- // Format amount if available
- let formattedMaxAmount = matchingAccept?.maxAmountRequired;
- if (matchingAccept?.maxAmountRequired && matchingAccept?.asset) {
- formattedMaxAmount = await formatPaymentOption(
- {
- asset: matchingAccept.asset,
- maxAmountRequired: matchingAccept.maxAmountRequired,
- network: matchingAccept.network,
- },
- walletProvider,
- );
- }
-
- return {
- resource: item.resource,
- description: matchingAccept?.description || "",
- cost: formattedMaxAmount,
- ...(matchingAccept?.outputSchema?.input && {
- input: matchingAccept.outputSchema.input,
- }),
- ...(matchingAccept?.outputSchema?.output && {
- output: matchingAccept.outputSchema.output,
- }),
- ...(item.metadata && item.metadata.length > 0 && { metadata: item.metadata }),
- };
- }),
+ // Format simplified output
+ const simplifiedResources = await formatSimplifiedResources(
+ filteredResources,
+ walletNetworks,
+ walletProvider,
);
return JSON.stringify(
{
success: true,
- walletNetwork,
- total: services.items.length,
- returned: filtered.length,
- items: filtered,
+ services: simplifiedResources,
+ walletNetworks,
+ total: allResources.length,
+ returned: simplifiedResources.length,
},
null,
2,
@@ -213,59 +178,72 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
args: z.infer,
): Promise {
try {
- const response = await axios.request({
- url: args.url,
+ const response = await fetch(args.url, {
method: args.method ?? "GET",
headers: args.headers ?? undefined,
- data: args.body,
- validateStatus: status => status === 402 || (status >= 200 && status < 300),
+ body: args.body ? JSON.stringify(args.body) : undefined,
});
if (response.status !== 402) {
+ const data = await this.parseResponseData(response);
return JSON.stringify(
{
success: true,
url: args.url,
method: args.method,
status: response.status,
- data: response.data,
+ data,
},
null,
2,
);
}
- // Check if wallet network matches any available payment options
- const walletNetwork = getX402Network(walletProvider.getNetwork());
- const availableNetworks = response.data.accepts.map(option => option.network);
- const hasMatchingNetwork = availableNetworks.includes(walletNetwork);
+ // Handle 402 Payment Required
+ const paymentData = await response.json();
+ const walletNetworks = getX402Networks(walletProvider.getNetwork());
+ const availableNetworks = (paymentData.accepts ?? []).map(
+ (option: { network: string }) => option.network,
+ );
+ const hasMatchingNetwork = availableNetworks.some((net: string) =>
+ walletNetworks.includes(net),
+ );
+
+ let paymentOptionsText = `The wallet networks ${walletNetworks.join(", ")} do not match any available payment options (${availableNetworks.join(", ")}).`;
- let paymentOptionsText = `The wallet network ${walletNetwork} does not match any available payment options (${availableNetworks.join(", ")}).`;
- // Format payment options for matching networks
if (hasMatchingNetwork) {
- const matchingOptions = response.data.accepts.filter(
- option => option.network === walletNetwork,
+ const matchingOptions = (paymentData.accepts ?? []).filter(
+ (option: { network: string }) => walletNetworks.includes(option.network),
);
const formattedOptions = await Promise.all(
- matchingOptions.map(option => formatPaymentOption(option, walletProvider)),
+ matchingOptions.map((option: { asset: string; maxAmountRequired?: string; amount?: string; network: string }) =>
+ formatPaymentOption(
+ {
+ asset: option.asset,
+ maxAmountRequired: option.maxAmountRequired ?? option.amount ?? "0",
+ network: option.network,
+ },
+ walletProvider,
+ ),
+ ),
);
paymentOptionsText = `The payment options are: ${formattedOptions.join(", ")}`;
}
return JSON.stringify({
status: "error_402_payment_required",
- acceptablePaymentOptions: response.data.accepts,
+ acceptablePaymentOptions: paymentData.accepts,
nextSteps: [
"Inform the user that the requested server replied with a 402 Payment Required response.",
paymentOptionsText,
hasMatchingNetwork ? "Ask the user if they want to retry the request with payment." : "",
hasMatchingNetwork
- ? `Use retry_http_request_with_x402 to retry the request with payment.`
+ ? "Use retry_http_request_with_x402 to retry the request with payment."
: "",
],
});
} catch (error) {
- return handleHttpError(error as AxiosError, args.url);
+ return handleHttpError(error, args.url);
}
}
@@ -296,15 +274,15 @@ DO NOT use this action directly without first trying make_http_request!`,
): Promise {
try {
// Check network compatibility before attempting payment
- const walletNetwork = getX402Network(walletProvider.getNetwork());
+ const walletNetworks = getX402Networks(walletProvider.getNetwork());
const selectedNetwork = args.selectedPaymentOption.network;
- if (walletNetwork !== selectedNetwork) {
+ if (!walletNetworks.includes(selectedNetwork)) {
return JSON.stringify(
{
error: true,
message: "Network mismatch",
- details: `Wallet is on ${walletNetwork} but payment requires ${selectedNetwork}`,
+ details: `Wallet is on ${walletNetworks.join(", ")} but payment requires ${selectedNetwork}`,
},
null,
2,
@@ -328,59 +306,42 @@ DO NOT use this action directly without first trying make_http_request!`,
);
}
+ // Create x402 client with appropriate signer
+ const client = await this.createX402Client(walletProvider);
+ const fetchWithPayment = wrapFetchWithPayment(fetch, client);
+
// Make the request with payment handling
- const account = await walletProvider.toSigner();
+ const response = await fetchWithPayment(args.url, {
+ method: args.method ?? "GET",
+ headers: args.headers ?? undefined,
+ body: args.body ? JSON.stringify(args.body) : undefined,
+ });
- const paymentSelector = (accepts: PaymentRequirements[]) => {
- const { scheme, network, maxAmountRequired, asset } = args.selectedPaymentOption;
+ const data = await this.parseResponseData(response);
- let paymentRequirements = accepts.find(
- accept =>
- accept.scheme === scheme &&
- accept.network === network &&
- accept.maxAmountRequired <= maxAmountRequired &&
- accept.asset === asset,
- );
- if (paymentRequirements) {
- return paymentRequirements;
- }
+ // Check for payment proof in headers (supports both v1 and v2 header names)
+ const paymentResponseHeader =
+ response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
- paymentRequirements = accepts.find(
- accept =>
- accept.scheme === scheme &&
- accept.network === network &&
- accept.maxAmountRequired <= maxAmountRequired &&
- accept.asset === asset,
- );
- if (paymentRequirements) {
- return paymentRequirements;
+ let paymentProof: Record | null = null;
+ if (paymentResponseHeader) {
+ try {
+ paymentProof = JSON.parse(atob(paymentResponseHeader));
+ } catch {
+ // If parsing fails, include raw header
+ paymentProof = { raw: paymentResponseHeader };
}
+ }
- return accepts[0];
- };
-
- const api = withPaymentInterceptor(
- axios.create({}),
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- account as any,
- paymentSelector as unknown as Parameters[2],
- );
-
- const response = await api.request({
- url: args.url,
- method: args.method ?? "GET",
- headers: args.headers ?? undefined,
- data: args.body,
- });
-
- // Check for payment proof
- const paymentProof = response.headers["x-payment-response"]
- ? decodeXPaymentResponse(response.headers["x-payment-response"])
- : null;
+ // Get the amount used (supports both v1 and v2 formats)
+ const amountUsed =
+ args.selectedPaymentOption.maxAmountRequired ??
+ args.selectedPaymentOption.amount ??
+ args.selectedPaymentOption.price;
return JSON.stringify({
status: "success",
- data: response.data,
+ data,
message: "Request completed successfully with payment",
details: {
url: args.url,
@@ -388,19 +349,13 @@ DO NOT use this action directly without first trying make_http_request!`,
paymentUsed: {
network: args.selectedPaymentOption.network,
asset: args.selectedPaymentOption.asset,
- amount: args.selectedPaymentOption.maxAmountRequired,
+ amount: amountUsed,
},
- paymentProof: paymentProof
- ? {
- transaction: paymentProof.transaction,
- network: paymentProof.network,
- payer: paymentProof.payer,
- }
- : null,
+ paymentProof,
},
});
} catch (error) {
- return handleHttpError(error as AxiosError, args.url);
+ return handleHttpError(error, args.url);
}
}
@@ -414,7 +369,7 @@ DO NOT use this action directly without first trying make_http_request!`,
@CreateAction({
name: "make_http_request_with_x402",
description: `
-⚠️ WARNING: This action automatically handles payments without asking for confirmation!
+WARNING: This action automatically handles payments without asking for confirmation!
Only use this when explicitly told to skip the confirmation flow.
For most cases, you should:
@@ -454,22 +409,32 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
2,
);
}
- const account = await walletProvider.toSigner();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const api = withPaymentInterceptor(axios.create({}), account as any);
+ // Create x402 client with appropriate signer
+ const client = await this.createX402Client(walletProvider);
+ const fetchWithPayment = wrapFetchWithPayment(fetch, client);
- const response = await api.request({
- url: args.url,
+ const response = await fetchWithPayment(args.url, {
method: args.method ?? "GET",
headers: args.headers ?? undefined,
- data: args.body,
+ body: args.body ? JSON.stringify(args.body) : undefined,
});
- // Check for payment proof
- const paymentProof = response.headers["x-payment-response"]
- ? decodeXPaymentResponse(response.headers["x-payment-response"])
- : null;
+ const data = await this.parseResponseData(response);
+
+ // Check for payment proof in headers (supports both v1 and v2 header names)
+ const paymentResponseHeader =
+ response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
+
+ let paymentProof: Record | null = null;
+ if (paymentResponseHeader) {
+ try {
+ paymentProof = JSON.parse(atob(paymentResponseHeader));
+ } catch {
+ // If parsing fails, include raw header
+ paymentProof = { raw: paymentResponseHeader };
+ }
+ }
return JSON.stringify(
{
@@ -478,21 +443,31 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
url: args.url,
method: args.method,
status: response.status,
- data: response.data,
- paymentProof: paymentProof
- ? {
- transaction: paymentProof.transaction,
- network: paymentProof.network,
- payer: paymentProof.payer,
- }
- : null,
+ data,
+ paymentProof,
},
null,
2,
);
} catch (error) {
- return handleHttpError(error as AxiosError, args.url);
+ return handleHttpError(error, args.url);
+ }
+ }
+
+ /**
+ * Parses response data based on content type.
+ *
+ * @param response - The fetch Response object
+ * @returns Parsed response data
+ */
+ private async parseResponseData(response: Response): Promise {
+ const contentType = response.headers.get("content-type") ?? "";
+
+ if (contentType.includes("application/json")) {
+ return response.json();
}
+
+ return response.text();
}
/**
diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml
index 931b0ffa6..4b9bc60ad 100644
--- a/typescript/pnpm-lock.yaml
+++ b/typescript/pnpm-lock.yaml
@@ -79,7 +79,7 @@ importers:
version: 0.20.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)
'@coinbase/x402':
specifier: ^0.6.3
- version: 0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ version: 0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@ensofinance/sdk':
specifier: ^2.0.6
version: 2.0.6
@@ -94,7 +94,7 @@ importers:
version: 1.18.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))
'@solana/kit':
specifier: ^2.1.1
- version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/spl-token':
specifier: ^0.4.12
version: 0.4.13(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10)
@@ -103,7 +103,16 @@ importers:
version: 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)
'@vaultsfyi/sdk':
specifier: ^2.1.9
- version: 2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ version: 2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@x402/evm':
+ specifier: ^2.0.0
+ version: 2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)
+ '@x402/fetch':
+ specifier: ^2.0.0
+ version: 2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)
+ '@x402/svm':
+ specifier: ^2.0.0
+ version: 2.0.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@zerodev/ecdsa-validator':
specifier: ^5.4.5
version: 5.4.5(@zerodev/sdk@5.4.28(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))
@@ -119,9 +128,6 @@ importers:
'@zoralabs/protocol-deployments':
specifier: 0.6.1
version: 0.6.1
- axios:
- specifier: ^1.9.0
- version: 1.9.0
bs58:
specifier: ^4.0.1
version: 4.0.1
@@ -158,12 +164,6 @@ importers:
viem:
specifier: 2.38.3
version: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)
- x402:
- specifier: ^0.6.0
- version: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- x402-axios:
- specifier: ^0.6.0
- version: 0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
zod:
specifier: ^3.23.8
version: 3.24.2
@@ -2469,6 +2469,18 @@ packages:
'@walletconnect/window-metadata@1.0.1':
resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==}
+ '@x402/core@2.0.0':
+ resolution: {integrity: sha512-tgC8Ujj8Bi9zU1318RAOcagffaHRMPupqj0tlmcOm4j/a/iWGoN58SIpqFV98qkZi7z6BpFAsScCyXnmaDvIyQ==}
+
+ '@x402/evm@2.0.0':
+ resolution: {integrity: sha512-BA8zC1/AWYq99tWRLNoqudJkeZJpkbPdHMEjrsEOjineVoijnz5N/0NO0klxT64oDG9kNbeczwLU/mTj7NneEw==}
+
+ '@x402/fetch@2.0.0':
+ resolution: {integrity: sha512-7odbj+d9Op6Ns62YPOAw37Afvf0ldZ90bHOrAJFP9ahcV6l0iHdTtr7NcSqIZI+0r4emDmIIpGtLRhyyVCQYnA==}
+
+ '@x402/svm@2.0.0':
+ resolution: {integrity: sha512-VJdxUoiNZ18/wucXToEscVKNfN+x18jK9c6p0AscS7iJaNq3z3ST/guf8asRedEWX9Rus62SilTmq3UYdNcnvw==}
+
'@xmtp/agent-sdk@1.1.4':
resolution: {integrity: sha512-98hFtZmQln+QxA3eN6Of3ot9jxjUsRoMBvGz4yRV7gRrqJtH4iVqWTCCN1SqFpBPaQECdKPJhlxCEE7II+5jMw==}
engines: {node: '>=20'}
@@ -2586,6 +2598,17 @@ packages:
zod:
optional: true
+ abitype@1.2.3:
+ resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ zod: ^3.22.0 || ^4.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ zod:
+ optional: true
+
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -4952,6 +4975,14 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
+ ox@0.10.5:
+ resolution: {integrity: sha512-mXJRiZswmX46abrzNkJpTN9sPJ/Rhevsp5Dfg0z80D55aoLNmEV4oN+/+feSNW593c2CnHavMqSVBanpJ0lUkQ==}
+ peerDependencies:
+ typescript: '>=5.4.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
ox@0.6.7:
resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==}
peerDependencies:
@@ -6197,6 +6228,14 @@ packages:
typescript:
optional: true
+ viem@2.43.1:
+ resolution: {integrity: sha512-S33pBNlRvOlVv4+L94Z8ydCMDB1j0cuHFUvaC28i6OTxw3uY1P4M3h1YDFK8YC1H9/lIbeBTTvCRhi0FqU/2iw==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
wagmi@2.17.0:
resolution: {integrity: sha512-NxdS/oHG3j4l3JzNIAb0zL14p3GXOS9YU4Q3WS+0eTChFIfdqBrurQMImPLNC9P5rXYym6cjC2feeqhHVMJjpg==}
peerDependencies:
@@ -6344,9 +6383,6 @@ packages:
utf-8-validate:
optional: true
- x402-axios@0.6.0:
- resolution: {integrity: sha512-UyKZnepVH3xz8+BsHIqAsvZohHa06yLdOOWo51ctEeazOdH57gRLVmtvEVbpBnrO2Fh0kbgcagzVlM/5v3M0Zw==}
-
x402-fetch@0.7.0:
resolution: {integrity: sha512-HS7v6wsIVrU8TvAGBwRmA3I+ZXbanPraA3OMj90y6Hn1Mej1wAELOK4VpGh6zI8d6w5E464BnGu9o0FE+8DRAA==}
@@ -6983,11 +7019,11 @@ snapshots:
- utf-8-validate
- zod
- '@coinbase/x402@0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@coinbase/x402@0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
'@coinbase/cdp-sdk': 1.38.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10)
viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
- x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
zod: 3.25.56
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -8643,18 +8679,18 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
- '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))':
+ '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))':
dependencies:
- '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))':
+ '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))':
dependencies:
- '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
- '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))':
+ '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))':
dependencies:
- '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)':
dependencies:
@@ -8828,7 +8864,7 @@ snapshots:
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
- '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
'@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
@@ -8841,11 +8877,11 @@ snapshots:
'@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/rpc-parsed-types': 2.3.0(typescript@5.8.2)
'@solana/rpc-spec-types': 2.3.0(typescript@5.8.2)
- '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
- '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
typescript: 5.8.2
@@ -8935,14 +8971,14 @@ snapshots:
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
- '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
'@solana/errors': 2.3.0(typescript@5.8.2)
'@solana/functional': 2.3.0(typescript@5.8.2)
'@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.2)
'@solana/subscribable': 2.3.0(typescript@5.8.2)
typescript: 5.8.2
- ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
+ ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
'@solana/rpc-subscriptions-spec@2.3.0(typescript@5.8.2)':
dependencies:
@@ -8952,7 +8988,7 @@ snapshots:
'@solana/subscribable': 2.3.0(typescript@5.8.2)
typescript: 5.8.2
- '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
'@solana/errors': 2.3.0(typescript@5.8.2)
'@solana/fast-stable-stringify': 2.3.0(typescript@5.8.2)
@@ -8960,7 +8996,7 @@ snapshots:
'@solana/promises': 2.3.0(typescript@5.8.2)
'@solana/rpc-spec-types': 2.3.0(typescript@5.8.2)
'@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
- '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.2)
'@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
@@ -9076,7 +9112,7 @@ snapshots:
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
- '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
'@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
@@ -9084,7 +9120,7 @@ snapshots:
'@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/promises': 2.3.0(typescript@5.8.2)
'@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
- '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
'@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)
@@ -9429,9 +9465,9 @@ snapshots:
'@uniswap/token-lists@1.0.0-beta.33': {}
- '@vaultsfyi/sdk@2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ '@vaultsfyi/sdk@2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
dependencies:
- x402-fetch: 0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ x402-fetch: 0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -10072,6 +10108,46 @@ snapshots:
'@walletconnect/window-getters': 1.0.1
tslib: 1.14.1
+ '@x402/core@2.0.0':
+ dependencies:
+ zod: 3.25.56
+
+ '@x402/evm@2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)':
+ dependencies:
+ '@x402/core': 2.0.0
+ viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
+ zod: 3.25.56
+ transitivePeerDependencies:
+ - bufferutil
+ - typescript
+ - utf-8-validate
+
+ '@x402/fetch@2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)':
+ dependencies:
+ '@x402/core': 2.0.0
+ viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
+ zod: 3.25.56
+ transitivePeerDependencies:
+ - bufferutil
+ - typescript
+ - utf-8-validate
+
+ '@x402/svm@2.0.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))':
+ dependencies:
+ '@scure/base': 1.2.6
+ '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@x402/core': 2.0.0
+ zod: 3.25.56
+ transitivePeerDependencies:
+ - '@solana/sysvars'
+ - fastestsmallesttextencoderdecoder
+ - typescript
+ - ws
+
'@xmtp/agent-sdk@1.1.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)':
dependencies:
'@xmtp/content-type-reaction': 2.0.2
@@ -10220,6 +10296,11 @@ snapshots:
typescript: 5.8.2
zod: 3.25.56
+ abitype@1.2.3(typescript@5.8.2)(zod@3.25.56):
+ optionalDependencies:
+ typescript: 5.8.2
+ zod: 3.25.56
+
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -13258,6 +13339,21 @@ snapshots:
object-keys: 1.1.1
safe-push-apply: 1.0.0
+ ox@0.10.5(typescript@5.8.2)(zod@3.25.56):
+ dependencies:
+ '@adraffy/ens-normalize': 1.11.1
+ '@noble/ciphers': 1.3.0
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.2.3(typescript@5.8.2)(zod@3.25.56)
+ eventemitter3: 5.0.1
+ optionalDependencies:
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - zod
+
ox@0.6.7(typescript@5.8.2)(zod@3.25.56):
dependencies:
'@adraffy/ens-normalize': 1.11.0
@@ -14665,6 +14761,23 @@ snapshots:
- utf-8-validate
- zod
+ viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56):
+ dependencies:
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.2.3(typescript@5.8.2)(zod@3.25.56)
+ isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ ox: 0.10.5(typescript@5.8.2)(zod@3.25.56)
+ ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
+ optionalDependencies:
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+ - zod
+
wagmi@2.17.0(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.25.56):
dependencies:
'@tanstack/react-query': 5.89.0(react@18.3.1)
@@ -14837,51 +14950,10 @@ snapshots:
bufferutil: 4.0.9
utf-8-validate: 5.0.10
- x402-axios@0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
- dependencies:
- axios: 1.9.0
- viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
- x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- zod: 3.25.56
- transitivePeerDependencies:
- - '@azure/app-configuration'
- - '@azure/cosmos'
- - '@azure/data-tables'
- - '@azure/identity'
- - '@azure/keyvault-secrets'
- - '@azure/storage-blob'
- - '@capacitor/preferences'
- - '@deno/kv'
- - '@netlify/blobs'
- - '@planetscale/database'
- - '@react-native-async-storage/async-storage'
- - '@solana/sysvars'
- - '@tanstack/query-core'
- - '@tanstack/react-query'
- - '@types/react'
- - '@upstash/redis'
- - '@vercel/blob'
- - '@vercel/functions'
- - '@vercel/kv'
- - aws4fetch
- - bufferutil
- - db0
- - debug
- - encoding
- - fastestsmallesttextencoderdecoder
- - immer
- - ioredis
- - react
- - supports-color
- - typescript
- - uploadthing
- - utf-8-validate
- - ws
-
- x402-fetch@0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
+ x402-fetch@0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
dependencies:
viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
- x402: 0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ x402: 0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
zod: 3.25.56
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -14917,14 +14989,14 @@ snapshots:
- utf-8-validate
- ws
- x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
+ x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
dependencies:
'@scure/base': 1.2.6
- '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
- '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
- '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))
- '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56)
wagmi: 2.17.0(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.25.56)
zod: 3.25.56
@@ -14962,14 +15034,14 @@ snapshots:
- utf-8-validate
- ws
- x402@0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
+ x402@0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
dependencies:
'@scure/base': 1.2.6
- '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
- '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
- '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))
- '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))
+ '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@solana/wallet-standard-features': 1.3.0
'@wallet-standard/app': 1.1.0
'@wallet-standard/base': 1.1.0
From 108ab21494801e612842e2c8bb5ca8c35e9f0413 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 19 Dec 2025 16:06:08 +0100
Subject: [PATCH 2/6] improve x402 requests
---
.../src/action-providers/x402/constants.ts | 76 +++++++
.../src/action-providers/x402/schemas.ts | 55 ++++-
.../src/action-providers/x402/utils.ts | 135 ++++--------
.../x402/x402ActionProvider.ts | 196 ++++++++++++++----
4 files changed, 319 insertions(+), 143 deletions(-)
create mode 100644 typescript/agentkit/src/action-providers/x402/constants.ts
diff --git a/typescript/agentkit/src/action-providers/x402/constants.ts b/typescript/agentkit/src/action-providers/x402/constants.ts
new file mode 100644
index 000000000..4d6490843
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/x402/constants.ts
@@ -0,0 +1,76 @@
+/**
+ * Supported networks for x402 payment protocol
+ */
+export const SUPPORTED_NETWORKS = [
+ "base-mainnet",
+ "base-sepolia",
+ "solana-mainnet",
+ "solana-devnet",
+] as const;
+
+/**
+ * USDC token addresses for Solana networks
+ */
+export const SOLANA_USDC_ADDRESSES = {
+ "solana-devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
+ "solana-mainnet": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
+} as const;
+
+/**
+ * Network mapping from internal network ID to both v1 and v2 (CAIP-2) formats.
+ * Used for filtering discovery results that may contain either format.
+ */
+export const NETWORK_MAPPINGS: Record = {
+ "base-mainnet": ["base", "eip155:8453"],
+ "base-sepolia": ["base-sepolia", "eip155:84532"],
+ "solana-mainnet": ["solana", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
+ "solana-devnet": ["solana-devnet", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
+};
+
+/**
+ * x402 protocol version type
+ */
+export type X402Version = 1 | 2;
+
+/**
+ * Payment option from discovery API (supports both v1 and v2 formats)
+ */
+export interface PaymentOption {
+ scheme: string;
+ network: string;
+ asset: string;
+ // v1 format
+ maxAmountRequired?: string;
+ // v2 format
+ amount?: string;
+ price?: string;
+ payTo?: string;
+ description?: string;
+}
+
+/**
+ * Resource from discovery API
+ */
+export interface DiscoveryResource {
+ url?: string;
+ resource?: string;
+ type?: string;
+ metadata?: {
+ description?: string;
+ input?: Record;
+ output?: Record;
+ [key: string]: unknown;
+ };
+ accepts?: PaymentOption[];
+ x402Version?: number;
+ lastUpdated?: string;
+}
+
+/**
+ * Simplified resource output for LLM consumption
+ */
+export interface SimplifiedResource {
+ url: string;
+ price: string;
+ description: string;
+}
diff --git a/typescript/agentkit/src/action-providers/x402/schemas.ts b/typescript/agentkit/src/action-providers/x402/schemas.ts
index 15d973e29..1c299efba 100644
--- a/typescript/agentkit/src/action-providers/x402/schemas.ts
+++ b/typescript/agentkit/src/action-providers/x402/schemas.ts
@@ -12,10 +12,12 @@ export const ListX402ServicesSchema = z
.url()
.default(DEFAULT_FACILITATOR_URL)
.describe(
- `Optional URL for the x402 facilitator service. Defaults to ${DEFAULT_FACILITATOR_URL}`,
+ `Optional URL for the x402 facilitator service.`,
),
maxUsdcPrice: z
.number()
+ .positive()
+ .finite()
.optional()
.describe(
"Optional maximum price in USDC whole units (e.g., 0.1 for 0.10 USDC). Only USDC payment options will be considered when this filter is applied.",
@@ -53,11 +55,25 @@ export const HttpRequestSchema = z
.optional()
.nullable()
.describe("Optional headers to include in the request"),
+ queryParams: z
+ .record(z.string())
+ .optional()
+ .nullable()
+ .describe(
+ "Query parameters to append to the URL as key-value string pairs. " +
+ "Use ONLY for GET/DELETE requests. " +
+ "For POST/PUT/PATCH, you must use the 'body' parameter instead. " +
+ "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric",
+ ),
body: z
.any()
.optional()
.nullable()
- .describe("Optional request body for POST/PUT/PATCH requests"),
+ .describe(
+ "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " +
+ "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " +
+ "Do NOT use for GET or DELETE, use queryParams instead.",
+ ),
})
.strip()
.describe("Instructions for making a basic HTTP request");
@@ -104,7 +120,24 @@ export const RetryWithX402Schema = z
.default("GET")
.describe("The HTTP method to use for the request"),
headers: z.record(z.string()).optional().describe("Optional headers to include in the request"),
- body: z.any().optional().describe("Optional request body for POST/PUT/PATCH requests"),
+ queryParams: z
+ .record(z.string())
+ .optional()
+ .nullable()
+ .describe(
+ "Query parameters to append to the URL as key-value string pairs. " +
+ "Use ONLY for GET/DELETE requests. " +
+ "For POST/PUT/PATCH, you must use the 'body' parameter instead. " +
+ "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric",
+ ),
+ body: z
+ .any()
+ .optional()
+ .describe(
+ "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " +
+ "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " +
+ "Do NOT use for GET or DELETE, use queryParams instead.",
+ ),
selectedPaymentOption: PaymentOptionSchema.describe("The payment option to use for this request"),
})
.strip()
@@ -127,11 +160,25 @@ export const DirectX402RequestSchema = z
.optional()
.nullable()
.describe("Optional headers to include in the request"),
+ queryParams: z
+ .record(z.string())
+ .optional()
+ .nullable()
+ .describe(
+ "Query parameters to append to the URL as key-value string pairs. " +
+ "Use ONLY for GET/DELETE requests. " +
+ "For POST/PUT/PATCH, use the 'body' parameter instead. " +
+ "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric",
+ ),
body: z
.any()
.optional()
.nullable()
- .describe("Optional request body for POST/PUT/PATCH requests"),
+ .describe(
+ "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " +
+ "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " +
+ "Do NOT use for GET or DELETE, use queryParams instead.",
+ ),
})
.strip()
.describe(
diff --git a/typescript/agentkit/src/action-providers/x402/utils.ts b/typescript/agentkit/src/action-providers/x402/utils.ts
index aff080a73..b3d032c3f 100644
--- a/typescript/agentkit/src/action-providers/x402/utils.ts
+++ b/typescript/agentkit/src/action-providers/x402/utils.ts
@@ -3,75 +3,13 @@ import { getTokenDetails } from "../erc20/utils";
import { TOKEN_ADDRESSES_BY_SYMBOLS } from "../erc20/constants";
import { formatUnits, parseUnits, LocalAccount } from "viem";
import { EvmWalletProvider, SvmWalletProvider, WalletProvider } from "../../wallet-providers";
-import type { ClientEvmSigner } from "@x402/evm";
-import type { ClientSvmSigner } from "@x402/svm";
-
-/**
- * USDC token addresses for Solana networks
- */
-const SOLANA_USDC_ADDRESSES = {
- "solana-devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
- "solana-mainnet": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
-} as const;
-
-/**
- * Network mapping from internal network ID to both v1 and v2 (CAIP-2) formats.
- * Used for filtering discovery results that may contain either format.
- */
-const NETWORK_MAPPINGS: Record = {
- "base-mainnet": ["base", "eip155:8453"],
- "base-sepolia": ["base-sepolia", "eip155:84532"],
- "solana-mainnet": ["solana", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
- "solana-devnet": ["solana-devnet", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
-};
-
-/**
- * Resource from discovery API
- */
-export interface DiscoveryResource {
- url?: string;
- resource?: string;
- type?: string;
- metadata?: {
- description?: string;
- input?: Record;
- output?: Record;
- [key: string]: unknown;
- };
- accepts?: PaymentOption[];
- x402Version?: number;
- lastUpdated?: string;
-}
-
-/**
- * Payment option from discovery API (supports both v1 and v2 formats)
- */
-export interface PaymentOption {
- scheme: string;
- network: string;
- asset: string;
- // v1 format
- maxAmountRequired?: string;
- // v2 format
- amount?: string;
- price?: string;
- payTo?: string;
- description?: string;
-}
-
-/**
- * Simplified resource output for LLM consumption
- */
-export interface SimplifiedResource {
- url: string;
- price: string;
- description: string;
-}
-
-/**
- * x402 protocol version type
- */
-export type X402Version = 1 | 2;
+import {
+ SOLANA_USDC_ADDRESSES,
+ NETWORK_MAPPINGS,
+ type DiscoveryResource,
+ type SimplifiedResource,
+ type X402Version,
+} from "./constants";
/**
* Returns array of matching network identifiers (both v1 and v2 CAIP-2 formats).
@@ -89,28 +27,18 @@ export function getX402Networks(network: Network): string[] {
}
/**
- * Converts a viem LocalAccount to a ClientEvmSigner for x402Client.
- *
- * @param localAccount - The LocalAccount from EvmWalletProvider.toSigner()
- * @returns A ClientEvmSigner compatible with @x402/evm
- */
-export function toClientEvmSigner(localAccount: LocalAccount): ClientEvmSigner {
- return {
- address: localAccount.address,
- signTypedData: async message => {
- return localAccount.signTypedData(message);
- },
- };
-}
-
-/**
- * Creates a client SVM signer from an SvmWalletProvider.
+ * Gets network ID from a CAIP-2 or v1 network identifier.
*
- * @param walletProvider - The SVM wallet provider
- * @returns A signer object compatible with @x402/svm
+ * @param network - The x402 network identifier (e.g., "eip155:8453" for v2 or "base" for v1)
+ * @returns The network ID (e.g., "base-mainnet") or the original if not found
*/
-export async function toClientSvmSigner(walletProvider: SvmWalletProvider): Promise {
- return walletProvider.toSigner();
+export function getNetworkId(network: string): string {
+ for (const [agentKitId, formats] of Object.entries(NETWORK_MAPPINGS)) {
+ if (formats.includes(network)) {
+ return agentKitId;
+ }
+ }
+ return network;
}
/**
@@ -498,7 +426,7 @@ export async function formatPaymentOption(
if (asset.toLowerCase() === address.toLowerCase()) {
const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18;
const formattedAmount = formatUnits(BigInt(maxAmountRequired), decimals);
- return `${formattedAmount} ${symbol} on ${network}`;
+ return `${formattedAmount} ${symbol} on ${getNetworkId(network)}`;
}
}
}
@@ -508,7 +436,7 @@ export async function formatPaymentOption(
const tokenDetails = await getTokenDetails(walletProvider, asset);
if (tokenDetails) {
const formattedAmount = formatUnits(BigInt(maxAmountRequired), tokenDetails.decimals);
- return `${formattedAmount} ${tokenDetails.name} on ${network}`;
+ return `${formattedAmount} ${tokenDetails.name} on ${getNetworkId(network)}`;
}
} catch {
// If we can't get token details, fall back to raw format
@@ -523,12 +451,12 @@ export async function formatPaymentOption(
if (usdcAddress && asset === usdcAddress) {
// USDC has 6 decimals on Solana
const formattedAmount = formatUnits(BigInt(maxAmountRequired), 6);
- return `${formattedAmount} USDC on ${network}`;
+ return `${formattedAmount} USDC on ${getNetworkId(network)}`;
}
}
// Fallback to original format for non-EVM/SVM networks or when token details can't be fetched
- return `${asset} ${maxAmountRequired} on ${network}`;
+ return `${asset} ${maxAmountRequired} on ${getNetworkId(network)}`;
}
/**
@@ -620,3 +548,24 @@ export async function convertWholeUnitsToAtomic(
// Fallback to 18 decimals for unknown tokens or non-EVM/SVM networks
return parseUnits(wholeUnits.toString(), 18).toString();
}
+
+ /**
+ * Builds a URL with query parameters appended.
+ *
+ * @param baseUrl - The base URL
+ * @param queryParams - Optional query parameters to append
+ * @returns URL string with query parameters
+ */
+ export function buildUrlWithParams(
+ baseUrl: string,
+ queryParams?: Record | null,
+ ): string {
+ if (!queryParams || Object.keys(queryParams).length === 0) {
+ return baseUrl;
+ }
+ const url = new URL(baseUrl);
+ Object.entries(queryParams).forEach(([key, value]) => {
+ url.searchParams.append(key, value);
+ });
+ return url.toString();
+ }
\ No newline at end of file
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
index fb43a3d98..c2f0a76f5 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
@@ -23,11 +23,10 @@ import {
filterByKeyword,
filterByMaxPrice,
formatSimplifiedResources,
- toClientEvmSigner,
- toClientSvmSigner,
+ buildUrlWithParams,
} from "./utils";
-
-const SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia", "solana-mainnet", "solana-devnet"];
+import { SUPPORTED_NETWORKS } from "./constants";
+import { ClientSvmSigner } from "@x402/svm";
/**
* X402ActionProvider provides actions for making HTTP requests, with optional x402 payment handling.
@@ -51,12 +50,12 @@ export class X402ActionProvider extends ActionProvider {
const client = new x402Client();
if (walletProvider instanceof EvmWalletProvider) {
- const signer = toClientEvmSigner(walletProvider.toSigner());
+ const signer = walletProvider.toSigner();
registerExactEvmScheme(client, { signer });
} else if (walletProvider instanceof SvmWalletProvider) {
- const signer = await toClientSvmSigner(walletProvider);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- registerExactSvmScheme(client, { signer: signer as any });
+ const signer = walletProvider.toSigner();
+ // TODO: Fix
+ registerExactSvmScheme(client, {signer: signer as unknown as ClientSvmSigner});
}
return client;
@@ -106,15 +105,10 @@ export class X402ActionProvider extends ActionProvider {
}
// Apply price filter if maxUsdcPrice is provided
- const hasValidMaxUsdcPrice =
- typeof args.maxUsdcPrice === "number" &&
- Number.isFinite(args.maxUsdcPrice) &&
- args.maxUsdcPrice > 0;
-
- if (hasValidMaxUsdcPrice) {
+ if (args.maxUsdcPrice !== undefined) {
filteredResources = await filterByMaxPrice(
filteredResources,
- args.maxUsdcPrice as number,
+ args.maxUsdcPrice,
walletProvider,
walletNetworks,
);
@@ -168,9 +162,9 @@ it will return payment details that can be used with retry_http_request_with_x40
EXAMPLES:
- Production API: make_http_request("https://api.example.com/weather")
- Local development: make_http_request("http://localhost:3000/api/data")
-- Testing x402: make_http_request("http://localhost:3000/protected")
-If you receive a 402 Payment Required response, use retry_http_request_with_x402 to handle the payment.`,
+If you receive a 402 Payment Required response, use retry_http_request_with_x402 to handle the payment.
+`,
schema: HttpRequestSchema,
})
async makeHttpRequest(
@@ -178,19 +172,34 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
args: z.infer,
): Promise {
try {
- const response = await fetch(args.url, {
- method: args.method ?? "GET",
+ const finalUrl = buildUrlWithParams(args.url, args.queryParams);
+ let method = args.method ?? "GET";
+ let canHaveBody = ["POST", "PUT", "PATCH"].includes(method);
+
+ let response = await fetch(finalUrl, {
+ method,
headers: args.headers ?? undefined,
- body: args.body ? JSON.stringify(args.body) : undefined,
+ body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined,
});
+ // Retry with other http method for 404 status code
+ if (response.status === 404){
+ method = method === "GET" ? "POST" : "GET";
+ canHaveBody = ["POST", "PUT", "PATCH"].includes(method);
+ response = await fetch(finalUrl, {
+ method,
+ headers: args.headers ?? undefined,
+ body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined,
+ });
+ }
+
if (response.status !== 402) {
const data = await this.parseResponseData(response);
return JSON.stringify(
{
success: true,
- url: args.url,
- method: args.method,
+ url: finalUrl,
+ method,
status: response.status,
data,
},
@@ -200,11 +209,38 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
}
// Handle 402 Payment Required
- const paymentData = await response.json();
+ // v2 sends requirements in PAYMENT-REQUIRED header; v1 sends in body
const walletNetworks = getX402Networks(walletProvider.getNetwork());
- const availableNetworks = (paymentData.accepts ?? []).map(
- (option: { network: string }) => option.network,
- );
+
+ let acceptsArray: Array<{
+ scheme?: string;
+ network: string;
+ asset: string;
+ maxAmountRequired?: string;
+ amount?: string;
+ payTo?: string;
+ }> = [];
+ let paymentData: Record = {};
+
+ // Check for v2 header-based payment requirements
+ const paymentRequiredHeader = response.headers.get("payment-required");
+ if (paymentRequiredHeader) {
+ try {
+ const decoded = JSON.parse(atob(paymentRequiredHeader));
+ acceptsArray = decoded.accepts ?? [];
+ paymentData = decoded;
+ } catch {
+ // Header parsing failed, fall back to body
+ }
+ }
+
+ // Fall back to v1 body-based requirements if header not present or empty
+ if (acceptsArray.length === 0) {
+ paymentData = await response.json();
+ acceptsArray = (paymentData.accepts as typeof acceptsArray) ?? [];
+ }
+
+ const availableNetworks = acceptsArray.map((option) => option.network);
const hasMatchingNetwork = availableNetworks.some((net: string) =>
walletNetworks.includes(net),
);
@@ -212,11 +248,11 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
let paymentOptionsText = `The wallet networks ${walletNetworks.join(", ")} do not match any available payment options (${availableNetworks.join(", ")}).`;
if (hasMatchingNetwork) {
- const matchingOptions = (paymentData.accepts ?? []).filter(
- (option: { network: string }) => walletNetworks.includes(option.network),
+ const matchingOptions = acceptsArray.filter(
+ (option) => walletNetworks.includes(option.network),
);
const formattedOptions = await Promise.all(
- matchingOptions.map((option: { asset: string; maxAmountRequired?: string; amount?: string; network: string }) =>
+ matchingOptions.map((option) =>
formatPaymentOption(
{
asset: option.asset,
@@ -230,15 +266,25 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
paymentOptionsText = `The payment options are: ${formattedOptions.join(", ")}`;
}
+ // Extract discovery info from v2 response (description, mimeType, extensions)
+ const discoveryInfo: Record = {};
+ if (paymentData.description) discoveryInfo.description = paymentData.description;
+ if (paymentData.mimeType) discoveryInfo.mimeType = paymentData.mimeType;
+ if (paymentData.extensions) discoveryInfo.extensions = paymentData.extensions;
+
return JSON.stringify({
status: "error_402_payment_required",
- acceptablePaymentOptions: paymentData.accepts,
+ acceptablePaymentOptions: acceptsArray,
+ ...(Object.keys(discoveryInfo).length > 0 && { discoveryInfo }),
nextSteps: [
"Inform the user that the requested server replied with a 402 Payment Required response.",
paymentOptionsText,
+ "Include the description of the service in the response.",
+ "IMPORTANT: Identify required or optional query or body parameters based on this response. If there are any, you must inform the user and request them to provide the values. Always suggest example values.",
+ "CRITICAL: For POST/PUT/PATCH requests, you MUST use the 'body' parameter (NOT queryParams) to send data.",
hasMatchingNetwork ? "Ask the user if they want to retry the request with payment." : "",
hasMatchingNetwork
- ? "Use retry_http_request_with_x402 to retry the request with payment."
+ ? "Use retry_http_request_with_x402 to retry the request with payment. IMPORTANT: You must retry_http_request_with_x402 with the correct Http method. "
: "",
],
});
@@ -273,6 +319,9 @@ DO NOT use this action directly without first trying make_http_request!`,
args: z.infer,
): Promise {
try {
+
+ console.log("args", args);
+
// Check network compatibility before attempting payment
const walletNetworks = getX402Networks(walletProvider.getNetwork());
const selectedNetwork = args.selectedPaymentOption.network;
@@ -310,16 +359,27 @@ DO NOT use this action directly without first trying make_http_request!`,
const client = await this.createX402Client(walletProvider);
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
+ // Build URL with query params and determine if body is allowed
+ const finalUrl = buildUrlWithParams(args.url, args.queryParams);
+ const method = args.method ?? "GET";
+ const canHaveBody = ["POST", "PUT", "PATCH"].includes(method);
+
+ // Build headers, adding Content-Type for JSON body
+ const headers: Record = { ...(args.headers ?? {}) };
+ if (canHaveBody && args.body) {
+ headers["Content-Type"] = "application/json";
+ }
+
// Make the request with payment handling
- const response = await fetchWithPayment(args.url, {
- method: args.method ?? "GET",
- headers: args.headers ?? undefined,
- body: args.body ? JSON.stringify(args.body) : undefined,
+ const response = await fetchWithPayment(finalUrl, {
+ method,
+ headers,
+ body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined,
});
const data = await this.parseResponseData(response);
- // Check for payment proof in headers (supports both v1 and v2 header names)
+ // Check for payment proof in headers (v2: payment-response, v1: x-payment-response)
const paymentResponseHeader =
response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
@@ -339,13 +399,28 @@ DO NOT use this action directly without first trying make_http_request!`,
args.selectedPaymentOption.amount ??
args.selectedPaymentOption.price;
+ // Check if the response was successful
+ // Payment is only settled on 200 status
+ if (response.status !== 200) {
+ return JSON.stringify({
+ status: "error",
+ message: `Request failed with status ${response.status}. Payment was not settled.`,
+ httpStatus: response.status,
+ data,
+ details: {
+ url: finalUrl,
+ method,
+ },
+ });
+ }
+
return JSON.stringify({
status: "success",
data,
message: "Request completed successfully with payment",
details: {
- url: args.url,
- method: args.method,
+ url: finalUrl,
+ method,
paymentUsed: {
network: args.selectedPaymentOption.network,
asset: args.selectedPaymentOption.asset,
@@ -414,15 +489,26 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
const client = await this.createX402Client(walletProvider);
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
- const response = await fetchWithPayment(args.url, {
- method: args.method ?? "GET",
- headers: args.headers ?? undefined,
- body: args.body ? JSON.stringify(args.body) : undefined,
+ // Build URL with query params and determine if body is allowed
+ const finalUrl = buildUrlWithParams(args.url, args.queryParams);
+ const method = args.method ?? "GET";
+ const canHaveBody = ["POST", "PUT", "PATCH"].includes(method);
+
+ // Build headers, adding Content-Type for JSON body
+ const headers: Record = { ...(args.headers ?? {}) };
+ if (canHaveBody && args.body) {
+ headers["Content-Type"] = "application/json";
+ }
+
+ const response = await fetchWithPayment(finalUrl, {
+ method,
+ headers,
+ body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined,
});
const data = await this.parseResponseData(response);
- // Check for payment proof in headers (supports both v1 and v2 header names)
+ // Check for payment proof in headers (v2: payment-response, v1: x-payment-response)
const paymentResponseHeader =
response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
@@ -436,12 +522,29 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
}
}
+ // Check if the response was successful
+ // Payment is only settled on 200 status
+ if (response.status !== 200) {
+ return JSON.stringify(
+ {
+ success: false,
+ message: `Request failed with status ${response.status}. Payment was not settled.`,
+ url: finalUrl,
+ method,
+ status: response.status,
+ data,
+ },
+ null,
+ 2,
+ );
+ }
+
return JSON.stringify(
{
success: true,
message: "Request completed successfully (payment handled automatically if required)",
- url: args.url,
- method: args.method,
+ url: finalUrl,
+ method,
status: response.status,
data,
paymentProof,
@@ -476,7 +579,8 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
* @param network - The network to check support for
* @returns True if the network is supported, false otherwise
*/
- supportsNetwork = (network: Network) => SUPPORTED_NETWORKS.includes(network.networkId!);
+ supportsNetwork = (network: Network) =>
+ (SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!);
}
export const x402ActionProvider = () => new X402ActionProvider();
From 92198756ef6f44b8780bbbf507c8cab0f2a03969 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 19 Dec 2025 16:14:19 +0100
Subject: [PATCH 3/6] fix svm signer
---
.../src/action-providers/x402/x402ActionProvider.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
index c2f0a76f5..d11798110 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
@@ -26,7 +26,6 @@ import {
buildUrlWithParams,
} from "./utils";
import { SUPPORTED_NETWORKS } from "./constants";
-import { ClientSvmSigner } from "@x402/svm";
/**
* X402ActionProvider provides actions for making HTTP requests, with optional x402 payment handling.
@@ -53,9 +52,8 @@ export class X402ActionProvider extends ActionProvider {
const signer = walletProvider.toSigner();
registerExactEvmScheme(client, { signer });
} else if (walletProvider instanceof SvmWalletProvider) {
- const signer = walletProvider.toSigner();
- // TODO: Fix
- registerExactSvmScheme(client, {signer: signer as unknown as ClientSvmSigner});
+ const signer = await walletProvider.toSigner();
+ registerExactSvmScheme(client, { signer });
}
return client;
From 025bbc2713e9572d5283e7d45dfe3c213524ae0a Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 19 Dec 2025 16:33:54 +0100
Subject: [PATCH 4/6] add payai
---
.../src/action-providers/x402/constants.ts | 12 +++++++
.../src/action-providers/x402/schemas.ts | 31 ++++++++++++++-----
.../x402/x402ActionProvider.ts | 7 +++--
3 files changed, 40 insertions(+), 10 deletions(-)
diff --git a/typescript/agentkit/src/action-providers/x402/constants.ts b/typescript/agentkit/src/action-providers/x402/constants.ts
index 4d6490843..c4b13b1f4 100644
--- a/typescript/agentkit/src/action-providers/x402/constants.ts
+++ b/typescript/agentkit/src/action-providers/x402/constants.ts
@@ -1,3 +1,15 @@
+/**
+ * Known facilitator registry
+ */
+export const KNOWN_FACILITATORS = {
+ cdp: "https://api.cdp.coinbase.com/platform/v2/x402",
+ payai: "https://facilitator.payai.network",
+} as const;
+
+export type KnownFacilitatorName = keyof typeof KNOWN_FACILITATORS;
+
+export const DEFAULT_FACILITATOR: KnownFacilitatorName = "cdp";
+
/**
* Supported networks for x402 payment protocol
*/
diff --git a/typescript/agentkit/src/action-providers/x402/schemas.ts b/typescript/agentkit/src/action-providers/x402/schemas.ts
index 1c299efba..a5c1c6229 100644
--- a/typescript/agentkit/src/action-providers/x402/schemas.ts
+++ b/typescript/agentkit/src/action-providers/x402/schemas.ts
@@ -1,18 +1,33 @@
import { z } from "zod";
+import {
+ KNOWN_FACILITATORS,
+ KnownFacilitatorName,
+ DEFAULT_FACILITATOR,
+} from "./constants";
-// Default facilitator URL for x402 Bazaar
-export const DEFAULT_FACILITATOR_URL =
- "https://api.cdp.coinbase.com/platform/v2/x402";
+/**
+ * Resolves a facilitator name or URL to the actual URL.
+ * @param facilitator - Either a known facilitator name ('cdp', 'payai') or a custom URL
+ * @returns The facilitator URL
+ */
+export function resolveFacilitatorUrl(facilitator: string): string {
+ if (facilitator in KNOWN_FACILITATORS) {
+ return KNOWN_FACILITATORS[facilitator as KnownFacilitatorName];
+ }
+ return facilitator;
+}
// Schema for listing x402 services
export const ListX402ServicesSchema = z
.object({
- facilitatorUrl: z
- .string()
- .url()
- .default(DEFAULT_FACILITATOR_URL)
+ facilitator: z
+ .union([
+ z.enum(["cdp", "payai"]),
+ z.string().url(),
+ ])
+ .default(DEFAULT_FACILITATOR)
.describe(
- `Optional URL for the x402 facilitator service.`,
+ "Facilitator to query: 'cdp' (Coinbase CDP), 'payai' (PayAI Network), or a custom facilitator URL.",
),
maxUsdcPrice: z
.number()
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
index d11798110..3f7cbfc29 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
@@ -7,6 +7,7 @@ import {
RetryWithX402Schema,
DirectX402RequestSchema,
ListX402ServicesSchema,
+ resolveFacilitatorUrl,
} from "./schemas";
import { EvmWalletProvider, WalletProvider, SvmWalletProvider } from "../../wallet-providers";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
@@ -77,7 +78,9 @@ export class X402ActionProvider extends ActionProvider {
args: z.infer,
): Promise {
try {
- const discoveryUrl = args.facilitatorUrl + "/discovery/resources";
+ console.log("args", args);
+ const facilitatorUrl = resolveFacilitatorUrl(args.facilitator);
+ const discoveryUrl = facilitatorUrl + "/discovery/resources";
// Fetch all resources with pagination
const allResources = await fetchAllDiscoveryResources(discoveryUrl);
@@ -94,7 +97,7 @@ export class X402ActionProvider extends ActionProvider {
// Apply filter pipeline
let filteredResources = filterByNetwork(allResources, walletNetworks);
- // filteredResources = filterByDescription(filteredResources);
+ filteredResources = filterByDescription(filteredResources);
filteredResources = filterByX402Version(filteredResources, args.x402Versions);
// Apply keyword filter if provided
From f97655544b9c499410d2549608c621acd332b167 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 19 Dec 2025 16:48:57 +0100
Subject: [PATCH 5/6] fix unit tests
---
.../src/action-providers/x402/constants.ts | 2 +-
.../src/action-providers/x402/schemas.ts | 40 +-
.../src/action-providers/x402/utils.ts | 45 +-
.../x402/x402ActionProvider.test.ts | 569 ++++++++----------
.../x402/x402ActionProvider.ts | 83 ++-
5 files changed, 335 insertions(+), 404 deletions(-)
diff --git a/typescript/agentkit/src/action-providers/x402/constants.ts b/typescript/agentkit/src/action-providers/x402/constants.ts
index c4b13b1f4..961491dd3 100644
--- a/typescript/agentkit/src/action-providers/x402/constants.ts
+++ b/typescript/agentkit/src/action-providers/x402/constants.ts
@@ -68,10 +68,10 @@ export interface DiscoveryResource {
resource?: string;
type?: string;
metadata?: {
+ [key: string]: unknown;
description?: string;
input?: Record;
output?: Record;
- [key: string]: unknown;
};
accepts?: PaymentOption[];
x402Version?: number;
diff --git a/typescript/agentkit/src/action-providers/x402/schemas.ts b/typescript/agentkit/src/action-providers/x402/schemas.ts
index a5c1c6229..b19b0de7d 100644
--- a/typescript/agentkit/src/action-providers/x402/schemas.ts
+++ b/typescript/agentkit/src/action-providers/x402/schemas.ts
@@ -1,12 +1,9 @@
import { z } from "zod";
-import {
- KNOWN_FACILITATORS,
- KnownFacilitatorName,
- DEFAULT_FACILITATOR,
-} from "./constants";
+import { KNOWN_FACILITATORS, KnownFacilitatorName, DEFAULT_FACILITATOR } from "./constants";
/**
* Resolves a facilitator name or URL to the actual URL.
+ *
* @param facilitator - Either a known facilitator name ('cdp', 'payai') or a custom URL
* @returns The facilitator URL
*/
@@ -21,10 +18,7 @@ export function resolveFacilitatorUrl(facilitator: string): string {
export const ListX402ServicesSchema = z
.object({
facilitator: z
- .union([
- z.enum(["cdp", "payai"]),
- z.string().url(),
- ])
+ .union([z.enum(["cdp", "payai"]), z.string().url()])
.default(DEFAULT_FACILITATOR)
.describe(
"Facilitator to query: 'cdp' (Coinbase CDP), 'payai' (PayAI Network), or a custom facilitator URL.",
@@ -40,9 +34,7 @@ export const ListX402ServicesSchema = z
x402Versions: z
.array(z.union([z.literal(1), z.literal(2)]))
.default([1, 2])
- .describe(
- "Filter by x402 protocol version (1 or 2). Defaults to accepting both versions.",
- ),
+ .describe("Filter by x402 protocol version (1 or 2). Defaults to accepting both versions."),
keyword: z
.string()
.optional()
@@ -102,23 +94,11 @@ const PaymentOptionSchema = z
.describe("Network identifier (v1: 'base-sepolia' or v2 CAIP-2: 'eip155:84532')"),
asset: z.string().describe("Asset address or identifier"),
// v1 format
- maxAmountRequired: z
- .string()
- .optional()
- .describe("Maximum amount required (v1 format)"),
+ maxAmountRequired: z.string().optional().describe("Maximum amount required (v1 format)"),
// v2 format
- amount: z
- .string()
- .optional()
- .describe("Amount required (v2 format)"),
- price: z
- .string()
- .optional()
- .describe("Price (v2 format, e.g., '$0.01')"),
- payTo: z
- .string()
- .optional()
- .describe("Payment recipient address (v2 format)"),
+ amount: z.string().optional().describe("Amount required (v2 format)"),
+ price: z.string().optional().describe("Price (v2 format, e.g., '$0.01')"),
+ payTo: z.string().optional().describe("Payment recipient address (v2 format)"),
})
.describe("Payment option supporting both v1 and v2 x402 formats");
@@ -153,7 +133,9 @@ export const RetryWithX402Schema = z
"Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " +
"Do NOT use for GET or DELETE, use queryParams instead.",
),
- selectedPaymentOption: PaymentOptionSchema.describe("The payment option to use for this request"),
+ selectedPaymentOption: PaymentOptionSchema.describe(
+ "The payment option to use for this request",
+ ),
})
.strip()
.describe("Instructions for retrying a request with x402 payment after receiving a 402 response");
diff --git a/typescript/agentkit/src/action-providers/x402/utils.ts b/typescript/agentkit/src/action-providers/x402/utils.ts
index b3d032c3f..42981d696 100644
--- a/typescript/agentkit/src/action-providers/x402/utils.ts
+++ b/typescript/agentkit/src/action-providers/x402/utils.ts
@@ -1,7 +1,7 @@
import { Network } from "../../network";
import { getTokenDetails } from "../erc20/utils";
import { TOKEN_ADDRESSES_BY_SYMBOLS } from "../erc20/constants";
-import { formatUnits, parseUnits, LocalAccount } from "viem";
+import { formatUnits, parseUnits } from "viem";
import { EvmWalletProvider, SvmWalletProvider, WalletProvider } from "../../wallet-providers";
import {
SOLANA_USDC_ADDRESSES,
@@ -327,7 +327,8 @@ export async function formatSimplifiedResources(
let price = "Unknown";
// Get the amount (supports both v1 and v2 formats)
- const amountStr = matchingOption.maxAmountRequired ?? matchingOption.amount ?? matchingOption.price;
+ const amountStr =
+ matchingOption.maxAmountRequired ?? matchingOption.amount ?? matchingOption.price;
if (amountStr && matchingOption.asset) {
price = await formatPaymentOption(
{
@@ -549,23 +550,23 @@ export async function convertWholeUnitsToAtomic(
return parseUnits(wholeUnits.toString(), 18).toString();
}
- /**
- * Builds a URL with query parameters appended.
- *
- * @param baseUrl - The base URL
- * @param queryParams - Optional query parameters to append
- * @returns URL string with query parameters
- */
- export function buildUrlWithParams(
- baseUrl: string,
- queryParams?: Record | null,
- ): string {
- if (!queryParams || Object.keys(queryParams).length === 0) {
- return baseUrl;
- }
- const url = new URL(baseUrl);
- Object.entries(queryParams).forEach(([key, value]) => {
- url.searchParams.append(key, value);
- });
- return url.toString();
- }
\ No newline at end of file
+/**
+ * Builds a URL with query parameters appended.
+ *
+ * @param baseUrl - The base URL
+ * @param queryParams - Optional query parameters to append
+ * @returns URL string with query parameters
+ */
+export function buildUrlWithParams(
+ baseUrl: string,
+ queryParams?: Record | null,
+): string {
+ if (!queryParams || Object.keys(queryParams).length === 0) {
+ return baseUrl;
+ }
+ const url = new URL(baseUrl);
+ Object.entries(queryParams).forEach(([key, value]) => {
+ url.searchParams.append(key, value);
+ });
+ return url.toString();
+}
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
index e3a8a4ec5..828fcb9a0 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
@@ -1,95 +1,67 @@
import { X402ActionProvider } from "./x402ActionProvider";
import { EvmWalletProvider } from "../../wallet-providers";
import { Network } from "../../network";
-import { AxiosError, AxiosResponse, AxiosRequestConfig, AxiosInstance } from "axios";
-import axios from "axios";
-import * as x402axios from "x402-axios";
-import * as x402Verify from "x402/verify";
-import * as utils from "./utils";
+import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
+import { registerExactEvmScheme } from "@x402/evm/exact/client";
+import { registerExactSvmScheme } from "@x402/svm/exact/client";
-// Mock external facilitator dependency
-jest.mock("@coinbase/x402", () => ({
- facilitator: {},
-}));
+import * as utils from "./utils";
+import { resolveFacilitatorUrl } from "./schemas";
-// Mock modules
-jest.mock("axios");
-jest.mock("x402-axios");
-jest.mock("x402/verify");
+// Mock external modules
+jest.mock("@x402/fetch");
+jest.mock("@x402/evm/exact/client");
+jest.mock("@x402/svm/exact/client");
jest.mock("./utils");
+jest.mock("./schemas", () => ({
+ ...jest.requireActual("./schemas"),
+ resolveFacilitatorUrl: jest.fn(),
+}));
// Create mock functions
-const mockRequest = jest.fn();
-
-// Create a complete mock axios instance
-const mockAxiosInstance = {
- request: mockRequest,
- get: jest.fn(),
- delete: jest.fn(),
- head: jest.fn(),
- options: jest.fn(),
- post: jest.fn(),
- put: jest.fn(),
- patch: jest.fn(),
- getUri: jest.fn(),
- defaults: {},
- interceptors: {
- request: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() },
- response: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() },
- },
-} as unknown as AxiosInstance;
-
-// Create a complete mock axios static
-const mockAxios = {
- create: jest.fn().mockReturnValue(mockAxiosInstance),
- request: mockRequest,
- get: jest.fn(),
- delete: jest.fn(),
- head: jest.fn(),
- options: jest.fn(),
- post: jest.fn(),
- put: jest.fn(),
- patch: jest.fn(),
- all: jest.fn(),
- spread: jest.fn(),
- isAxiosError: jest.fn(),
- isCancel: jest.fn(),
- CancelToken: {
- source: jest.fn(),
- },
- VERSION: "1.x",
-} as unknown as jest.Mocked;
-
-const mockWithPaymentInterceptor = jest.fn().mockReturnValue(mockAxiosInstance);
-const mockDecodeXPaymentResponse = jest.fn();
-const mockUseFacilitator = jest.fn();
-const mockIsUsdcAsset = jest.fn();
-const mockConvertWholeUnitsToAtomic = jest.fn();
-const mockFormatPaymentOption = jest.fn();
-const mockGetX402Network = jest.fn();
-
-// Override the mocked modules
-(axios as jest.Mocked).create = mockAxios.create;
-(axios as jest.Mocked).request = mockRequest;
-(axios as jest.Mocked).isAxiosError = mockAxios.isAxiosError;
+const mockFetch = jest.fn();
+const mockFetchWithPayment = jest.fn();
-// Mock x402-axios functions
-jest.mocked(x402axios.withPaymentInterceptor).mockImplementation(mockWithPaymentInterceptor);
-jest.mocked(x402axios.decodeXPaymentResponse).mockImplementation(mockDecodeXPaymentResponse);
-jest.mocked(x402Verify.useFacilitator).mockImplementation(mockUseFacilitator);
+// Mock x402 client
+const mockX402Client = {
+ registerScheme: jest.fn(),
+};
// Mock utils functions
-jest.mocked(utils.isUsdcAsset).mockImplementation(mockIsUsdcAsset);
-jest.mocked(utils.convertWholeUnitsToAtomic).mockImplementation(mockConvertWholeUnitsToAtomic);
+const mockGetX402Networks = jest.fn();
+const mockHandleHttpError = jest.fn();
+const mockFormatPaymentOption = jest.fn();
+const mockFetchAllDiscoveryResources = jest.fn();
+const mockFilterByNetwork = jest.fn();
+const mockFilterByDescription = jest.fn();
+const mockFilterByX402Version = jest.fn();
+const mockFilterByKeyword = jest.fn();
+const mockFilterByMaxPrice = jest.fn();
+const mockFormatSimplifiedResources = jest.fn();
+const mockBuildUrlWithParams = jest.fn();
+const mockResolveFacilitatorUrl = jest.fn();
+
+// Setup mocks
+jest.mocked(x402Client).mockImplementation(() => mockX402Client as unknown as InstanceType);
+jest.mocked(wrapFetchWithPayment).mockReturnValue(mockFetchWithPayment);
+jest.mocked(registerExactEvmScheme).mockImplementation(() => mockX402Client as unknown as InstanceType);
+jest.mocked(registerExactSvmScheme).mockImplementation(() => mockX402Client as unknown as InstanceType);
+
+jest.mocked(utils.getX402Networks).mockImplementation(mockGetX402Networks);
+jest.mocked(utils.handleHttpError).mockImplementation(mockHandleHttpError);
jest.mocked(utils.formatPaymentOption).mockImplementation(mockFormatPaymentOption);
-jest.mocked(utils.getX402Network).mockImplementation(mockGetX402Network);
-jest.mocked(utils.handleHttpError).mockImplementation((error, url) => {
- return JSON.stringify({
- error: true,
- message: error instanceof Error ? error.message : "Network error",
- url: url,
- });
-});
+jest.mocked(utils.fetchAllDiscoveryResources).mockImplementation(mockFetchAllDiscoveryResources);
+jest.mocked(utils.filterByNetwork).mockImplementation(mockFilterByNetwork);
+jest.mocked(utils.filterByDescription).mockImplementation(mockFilterByDescription);
+jest.mocked(utils.filterByX402Version).mockImplementation(mockFilterByX402Version);
+jest.mocked(utils.filterByKeyword).mockImplementation(mockFilterByKeyword);
+jest.mocked(utils.filterByMaxPrice).mockImplementation(mockFilterByMaxPrice);
+jest.mocked(utils.formatSimplifiedResources).mockImplementation(mockFormatSimplifiedResources);
+jest.mocked(utils.buildUrlWithParams).mockImplementation(mockBuildUrlWithParams);
+jest.mocked(resolveFacilitatorUrl).mockImplementation(mockResolveFacilitatorUrl);
+
+// Mock global fetch
+global.fetch = mockFetch;
// Mock wallet provider
const makeMockWalletProvider = (networkId: string) => {
@@ -101,38 +73,51 @@ const makeMockWalletProvider = (networkId: string) => {
// Sample responses based on real examples
const MOCK_PAYMENT_INFO_RESPONSE = {
- paymentRequired: true,
- url: "https://www.x402.org/protected",
- status: 402,
- data: {
- x402Version: 1,
- error: "X-PAYMENT header is required",
- accepts: [
- {
- scheme: "exact",
- network: "base-sepolia",
- maxAmountRequired: "10000",
- resource: "https://www.x402.org/protected",
- description: "Access to protected content",
- mimeType: "application/json",
- payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
- maxTimeoutSeconds: 300,
- asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
- extra: {
- name: "USDC",
- version: "2",
- },
+ x402Version: 1,
+ error: "X-PAYMENT header is required",
+ accepts: [
+ {
+ scheme: "exact",
+ network: "base-sepolia",
+ maxAmountRequired: "10000",
+ resource: "https://www.x402.org/protected",
+ description: "Access to protected content",
+ mimeType: "application/json",
+ payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
+ maxTimeoutSeconds: 300,
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+ extra: {
+ name: "USDC",
+ version: "2",
},
- ],
- },
+ },
+ ],
+};
+
+const MOCK_PAYMENT_PROOF = {
+ transaction: "0xcbc385789d3744b52af5106c32809534f64adcbe097e050ec03d6b53fed5d305",
+ network: "base-sepolia",
+ payer: "0xa8c1a5D3C372C65c04f91f87a43F549619A9483f",
};
-const MOCK_PAYMENT_RESPONSE = {
- success: true,
- transaction:
- "0xcbc385789d3744b52af5106c32809534f64adcbe097e050ec03d6b53fed5d305" as `0x${string}`,
- network: "base-sepolia" as const,
- payer: "0xa8c1a5D3C372C65c04f91f87a43F549619A9483f" as `0x${string}`,
+// Helper to create mock Response
+const createMockResponse = (options: {
+ status: number;
+ statusText?: string;
+ data?: unknown;
+ headers?: Record;
+}): Response => {
+ const headersMap = new Map(Object.entries(options.headers ?? {}));
+ return {
+ status: options.status,
+ statusText: options.statusText ?? "OK",
+ ok: options.status >= 200 && options.status < 300,
+ headers: {
+ get: (name: string) => headersMap.get(name.toLowerCase()) ?? null,
+ },
+ json: jest.fn().mockResolvedValue(options.data),
+ text: jest.fn().mockResolvedValue(JSON.stringify(options.data)),
+ } as unknown as Response;
};
describe("X402ActionProvider", () => {
@@ -142,26 +127,18 @@ describe("X402ActionProvider", () => {
provider = new X402ActionProvider();
jest.clearAllMocks();
- // Setup mocks
- mockAxios.create.mockReturnValue(mockAxiosInstance);
- mockWithPaymentInterceptor.mockReturnValue(mockAxiosInstance);
-
- // Setup axios.isAxiosError mock
- jest
- .mocked(axios.isAxiosError)
- .mockImplementation((error: unknown): boolean =>
- Boolean(
- error &&
- typeof error === "object" &&
- ("isAxiosError" in error || "response" in error || "request" in error),
- ),
- );
-
- // Reset all utility mocks to default behavior
- mockGetX402Network.mockImplementation(network => network.networkId);
- mockIsUsdcAsset.mockReturnValue(false);
- mockConvertWholeUnitsToAtomic.mockResolvedValue("100000");
+ // Setup default mock behaviors
+ mockGetX402Networks.mockImplementation(network => [network.networkId]);
+ mockBuildUrlWithParams.mockImplementation(url => url);
+ mockHandleHttpError.mockImplementation((error, url) => {
+ return JSON.stringify({
+ error: true,
+ message: error instanceof Error ? error.message : "Network error",
+ url: url,
+ });
+ });
mockFormatPaymentOption.mockResolvedValue("mocked payment option");
+ mockResolveFacilitatorUrl.mockReturnValue("https://api.cdp.coinbase.com/platform/v2/x402");
});
afterEach(() => {
@@ -197,12 +174,13 @@ describe("X402ActionProvider", () => {
describe("makeHttpRequest", () => {
it("should handle successful non-payment requests", async () => {
- mockRequest.mockResolvedValue({
- status: 200,
- data: { message: "Success" },
- headers: {},
- config: {} as AxiosRequestConfig,
- } as AxiosResponse);
+ mockFetch.mockResolvedValue(
+ createMockResponse({
+ status: 200,
+ data: { message: "Success" },
+ headers: { "content-type": "application/json" },
+ }),
+ );
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/free",
@@ -216,15 +194,16 @@ describe("X402ActionProvider", () => {
});
it("should handle 402 responses with payment options", async () => {
- mockGetX402Network.mockReturnValue("base-sepolia");
+ mockGetX402Networks.mockReturnValue(["base-sepolia"]);
mockFormatPaymentOption.mockResolvedValue("10000 USDC on base-sepolia network");
- mockRequest.mockResolvedValue({
- status: 402,
- data: MOCK_PAYMENT_INFO_RESPONSE.data,
- headers: {},
- config: {} as AxiosRequestConfig,
- } as AxiosResponse);
+ mockFetch.mockResolvedValue(
+ createMockResponse({
+ status: 402,
+ data: MOCK_PAYMENT_INFO_RESPONSE,
+ headers: { "content-type": "application/json" },
+ }),
+ );
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
@@ -233,18 +212,13 @@ describe("X402ActionProvider", () => {
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("error_402_payment_required");
- expect(parsedResult.acceptablePaymentOptions).toEqual(
- MOCK_PAYMENT_INFO_RESPONSE.data.accepts,
- );
+ expect(parsedResult.acceptablePaymentOptions).toEqual(MOCK_PAYMENT_INFO_RESPONSE.accepts);
expect(parsedResult.nextSteps).toBeDefined();
});
it("should handle network errors", async () => {
- const error = new Error("Network error") as AxiosError;
- error.isAxiosError = true;
- error.request = {};
-
- mockRequest.mockRejectedValue(error);
+ const error = new TypeError("fetch failed");
+ mockFetch.mockRejectedValue(error);
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/endpoint",
@@ -253,147 +227,143 @@ describe("X402ActionProvider", () => {
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
- expect(parsedResult.message).toContain("Network error");
+ expect(parsedResult.message).toBeDefined();
});
});
- describe("listX402Services", () => {
+ describe("discoverX402Services", () => {
it("should list services without filters", async () => {
- const mockList = jest.fn().mockResolvedValue({
- items: [
- {
- resource: "https://example.com/service1",
- metadata: { category: "test" },
- accepts: [
- {
- asset: "0xUSDC",
- maxAmountRequired: "90000",
- network: "base-sepolia",
- scheme: "exact",
- description: "Test service 1",
- outputSchema: { type: "object" },
- extra: { name: "USDC" },
- },
- ],
- },
- ],
- });
+ const mockResources = [
+ {
+ resource: "https://example.com/service1",
+ x402Version: 1,
+ accepts: [
+ {
+ asset: "0xUSDC",
+ maxAmountRequired: "90000",
+ network: "base-sepolia",
+ scheme: "exact",
+ description: "Test service 1",
+ },
+ ],
+ },
+ ];
- mockUseFacilitator.mockReturnValue({ list: mockList });
- mockGetX402Network.mockReturnValue("base-sepolia");
+ const mockSimplified = [
+ {
+ url: "https://example.com/service1",
+ price: "0.09 USDC on base-sepolia",
+ description: "Test service 1",
+ },
+ ];
- const result = await provider.discoverX402Services(
- makeMockWalletProvider("base-sepolia"),
- {},
- );
+ mockFetchAllDiscoveryResources.mockResolvedValue(mockResources);
+ mockFilterByNetwork.mockReturnValue(mockResources);
+ mockFilterByDescription.mockReturnValue(mockResources);
+ mockFilterByX402Version.mockReturnValue(mockResources);
+ mockFormatSimplifiedResources.mockResolvedValue(mockSimplified);
+ mockGetX402Networks.mockReturnValue(["base-sepolia"]);
+
+ const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
+ facilitator: "cdp",
+ x402Versions: [1, 2],
+ });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.total).toBe(1);
expect(parsed.returned).toBe(1);
- expect(parsed.items.length).toBe(1);
+ expect(parsed.services.length).toBe(1);
});
- it("should filter services by asset and maxPrice", async () => {
- const mockList = jest.fn().mockResolvedValue({
- items: [
- {
- resource: "https://example.com/service1",
- metadata: { category: "test" },
- accepts: [
- {
- asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia
- maxAmountRequired: "90000", // 0.09 USDC (should pass 0.1 filter)
- network: "base-sepolia",
- scheme: "exact",
- description: "Test service 1",
- outputSchema: { type: "object" },
- extra: { name: "USDC" },
- },
- ],
- },
- {
- resource: "https://example.com/service2",
- metadata: { category: "test" },
- accepts: [
- {
- asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia
- maxAmountRequired: "150000", // 0.15 USDC (should fail 0.1 filter)
- network: "base-sepolia",
- scheme: "exact",
- description: "Test service 2",
- outputSchema: { type: "object" },
- extra: { name: "USDC" },
- },
- ],
- },
- {
- resource: "https://example.com/service3",
- metadata: { category: "test" },
- accepts: [
- {
- asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia
- maxAmountRequired: "50000", // 0.05 USDC (should pass 0.1 filter)
- network: "base-sepolia",
- scheme: "exact",
- description: "Test service 3",
- outputSchema: { type: "object" },
- extra: { name: "USDC" },
- },
- ],
- },
- ],
- });
+ it("should filter services by maxPrice", async () => {
+ const mockResources = [
+ {
+ resource: "https://example.com/service1",
+ x402Version: 1,
+ accepts: [
+ {
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+ maxAmountRequired: "90000",
+ network: "base-sepolia",
+ scheme: "exact",
+ description: "Test service 1",
+ },
+ ],
+ },
+ {
+ resource: "https://example.com/service2",
+ x402Version: 1,
+ accepts: [
+ {
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+ maxAmountRequired: "150000",
+ network: "base-sepolia",
+ scheme: "exact",
+ description: "Test service 2",
+ },
+ ],
+ },
+ ];
- mockUseFacilitator.mockReturnValue({ list: mockList });
+ const filteredResources = [mockResources[0]];
+ const mockSimplified = [
+ {
+ url: "https://example.com/service1",
+ price: "0.09 USDC on base-sepolia",
+ description: "Test service 1",
+ },
+ ];
- // Mock the utility functions for this test
- mockGetX402Network.mockReturnValue("base-sepolia");
- mockIsUsdcAsset.mockReturnValue(true); // All assets are USDC
- mockConvertWholeUnitsToAtomic
- .mockResolvedValueOnce("100000") // 0.1 USDC in atomic units
- .mockResolvedValueOnce("100000")
- .mockResolvedValueOnce("100000");
- mockFormatPaymentOption.mockResolvedValue("formatted payment option");
+ mockFetchAllDiscoveryResources.mockResolvedValue(mockResources);
+ mockFilterByNetwork.mockReturnValue(mockResources);
+ mockFilterByDescription.mockReturnValue(mockResources);
+ mockFilterByX402Version.mockReturnValue(mockResources);
+ mockFilterByMaxPrice.mockResolvedValue(filteredResources);
+ mockFormatSimplifiedResources.mockResolvedValue(mockSimplified);
+ mockGetX402Networks.mockReturnValue(["base-sepolia"]);
const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
+ facilitator: "cdp",
maxUsdcPrice: 0.1,
+ x402Versions: [1, 2],
});
+
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
- expect(parsed.returned).toBe(2);
- expect(parsed.items.map(item => item.resource)).toEqual(
- expect.arrayContaining(["https://example.com/service1", "https://example.com/service3"]),
- );
+ expect(parsed.returned).toBe(1);
+ expect(parsed.services[0].url).toBe("https://example.com/service1");
});
- it("should handle errors from facilitator", async () => {
- const mockList = jest.fn().mockRejectedValue(new Error("boom"));
- mockUseFacilitator.mockReturnValue({ list: mockList });
+ it("should handle errors from discovery", async () => {
+ mockFetchAllDiscoveryResources.mockRejectedValue(new Error("boom"));
- const result = await provider.discoverX402Services(
- makeMockWalletProvider("base-sepolia"),
- {},
- );
+ const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
+ facilitator: "cdp",
+ x402Versions: [1, 2],
+ });
const parsed = JSON.parse(result);
expect(parsed.error).toBe(true);
expect(parsed.message).toContain("Failed to list x402 services");
});
});
- describe("retryHttpRequestWithX402", () => {
+ describe("retryWithX402", () => {
it("should successfully retry with payment", async () => {
- mockDecodeXPaymentResponse.mockReturnValue(MOCK_PAYMENT_RESPONSE);
- mockGetX402Network.mockReturnValue("base-sepolia");
-
- mockRequest.mockResolvedValue({
- status: 200,
- statusText: "OK",
- data: { message: "Paid content" },
- headers: {
- "x-payment-response": "encoded-payment-data",
- },
- config: {} as AxiosRequestConfig,
- } as AxiosResponse);
+ mockGetX402Networks.mockReturnValue(["base-sepolia"]);
+
+ // Encode the payment proof as base64
+ const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF));
+
+ mockFetchWithPayment.mockResolvedValue(
+ createMockResponse({
+ status: 200,
+ data: { message: "Paid content" },
+ headers: {
+ "content-type": "application/json",
+ "x-payment-response": encodedPaymentProof,
+ },
+ }),
+ );
const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
@@ -406,29 +376,17 @@ describe("X402ActionProvider", () => {
},
});
- // Update expectation to accept the payment selector function
- expect(mockWithPaymentInterceptor).toHaveBeenCalledWith(
- mockAxiosInstance,
- "mock-signer",
- expect.any(Function),
- );
+ expect(wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client);
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("success");
- expect(parsedResult.details.paymentProof).toEqual({
- transaction: MOCK_PAYMENT_RESPONSE.transaction,
- network: MOCK_PAYMENT_RESPONSE.network,
- payer: MOCK_PAYMENT_RESPONSE.payer,
- });
+ expect(parsedResult.details.paymentProof).toEqual(MOCK_PAYMENT_PROOF);
});
it("should handle network errors during payment", async () => {
- const error = new Error("Network error") as AxiosError;
- error.isAxiosError = true;
- error.request = {};
-
- mockRequest.mockRejectedValue(error);
- mockGetX402Network.mockReturnValue("base-sepolia");
+ const error = new TypeError("fetch failed");
+ mockGetX402Networks.mockReturnValue(["base-sepolia"]);
+ mockFetchWithPayment.mockRejectedValue(error);
const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
@@ -443,23 +401,24 @@ describe("X402ActionProvider", () => {
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
- expect(parsedResult.message).toContain("Network error");
});
});
describe("makeHttpRequestWithX402", () => {
it("should handle successful direct payment requests", async () => {
- mockDecodeXPaymentResponse.mockReturnValue(MOCK_PAYMENT_RESPONSE);
-
- mockRequest.mockResolvedValue({
- status: 200,
- statusText: "OK",
- data: { message: "Paid content" },
- headers: {
- "x-payment-response": "encoded-payment-data",
- },
- config: {} as AxiosRequestConfig,
- } as AxiosResponse);
+ // Encode the payment proof as base64
+ const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF));
+
+ mockFetchWithPayment.mockResolvedValue(
+ createMockResponse({
+ status: 200,
+ data: { message: "Paid content" },
+ headers: {
+ "content-type": "application/json",
+ "x-payment-response": encodedPaymentProof,
+ },
+ }),
+ );
const result = await provider.makeHttpRequestWithX402(
makeMockWalletProvider("base-sepolia"),
@@ -469,28 +428,22 @@ describe("X402ActionProvider", () => {
},
);
- expect(mockWithPaymentInterceptor).toHaveBeenCalledWith(mockAxiosInstance, "mock-signer");
+ expect(wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client);
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(true);
expect(parsedResult.data).toEqual({ message: "Paid content" });
- expect(parsedResult.paymentProof).toEqual({
- transaction: MOCK_PAYMENT_RESPONSE.transaction,
- network: MOCK_PAYMENT_RESPONSE.network,
- payer: MOCK_PAYMENT_RESPONSE.payer,
- });
+ expect(parsedResult.paymentProof).toEqual(MOCK_PAYMENT_PROOF);
});
it("should handle successful non-payment requests", async () => {
- mockDecodeXPaymentResponse.mockReturnValue(null); // No payment made
-
- mockRequest.mockResolvedValue({
- status: 200,
- statusText: "OK",
- data: { message: "Free content" },
- headers: {},
- config: {} as AxiosRequestConfig,
- } as AxiosResponse);
+ mockFetchWithPayment.mockResolvedValue(
+ createMockResponse({
+ status: 200,
+ data: { message: "Free content" },
+ headers: { "content-type": "application/json" },
+ }),
+ );
const result = await provider.makeHttpRequestWithX402(
makeMockWalletProvider("base-sepolia"),
@@ -507,11 +460,8 @@ describe("X402ActionProvider", () => {
});
it("should handle network errors", async () => {
- const error = new Error("Network error") as AxiosError;
- error.isAxiosError = true;
- error.request = {};
-
- mockRequest.mockRejectedValue(error);
+ const error = new TypeError("fetch failed");
+ mockFetchWithPayment.mockRejectedValue(error);
const result = await provider.makeHttpRequestWithX402(
makeMockWalletProvider("base-sepolia"),
@@ -523,7 +473,6 @@ describe("X402ActionProvider", () => {
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
- expect(parsedResult.message).toContain("Network error");
});
});
});
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
index 3f7cbfc29..5ed22c17d 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts
@@ -40,26 +40,6 @@ export class X402ActionProvider extends ActionProvider {
super("x402", []);
}
- /**
- * Creates an x402 client configured for the given wallet provider.
- *
- * @param walletProvider - The wallet provider to configure the client for
- * @returns Configured x402Client
- */
- private async createX402Client(walletProvider: WalletProvider): Promise {
- const client = new x402Client();
-
- if (walletProvider instanceof EvmWalletProvider) {
- const signer = walletProvider.toSigner();
- registerExactEvmScheme(client, { signer });
- } else if (walletProvider instanceof SvmWalletProvider) {
- const signer = await walletProvider.toSigner();
- registerExactSvmScheme(client, { signer });
- }
-
- return client;
- }
-
/**
* Discovers available x402 services with optional filtering.
*
@@ -184,7 +164,7 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
});
// Retry with other http method for 404 status code
- if (response.status === 404){
+ if (response.status === 404) {
method = method === "GET" ? "POST" : "GET";
canHaveBody = ["POST", "PUT", "PATCH"].includes(method);
response = await fetch(finalUrl, {
@@ -212,12 +192,12 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
// Handle 402 Payment Required
// v2 sends requirements in PAYMENT-REQUIRED header; v1 sends in body
const walletNetworks = getX402Networks(walletProvider.getNetwork());
-
- let acceptsArray: Array<{
- scheme?: string;
- network: string;
- asset: string;
- maxAmountRequired?: string;
+
+ let acceptsArray: Array<{
+ scheme?: string;
+ network: string;
+ asset: string;
+ maxAmountRequired?: string;
amount?: string;
payTo?: string;
}> = [];
@@ -241,7 +221,7 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
acceptsArray = (paymentData.accepts as typeof acceptsArray) ?? [];
}
- const availableNetworks = acceptsArray.map((option) => option.network);
+ const availableNetworks = acceptsArray.map(option => option.network);
const hasMatchingNetwork = availableNetworks.some((net: string) =>
walletNetworks.includes(net),
);
@@ -249,11 +229,11 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402
let paymentOptionsText = `The wallet networks ${walletNetworks.join(", ")} do not match any available payment options (${availableNetworks.join(", ")}).`;
if (hasMatchingNetwork) {
- const matchingOptions = acceptsArray.filter(
- (option) => walletNetworks.includes(option.network),
+ const matchingOptions = acceptsArray.filter(option =>
+ walletNetworks.includes(option.network),
);
const formattedOptions = await Promise.all(
- matchingOptions.map((option) =>
+ matchingOptions.map(option =>
formatPaymentOption(
{
asset: option.asset,
@@ -320,9 +300,8 @@ DO NOT use this action directly without first trying make_http_request!`,
args: z.infer,
): Promise {
try {
-
console.log("args", args);
-
+
// Check network compatibility before attempting payment
const walletNetworks = getX402Networks(walletProvider.getNetwork());
const selectedNetwork = args.selectedPaymentOption.network;
@@ -558,6 +537,35 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
}
}
+ /**
+ * Checks if the action provider supports the given network.
+ *
+ * @param network - The network to check support for
+ * @returns True if the network is supported, false otherwise
+ */
+ supportsNetwork = (network: Network) =>
+ (SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!);
+
+ /**
+ * Creates an x402 client configured for the given wallet provider.
+ *
+ * @param walletProvider - The wallet provider to configure the client for
+ * @returns Configured x402Client
+ */
+ private async createX402Client(walletProvider: WalletProvider): Promise {
+ const client = new x402Client();
+
+ if (walletProvider instanceof EvmWalletProvider) {
+ const signer = walletProvider.toSigner();
+ registerExactEvmScheme(client, { signer });
+ } else if (walletProvider instanceof SvmWalletProvider) {
+ const signer = await walletProvider.toSigner();
+ registerExactSvmScheme(client, { signer });
+ }
+
+ return client;
+ }
+
/**
* Parses response data based on content type.
*
@@ -573,15 +581,6 @@ Unless specifically instructed otherwise, prefer the two-step approach with make
return response.text();
}
-
- /**
- * Checks if the action provider supports the given network.
- *
- * @param network - The network to check support for
- * @returns True if the network is supported, false otherwise
- */
- supportsNetwork = (network: Network) =>
- (SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!);
}
export const x402ActionProvider = () => new X402ActionProvider();
From 0211146ba3ce88caee4a8e48d1d349e098796602 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 19 Dec 2025 16:55:46 +0100
Subject: [PATCH 6/6] add changelog and update readme
---
typescript/.changeset/funny-masks-lie.md | 5 +++++
.../agentkit/src/action-providers/x402/README.md | 7 +++++--
.../action-providers/x402/x402ActionProvider.test.ts | 12 +++++++++---
3 files changed, 19 insertions(+), 5 deletions(-)
create mode 100644 typescript/.changeset/funny-masks-lie.md
diff --git a/typescript/.changeset/funny-masks-lie.md b/typescript/.changeset/funny-masks-lie.md
new file mode 100644
index 000000000..8a6181345
--- /dev/null
+++ b/typescript/.changeset/funny-masks-lie.md
@@ -0,0 +1,5 @@
+---
+"@coinbase/agentkit": patch
+---
+
+Updated x402 actions to v2 and improved discovery and query/body parameter handling
diff --git a/typescript/agentkit/src/action-providers/x402/README.md b/typescript/agentkit/src/action-providers/x402/README.md
index 7087d5cca..d44ae587f 100644
--- a/typescript/agentkit/src/action-providers/x402/README.md
+++ b/typescript/agentkit/src/action-providers/x402/README.md
@@ -8,6 +8,7 @@ This directory contains the **X402ActionProvider** implementation, which provide
x402/
├── x402ActionProvider.ts # Main provider with x402 payment functionality
├── schemas.ts # x402 action schemas
+├── constants.ts # Network mappings and type definitions
├── index.ts # Main exports
├── utils.ts # Utility functions
└── README.md # This file
@@ -114,8 +115,10 @@ Fetches all available services from the x402 Bazaar with full pagination support
```typescript
{
- discoveryUrl: "https://...", // Optional, defaults to CDP discovery endpoint
- maxUsdcPrice: 0.1 // Optional, filter by max price in USDC
+ facilitator: "cdp", // Optional: 'cdp', 'payai', or custom URL
+ maxUsdcPrice: 0.1, // Optional, filter by max price in USDC
+ keyword: "weather", // Optional, filter by description/URL keyword
+ x402Versions: [1, 2] // Optional, filter by protocol version
}
```
diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
index 828fcb9a0..e3af83218 100644
--- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
+++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts
@@ -42,10 +42,16 @@ const mockBuildUrlWithParams = jest.fn();
const mockResolveFacilitatorUrl = jest.fn();
// Setup mocks
-jest.mocked(x402Client).mockImplementation(() => mockX402Client as unknown as InstanceType);
+jest
+ .mocked(x402Client)
+ .mockImplementation(() => mockX402Client as unknown as InstanceType);
jest.mocked(wrapFetchWithPayment).mockReturnValue(mockFetchWithPayment);
-jest.mocked(registerExactEvmScheme).mockImplementation(() => mockX402Client as unknown as InstanceType);
-jest.mocked(registerExactSvmScheme).mockImplementation(() => mockX402Client as unknown as InstanceType);
+jest
+ .mocked(registerExactEvmScheme)
+ .mockImplementation(() => mockX402Client as unknown as InstanceType);
+jest
+ .mocked(registerExactSvmScheme)
+ .mockImplementation(() => mockX402Client as unknown as InstanceType);
jest.mocked(utils.getX402Networks).mockImplementation(mockGetX402Networks);
jest.mocked(utils.handleHttpError).mockImplementation(mockHandleHttpError);