Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/ready-eggs-bake.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added action to discover x402 services and fixed x402 request for smart wallets
8 changes: 8 additions & 0 deletions typescript/agentkit/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -541,6 +541,10 @@ const agent = createReactAgent({
<details>
<summary><strong>x402</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>discover_x402_services</code></td>
<td width="768">Discover available x402 services with optional filtering by maximum USDC price.</td>
</tr>
<tr>
<td width="200"><code>make_http_request</code></td>
<td width="768">Makes a basic HTTP request to an API endpoint. If the endpoint requires payment (returns 402),
Expand All@@ -553,6 +557,10 @@ it will return payment details that can be used on retry.</td>
<tr>
<td width="200"><code>make_http_request_with_x402</code></td>
<td width="768">Combines make_http_request and retry_http_request_with_x402 into a single step.</td>
</tr>
</table>
</details>
<details>
<summary><strong>ZeroX</strong></summary>
<table width="100%">
<tr>
Expand Down
7 changes: 4 additions & 3 deletions typescript/agentkit/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,9 @@
"dependencies": {
"@across-protocol/app-sdk": "^0.2.0",
"@alloralabs/allora-sdk": "^0.1.0",
"@coinbase/cdp-sdk": "^1.36.1",
"@coinbase/cdp-sdk": "^1.38.0",
"@coinbase/coinbase-sdk": "^0.20.0",
"@coinbase/x402": "^0.6.3",
"@jup-ag/api": "^6.0.39",
"@privy-io/public-api": "2.18.5",
"@privy-io/server-auth": "1.18.4",
Expand All@@ -64,8 +65,8 @@
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.22.16",
"x402": "^0.4.1",
"x402-axios": "^0.3.3",
"x402": "^0.6.0",
"x402-axios": "^0.6.0",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
29 changes: 23 additions & 6 deletions typescript/agentkit/src/action-providers/x402/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ x402/
├── x402ActionProvider.ts # Main provider with x402 payment functionality
├── schemas.ts # x402 action schemas
├── index.ts # Main exports
├── utils.ts # Utility functions
└── README.md # This file
```

Expand All@@ -22,6 +23,7 @@ x402/
### Alternative Action

- `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)

## Overview

Expand DownExpand Up@@ -70,15 +72,10 @@ Retries request with payment after 402:
method: "GET", // Optional, defaults to GET
headers: { "Accept": "..." }, // Optional
body: { ... }, // Optional
paymentOption: { // Payment details from 402 response
selectedPaymentOption: { // Payment details from 402 response
scheme: "exact",
network: "base-sepolia",
maxAmountRequired: "1000",
resource: "https://api.example.com/data",
description: "Access to data",
mimeType: "application/json",
payTo: "0x...",
maxTimeoutSeconds: 300,
asset: "0x..."
}
}
Expand All@@ -97,6 +94,26 @@ 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:

```typescript
{
maxUsdcPrice: 0.1 // optional (e.g., 0.1 for $0.10 USDC)
}
```

Example filtering for USDC services under $0.10:

```ts
const maxUsdcPrice = 0.1;

const services = await discover_x402_services({ maxUsdcPrice });


```

## Response Format

Successful responses include payment proof when payment was made:
Expand Down
13 changes: 13 additions & 0 deletions typescript/agentkit/src/action-providers/x402/schemas.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
import { z } from "zod";

// Schema for listing x402 services
export const ListX402ServicesSchema = z
.object({
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.",
),
})
.strip()
.describe("Parameters for listing x402 services with optional filtering");

// Schema for initial HTTP request
export const HttpRequestSchema = z
.object({
Expand Down
197 changes: 197 additions & 0 deletions typescript/agentkit/src/action-providers/x402/utils.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 { EvmWalletProvider } from "../../wallet-providers";

/**
* Supported network types for x402 protocol
*/
export type X402Network = "base" | "base-sepolia" | "solana" | "solana-devnet";

/**
* 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;
}
}

/**
* Helper method to handle HTTP errors consistently.
*
* @param error - The axios 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) {
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,
suggestion: "Check if the URL is correct and the API is available.",
},
null,
2,
);
}

if (error.request) {
return JSON.stringify(
{
error: true,
message: `Network error when accessing ${url}`,
details: error.message,
suggestion: "Check your internet connection and verify the API endpoint is accessible.",
},
null,
2,
);
}

return JSON.stringify(
{
error: true,
message: `Error making request to ${url}`,
details: error.message,
suggestion: "Please check the request parameters and try again.",
},
null,
2,
);
}

/**
* Formats a payment option into a human-readable string.
*
* @param option - The payment option to format
* @param option.asset - The asset address or identifier
* @param option.maxAmountRequired - The maximum amount required for the payment
* @param option.network - The network identifier
* @param walletProvider - The wallet provider for token details lookup
* @returns A formatted string like "0.1 USDC on base"
*/
export async function formatPaymentOption(
option: { asset: string; maxAmountRequired: string; network: string },
walletProvider: EvmWalletProvider,
): Promise<string> {
const { asset, maxAmountRequired, network } = option;

// Check if this is an EVM network and we can use ERC20 helpers
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";

if (isEvmNetwork) {
const networkId = walletNetwork.networkId as keyof typeof TOKEN_ADDRESSES_BY_SYMBOLS;
const tokenSymbols = TOKEN_ADDRESSES_BY_SYMBOLS[networkId];

if (tokenSymbols) {
for (const [symbol, address] of Object.entries(tokenSymbols)) {
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`;
}
}
}

// Fall back to getTokenDetails for unknown tokens
try {
const tokenDetails = await getTokenDetails(walletProvider, asset);
if (tokenDetails) {
const formattedAmount = formatUnits(BigInt(maxAmountRequired), tokenDetails.decimals);
return `${formattedAmount} ${tokenDetails.name} on ${network} network`;
}
} catch {
// If we can't get token details, fall back to raw format
}
}

// Fallback to original format for non-EVM networks or when token details can't be fetched
return `${asset} ${maxAmountRequired} on ${network} network`;
}

/**
* Checks if an asset is USDC on any supported network.
*
* @param asset - The asset address or identifier
* @param walletProvider - The wallet provider for network context
* @returns True if the asset is USDC, false otherwise
*/
export function isUsdcAsset(asset: string, walletProvider: EvmWalletProvider): boolean {
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";

if (isEvmNetwork) {
const networkId = walletNetwork.networkId as keyof typeof TOKEN_ADDRESSES_BY_SYMBOLS;
const tokenSymbols = TOKEN_ADDRESSES_BY_SYMBOLS[networkId];

if (tokenSymbols && tokenSymbols.USDC) {
return asset.toLowerCase() === tokenSymbols.USDC.toLowerCase();
}
}

return false;
}

/**
* Converts whole units to atomic units for a given asset.
*
* @param wholeUnits - The amount in whole units (e.g., 0.1 for 0.1 USDC)
* @param asset - The asset address or identifier
* @param walletProvider - The wallet provider for token details lookup
* @returns The amount in atomic units as a string, or null if conversion fails
*/
export async function convertWholeUnitsToAtomic(
wholeUnits: number,
asset: string,
walletProvider: EvmWalletProvider,
): Promise<string | null> {
// Check if this is an EVM network and we can use ERC20 helpers
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";

if (isEvmNetwork) {
const networkId = walletNetwork.networkId as keyof typeof TOKEN_ADDRESSES_BY_SYMBOLS;
const tokenSymbols = TOKEN_ADDRESSES_BY_SYMBOLS[networkId];

if (tokenSymbols) {
for (const [symbol, address] of Object.entries(tokenSymbols)) {
if (asset.toLowerCase() === address.toLowerCase()) {
const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18;
return parseUnits(wholeUnits.toString(), decimals).toString();
}
}
}

// Fall back to getTokenDetails for unknown tokens
try {
const tokenDetails = await getTokenDetails(walletProvider, asset);
if (tokenDetails) {
return parseUnits(wholeUnits.toString(), tokenDetails.decimals).toString();
}
} catch {
// If we can't get token details, fall back to assuming 18 decimals
}
}

// Fallback to 18 decimals for unknown tokens or non-EVM networks
return parseUnits(wholeUnits.toString(), 18).toString();
}
Loading
Loading