diff --git a/typescript/.changeset/whole-times-fetch.md b/typescript/.changeset/whole-times-fetch.md
new file mode 100644
index 000000000..63bb04da8
--- /dev/null
+++ b/typescript/.changeset/whole-times-fetch.md
@@ -0,0 +1,5 @@
+---
+"@coinbase/agentkit": patch
+---
+
+Added ZeroX Action Provider to enable token swaps using the 0x Protocol API
diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md
index 457109e79..1dd2f92a1 100644
--- a/typescript/agentkit/README.md
+++ b/typescript/agentkit/README.md
@@ -524,6 +524,15 @@ it will return payment details that can be used on retry.
make_http_request_with_x402 |
Combines make_http_request and retry_http_request_with_x402 into a single step. |
+ZeroX
+
+
+ get_swap_price_quote_from_0x |
+ Fetches a price quote for swapping between two tokens using the 0x API. |
+
+
+ execute_swap_on_0x |
+ Executes a token swap between two tokens using the 0x API. |
diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts
index 35ff1f726..7ac55703e 100644
--- a/typescript/agentkit/src/action-providers/index.ts
+++ b/typescript/agentkit/src/action-providers/index.ts
@@ -33,4 +33,5 @@ export * from "./vaultsfyi";
export * from "./x402";
export * from "./zerion";
export * from "./zerodev";
+export * from "./zeroX";
export * from "./zora";
diff --git a/typescript/agentkit/src/action-providers/zeroX/README.md b/typescript/agentkit/src/action-providers/zeroX/README.md
new file mode 100644
index 000000000..7abee511c
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/README.md
@@ -0,0 +1,66 @@
+# 0x Action Provider
+
+This directory contains the **ZeroXActionProvider** implementation, which provides actions to interact with the **0x Protocol** for token swaps on EVM-compatible networks.
+
+## Directory Structure
+
+```
+zeroX/
+├── zeroXActionProvider.ts # Main provider with 0x Protocol functionality
+├── zeroXActionProvider.test.ts # Tests
+├── schemas.ts # Swap schemas
+├── index.ts # Main exports
+└── README.md # This file
+```
+
+## Actions
+
+- `get_swap_price_quote_from_0x`: Get a price quote for swapping tokens
+ - Returns detailed price information including exchange rate, fees, and minimum buy amount
+ - Does not execute any transactions
+ - Supports optional affiliate fee collection
+
+- `execute_swap_on_0x`: Execute a token swap between two tokens
+ - Handles token approvals automatically if needed
+ - Executes the swap transaction
+ - Returns the transaction hash and swap details upon success
+ - Supports optional affiliate fee collection
+
+## Adding New Actions
+
+To add new 0x Protocol actions:
+
+1. Define your action schema in `schemas.ts`
+2. Implement the action in `zeroXActionProvider.ts`
+3. Add tests in `zeroXActionProvider.test.ts`
+
+## Network Support
+
+The 0x provider supports all EVM-compatible networks where the 0x API is available.
+
+## Configuration
+
+The provider requires the following configuration:
+
+```typescript
+const provider = zeroXActionProvider({
+ apiKey: "your-0x-api-key" // Required for 0x API access
+});
+```
+
+You can also set the API key via environment variable:
+
+```
+ZEROX_API_KEY=your-0x-api-key
+```
+
+## Notes
+
+- The contract address for native ETH is `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`
+- All token amounts should be specified in whole units (e.g., "1.5" ETH, not wei)
+- Slippage setting is optional and defaults to 1% (100 basis points) if not specified
+- Always check price quotes before executing swaps to ensure favorable rates
+- For affiliate fees: provide swapFeeRecipient to enable fees (swapFeeBps defaults to 1%)
+- Affiliate fees are automatically collected from the sellToken and sent to the specified recipient address
+
+For more information on the **0x Protocol**, visit [0x Protocol Documentation](https://docs.0x.org/).
\ No newline at end of file
diff --git a/typescript/agentkit/src/action-providers/zeroX/index.ts b/typescript/agentkit/src/action-providers/zeroX/index.ts
new file mode 100644
index 000000000..dae43042d
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/index.ts
@@ -0,0 +1 @@
+export * from "./zeroXActionProvider";
diff --git a/typescript/agentkit/src/action-providers/zeroX/schemas.ts b/typescript/agentkit/src/action-providers/zeroX/schemas.ts
new file mode 100644
index 000000000..a55013e4e
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/schemas.ts
@@ -0,0 +1,85 @@
+import { z } from "zod";
+
+/**
+ * Input schema for getting a swap price.
+ */
+export const GetSwapPriceSchema = z
+ .object({
+ sellToken: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .describe("The token contract address to sell"),
+ buyToken: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .describe("The token contract address to buy"),
+ sellAmount: z
+ .string()
+ .describe("The amount of sellToken to sell in whole units (e.g., 1.5 WETH, 10 USDC)"),
+ slippageBps: z
+ .number()
+ .int()
+ .min(0)
+ .max(10000)
+ .optional()
+ .default(100)
+ .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"),
+ swapFeeRecipient: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .optional()
+ .describe("The wallet address to receive an affiliate fee on the trade"),
+ swapFeeBps: z
+ .number()
+ .int()
+ .min(0)
+ .max(1000)
+ .default(100)
+ .describe(
+ "The amount in basis points (0-1000) of the sellToken to charge as trading fee (defaults to 100 = 1%), only used if swapFeeRecipient is provided",
+ ),
+ })
+ .strip()
+ .describe("Get a price quote for swapping one token for another");
+
+/**
+ * Input schema for executing a swap.
+ */
+export const ExecuteSwapSchema = z
+ .object({
+ sellToken: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .describe("The token contract address to sell"),
+ buyToken: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .describe("The token contract address to buy"),
+ sellAmount: z
+ .string()
+ .describe("The amount of sellToken to sell in whole units (e.g., 1.5 WETH, 10 USDC)"),
+ slippageBps: z
+ .number()
+ .int()
+ .min(0)
+ .max(10000)
+ .optional()
+ .default(100)
+ .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"),
+ swapFeeRecipient: z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
+ .optional()
+ .describe("The wallet address to receive an affiliate fee on the trade"),
+ swapFeeBps: z
+ .number()
+ .int()
+ .min(0)
+ .max(1000)
+ .default(100)
+ .describe(
+ "The amount in basis points (0-1000) of the sellToken to charge as trading fee (defaults to 100 = 1%), only used if swapFeeRecipient is provided",
+ ),
+ })
+ .strip()
+ .describe("Execute a swap between two tokens");
diff --git a/typescript/agentkit/src/action-providers/zeroX/utils.ts b/typescript/agentkit/src/action-providers/zeroX/utils.ts
new file mode 100644
index 000000000..4d9eee7d5
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/utils.ts
@@ -0,0 +1,130 @@
+import { Hex, erc20Abi } from "viem";
+import { EvmWalletProvider } from "../../wallet-providers";
+
+// Permit2 contract address is the same across all networks
+export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
+
+/**
+ * Checks if a token is native ETH.
+ *
+ * @param token - The token address to check.
+ * @returns True if the token is native ETH, false otherwise.
+ */
+export function isNativeEth(token: string): boolean {
+ return token.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
+}
+
+/**
+ * Gets the details (decimals and name) for both fromToken and toToken
+ *
+ * @param walletProvider - The EVM wallet provider to read contracts
+ * @param fromToken - The contract address of the from token
+ * @param toToken - The contract address of the to token
+ * @returns Promise<{fromTokenDecimals: number, toTokenDecimals: number, fromTokenName: string, toTokenName: string}>
+ */
+export async function getTokenDetails(
+ walletProvider: EvmWalletProvider,
+ fromToken: string,
+ toToken: string,
+): Promise<{
+ fromTokenDecimals: number;
+ toTokenDecimals: number;
+ fromTokenName: string;
+ toTokenName: string;
+}> {
+ // Initialize default values for native ETH
+ let fromTokenDecimals = 18;
+ let fromTokenName = "ETH";
+ let toTokenDecimals = 18;
+ let toTokenName = "ETH";
+
+ // Prepare multicall contracts array
+ const contracts: {
+ address: Hex;
+ abi: typeof erc20Abi;
+ functionName: "decimals" | "name";
+ }[] = [];
+ const contractIndexMap = {
+ fromDecimals: -1,
+ fromName: -1,
+ toDecimals: -1,
+ toName: -1,
+ };
+
+ // Add from token contracts if not native ETH
+ if (!isNativeEth(fromToken)) {
+ contractIndexMap.fromDecimals = contracts.length;
+ contracts.push({
+ address: fromToken as Hex,
+ abi: erc20Abi,
+ functionName: "decimals",
+ });
+
+ contractIndexMap.fromName = contracts.length;
+ contracts.push({
+ address: fromToken as Hex,
+ abi: erc20Abi,
+ functionName: "name",
+ });
+ }
+
+ // Add to token contracts if not native ETH
+ if (!isNativeEth(toToken)) {
+ contractIndexMap.toDecimals = contracts.length;
+ contracts.push({
+ address: toToken as Hex,
+ abi: erc20Abi,
+ functionName: "decimals",
+ });
+
+ contractIndexMap.toName = contracts.length;
+ contracts.push({
+ address: toToken as Hex,
+ abi: erc20Abi,
+ functionName: "name",
+ });
+ }
+
+ // Execute multicall if there are contracts to call
+ if (contracts.length > 0) {
+ try {
+ const results = await walletProvider.getPublicClient().multicall({
+ contracts,
+ });
+
+ // Extract from token details
+ if (contractIndexMap.fromDecimals !== -1) {
+ const decimalsResult = results[contractIndexMap.fromDecimals];
+ const nameResult = results[contractIndexMap.fromName];
+
+ if (decimalsResult.status === "success" && nameResult.status === "success") {
+ fromTokenDecimals = decimalsResult.result as number;
+ fromTokenName = nameResult.result as string;
+ } else {
+ throw new Error(
+ `Failed to read details for fromToken ${fromToken}. This address may not be a valid ERC20 contract.`,
+ );
+ }
+ }
+
+ // Extract to token details
+ if (contractIndexMap.toDecimals !== -1) {
+ const decimalsResult = results[contractIndexMap.toDecimals];
+ const nameResult = results[contractIndexMap.toName];
+
+ if (decimalsResult.status === "success" && nameResult.status === "success") {
+ toTokenDecimals = decimalsResult.result as number;
+ toTokenName = nameResult.result as string;
+ } else {
+ throw new Error(
+ `Failed to read details for toToken ${toToken}. This address may not be a valid ERC20 contract.`,
+ );
+ }
+ }
+ } catch (error) {
+ throw new Error(`Failed to read token details via multicall. Error: ${error}`);
+ }
+ }
+
+ return { fromTokenDecimals, toTokenDecimals, fromTokenName, toTokenName };
+}
diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts
new file mode 100644
index 000000000..4b4e231cf
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts
@@ -0,0 +1,542 @@
+import { zeroXActionProvider } from "./zeroXActionProvider";
+import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas";
+import { EvmWalletProvider } from "../../wallet-providers";
+import { Hex } from "viem";
+
+// Mock the fetch function
+global.fetch = jest.fn();
+
+describe("ZeroX Schema Validation", () => {
+ it("should validate GetSwapPrice schema with valid input", () => {
+ const validInput = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ slippageBps: 50,
+ };
+
+ const result = GetSwapPriceSchema.safeParse(validInput);
+ expect(result.success).toBe(true);
+ });
+
+ it("should fail validation with invalid address format", () => {
+ const invalidInput = {
+ sellToken: "invalid-address",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ slippageBps: 50,
+ };
+
+ const result = GetSwapPriceSchema.safeParse(invalidInput);
+ expect(result.success).toBe(false);
+ });
+
+ it("should use default slippageBps when not provided", () => {
+ const inputWithoutSlippage = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ };
+
+ const result = GetSwapPriceSchema.safeParse(inputWithoutSlippage);
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.slippageBps).toBe(100); // Default value from schema
+ }
+ });
+
+ it("should validate swap fee parameters when both provided", () => {
+ const inputWithSwapFees = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ swapFeeBps: 50,
+ };
+
+ const result = GetSwapPriceSchema.safeParse(inputWithSwapFees);
+ expect(result.success).toBe(true);
+ });
+
+ it("should validate when only swapFeeRecipient provided (swapFeeBps defaults to 100)", () => {
+ const inputWithOnlyRecipient = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ swapFeeBps: 100,
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ };
+
+ const result = GetSwapPriceSchema.safeParse(inputWithOnlyRecipient);
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.swapFeeBps).toBe(100); // Default value
+ }
+ });
+
+ it("should fail validation when swapFeeBps exceeds maximum", () => {
+ const inputWithInvalidSwapFeeBps = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ swapFeeBps: 1500, // Exceeds maximum of 1000
+ };
+
+ const result = GetSwapPriceSchema.safeParse(inputWithInvalidSwapFeeBps);
+ expect(result.success).toBe(false);
+ });
+
+ it("should validate ExecuteSwap schema with valid input", () => {
+ const validInput = {
+ sellToken: "0x1234567890123456789012345678901234567890",
+ buyToken: "0x0987654321098765432109876543210987654321",
+ sellAmount: "1.5",
+ slippageBps: 50,
+ };
+
+ const result = ExecuteSwapSchema.safeParse(validInput);
+ expect(result.success).toBe(true);
+ });
+});
+
+describe("ZeroX Action Provider", () => {
+ let provider: ReturnType;
+ let mockWalletProvider: jest.Mocked;
+
+ const MOCK_SELL_TOKEN = "0x1234567890123456789012345678901234567890";
+ const MOCK_BUY_TOKEN = "0x0987654321098765432109876543210987654321";
+ const MOCK_SELL_AMOUNT = "1.5";
+ const MOCK_CHAIN_ID = 1;
+ const MOCK_ADDRESS = "0xabcdef1234567890abcdef1234567890abcdef12";
+
+ beforeEach(() => {
+ provider = zeroXActionProvider({ apiKey: "test-api-key" });
+
+ mockWalletProvider = {
+ getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS),
+ getNetwork: jest.fn().mockReturnValue({
+ chainId: MOCK_CHAIN_ID,
+ protocolFamily: "evm",
+ networkId: "ethereum-mainnet",
+ }),
+ readContract: jest.fn(),
+ getPublicClient: jest.fn().mockReturnValue({
+ multicall: jest.fn(),
+ }),
+ sendTransaction: jest.fn(),
+ waitForTransactionReceipt: jest.fn(),
+ signTypedData: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ // Reset mocks
+ (global.fetch as jest.Mock).mockReset();
+ });
+
+ describe("getSwapPrice", () => {
+ beforeEach(() => {
+ // Mock multicall for token details (decimals and names)
+ const mockMulticallResults = [
+ { status: "success", result: 18 }, // sellToken decimals
+ { status: "success", result: "TEST" }, // sellToken name
+ { status: "success", result: 6 }, // buyToken decimals
+ { status: "success", result: "USDC" }, // buyToken name
+ ];
+
+ (mockWalletProvider.getPublicClient().multicall as jest.Mock).mockResolvedValue(
+ mockMulticallResults,
+ );
+
+ // Mock fetch for price API
+ const mockPriceResponse = {
+ buyAmount: "1000000", // 1 USDC with 6 decimals
+ minBuyAmount: "990000", // 0.99 USDC with 6 decimals
+ totalNetworkFee: "100000000000000", // 0.0001 ETH
+ issues: { balance: null },
+ liquidityAvailable: true,
+ };
+
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValueOnce(mockPriceResponse),
+ });
+ });
+
+ it("should get swap price successfully", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.getSwapPrice(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ // Verify fetch was called with correct URL params
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(
+ "api.0x.org/swap/permit2/price",
+ );
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(`chainId=${MOCK_CHAIN_ID}`);
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(
+ `sellToken=${MOCK_SELL_TOKEN}`,
+ );
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(`buyToken=${MOCK_BUY_TOKEN}`);
+
+ // Verify response formatting
+ expect(parsedResponse.success).toBe(true);
+ expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN);
+ expect(parsedResponse.sellTokenName).toBe("TEST");
+ expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN);
+ expect(parsedResponse.buyTokenName).toBe("USDC");
+ expect(parsedResponse.liquidityAvailable).toBe(true);
+ expect(parsedResponse.balanceEnough).toBe(true);
+ expect(parsedResponse.slippageBps).toBe(50);
+ expect(parsedResponse.buyAmount).toBeDefined();
+ expect(parsedResponse.minBuyAmount).toBeDefined();
+ });
+
+ it("should handle API errors", async () => {
+ (global.fetch as jest.Mock).mockReset();
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 400,
+ statusText: "Bad Request",
+ text: jest.fn().mockResolvedValueOnce("Invalid request parameters"),
+ });
+
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.getSwapPrice(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ expect(parsedResponse.success).toBe(false);
+ expect(parsedResponse.error).toContain("Error fetching swap price");
+ });
+
+ it("should handle fetch errors", async () => {
+ (global.fetch as jest.Mock).mockReset();
+ (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("Network error"));
+
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.getSwapPrice(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ expect(parsedResponse.success).toBe(false);
+ expect(parsedResponse.error).toContain("Error fetching swap price");
+ });
+
+ it("should include swap fee parameters in API call when provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ swapFeeBps: 100,
+ };
+
+ await provider.getSwapPrice(mockWalletProvider, args);
+
+ // Verify fetch was called with swap fee parameters
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(fetchUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(fetchUrl).toContain("swapFeeBps=100");
+ expect(fetchUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+ });
+
+ it("should not include swap fee parameters when not provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ await provider.getSwapPrice(mockWalletProvider, args);
+
+ // Verify fetch was called without swap fee parameters
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(fetchUrl).not.toContain("swapFeeRecipient");
+ expect(fetchUrl).not.toContain("swapFeeBps");
+ expect(fetchUrl).not.toContain("swapFeeToken");
+ });
+
+ it("should include swap fee parameters with default swapFeeBps when only recipient provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ };
+
+ await provider.getSwapPrice(mockWalletProvider, args);
+
+ // Verify fetch was called with swap fee parameters including default swapFeeBps
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(fetchUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(fetchUrl).toContain("swapFeeBps=100"); // Default value
+ expect(fetchUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+ });
+ });
+
+ describe("executeSwap", () => {
+ const MOCK_TX_HASH = "0xtxhash123456";
+
+ beforeEach(() => {
+ // Mock multicall for token details (decimals and names)
+ const mockMulticallResults = [
+ { status: "success", result: 18 }, // sellToken decimals
+ { status: "success", result: "TEST" }, // sellToken name
+ { status: "success", result: 6 }, // buyToken decimals
+ { status: "success", result: "USDC" }, // buyToken name
+ ];
+
+ (mockWalletProvider.getPublicClient().multicall as jest.Mock).mockResolvedValue(
+ mockMulticallResults,
+ );
+
+ // Mock API responses
+ const mockPriceResponse = {
+ buyAmount: "1000000", // 1 USDC with 6 decimals
+ minBuyAmount: "990000", // 0.99 USDC with 6 decimals
+ totalNetworkFee: "100000000000000", // 0.0001 ETH
+ issues: null,
+ liquidityAvailable: true,
+ };
+
+ const mockQuoteResponse = {
+ buyAmount: "1000000",
+ minBuyAmount: "990000",
+ totalNetworkFee: "100000000000000",
+ transaction: {
+ to: "0x0000000000000000000000000000000000000001",
+ data: "0x12345678",
+ value: "1500000000000000000", // 1.5 ETH
+ gas: "300000",
+ gasPrice: "20000000000",
+ },
+ };
+
+ // First fetch for price
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValueOnce(mockPriceResponse),
+ });
+
+ // Second fetch for quote
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValueOnce(mockQuoteResponse),
+ });
+
+ // Mock transaction functions
+ mockWalletProvider.sendTransaction.mockResolvedValueOnce(MOCK_TX_HASH as Hex);
+ mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({
+ transactionHash: MOCK_TX_HASH,
+ status: "success",
+ });
+ });
+
+ it("should execute swap successfully", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.executeSwap(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ // Verify API calls
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(
+ "api.0x.org/swap/permit2/price",
+ );
+ expect((global.fetch as jest.Mock).mock.calls[1][0]).toContain(
+ "api.0x.org/swap/permit2/quote",
+ );
+
+ // Verify transaction was sent
+ expect(mockWalletProvider.sendTransaction).toHaveBeenCalledTimes(1);
+ expect(mockWalletProvider.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
+
+ // Verify response formatting
+ expect(parsedResponse.success).toBe(true);
+ expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN);
+ expect(parsedResponse.sellTokenName).toBe("TEST");
+ expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN);
+ expect(parsedResponse.buyTokenName).toBe("USDC");
+ expect(parsedResponse.transactionHash).toBe(MOCK_TX_HASH);
+ expect(parsedResponse.slippageBps).toBe(50);
+ expect(parsedResponse.network).toBe("ethereum-mainnet");
+ });
+
+ it("should handle price API errors", async () => {
+ (global.fetch as jest.Mock).mockReset();
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 400,
+ statusText: "Bad Request",
+ text: jest.fn().mockResolvedValueOnce("Invalid request parameters"),
+ });
+
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.executeSwap(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ expect(parsedResponse.success).toBe(false);
+ expect(parsedResponse.error).toContain("Error fetching swap price");
+ });
+
+ it("should handle no liquidity available", async () => {
+ (global.fetch as jest.Mock).mockReset();
+
+ const mockPriceResponse = {
+ liquidityAvailable: false,
+ };
+
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValueOnce(mockPriceResponse),
+ });
+
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ const response = await provider.executeSwap(mockWalletProvider, args);
+ const parsedResponse = JSON.parse(response);
+
+ expect(parsedResponse.success).toBe(false);
+ expect(parsedResponse.error).toContain("No liquidity available");
+ });
+
+ it("should include swap fee parameters in both API calls when provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ swapFeeBps: 100,
+ };
+
+ await provider.executeSwap(mockWalletProvider, args);
+
+ // Verify both API calls include swap fee parameters
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+
+ // Check price API call
+ const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(priceUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(priceUrl).toContain("swapFeeBps=100");
+ expect(priceUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+
+ // Check quote API call
+ const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0];
+ expect(quoteUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(quoteUrl).toContain("swapFeeBps=100");
+ expect(quoteUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+ });
+
+ it("should not include swap fee parameters when not provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ };
+
+ await provider.executeSwap(mockWalletProvider, args);
+
+ // Verify both API calls exclude swap fee parameters
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+
+ // Check price API call
+ const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(priceUrl).not.toContain("swapFeeRecipient");
+ expect(priceUrl).not.toContain("swapFeeBps");
+ expect(priceUrl).not.toContain("swapFeeToken");
+
+ // Check quote API call
+ const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0];
+ expect(quoteUrl).not.toContain("swapFeeRecipient");
+ expect(quoteUrl).not.toContain("swapFeeBps");
+ expect(quoteUrl).not.toContain("swapFeeToken");
+ });
+
+ it("should include swap fee parameters with default swapFeeBps when only recipient provided", async () => {
+ const args = {
+ sellToken: MOCK_SELL_TOKEN,
+ buyToken: MOCK_BUY_TOKEN,
+ sellAmount: MOCK_SELL_AMOUNT,
+ slippageBps: 50,
+ swapFeeBps: 100,
+ swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12",
+ };
+
+ await provider.executeSwap(mockWalletProvider, args);
+
+ // Verify both API calls include swap fee parameters with default swapFeeBps
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+
+ // Check price API call
+ const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(priceUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(priceUrl).toContain("swapFeeBps=100"); // Default value
+ expect(priceUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+
+ // Check quote API call
+ const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0];
+ expect(quoteUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12");
+ expect(quoteUrl).toContain("swapFeeBps=100"); // Default value
+ expect(quoteUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`);
+ });
+ });
+
+ describe("supportsNetwork", () => {
+ it("should return true for evm networks", () => {
+ expect(provider.supportsNetwork({ protocolFamily: "evm" })).toBe(true);
+ });
+
+ it("should return false for non-evm networks", () => {
+ expect(provider.supportsNetwork({ protocolFamily: "solana" })).toBe(false);
+ });
+ });
+});
diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts
new file mode 100644
index 000000000..7079d6f76
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts
@@ -0,0 +1,447 @@
+import { z } from "zod";
+import { ActionProvider } from "../actionProvider";
+import { Network } from "../../network";
+import { CreateAction } from "../actionDecorator";
+import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas";
+import { EvmWalletProvider, LegacyCdpWalletProvider } from "../../wallet-providers";
+import {
+ erc20Abi,
+ formatUnits,
+ parseUnits,
+ maxUint256,
+ encodeFunctionData,
+ size,
+ concat,
+ Hex,
+ numberToHex,
+} from "viem";
+import { getTokenDetails, PERMIT2_ADDRESS } from "./utils";
+/**
+ * Configuration for the ZeroXActionProvider.
+ */
+export interface ZeroXActionProviderConfig {
+ /**
+ * The API key to use for 0x API requests.
+ */
+ apiKey?: string;
+}
+
+/**
+ * 0x API Action Provider for token swaps.
+ * Requires a 0x API key.
+ */
+export class ZeroXActionProvider extends ActionProvider {
+ #apiKey: string;
+
+ /**
+ * Constructor for the ZeroXActionProvider.
+ *
+ * @param config - Configuration for the provider.
+ */
+ constructor(config: ZeroXActionProviderConfig) {
+ super("zerox", []);
+ const apiKey = config.apiKey || process.env.ZEROX_API_KEY;
+ if (!apiKey) {
+ throw new Error("0x API key not provided.");
+ }
+ this.#apiKey = apiKey;
+ }
+
+ /**
+ * Gets a price quote for swapping one token for another.
+ *
+ * @param walletProvider - The wallet provider to get information from.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the price quote.
+ */
+ @CreateAction({
+ name: "get_swap_price_quote_from_0x",
+ description: `
+This tool fetches a price quote for swapping between two tokens using the 0x API.
+
+It takes the following inputs:
+- sellToken: The contract address of the token to sell
+- buyToken: The contract address of the token to buy
+- sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC)
+- slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%)
+- swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees
+- swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided
+
+Important notes:
+- The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+- This only fetches a price quote and does not execute a swap
+- Supported on all EVM networks compatible with 0x API
+- Use sellToken units exactly as provided, do not convert to wei or any other units
+`,
+ schema: GetSwapPriceSchema,
+ })
+ async getSwapPrice(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ const network = walletProvider.getNetwork();
+ const chainId = network.chainId;
+ if (!chainId) throw new Error("Chain ID not available from wallet provider");
+
+ try {
+ // Get token details
+ const {
+ fromTokenDecimals: sellTokenDecimals,
+ toTokenDecimals: buyTokenDecimals,
+ fromTokenName: sellTokenName,
+ toTokenName: buyTokenName,
+ } = await getTokenDetails(walletProvider, args.sellToken, args.buyToken);
+
+ // Convert sell amount to base units
+ const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString();
+
+ // Create URL for the price API request
+ const url = new URL("https://api.0x.org/swap/permit2/price");
+ url.searchParams.append("chainId", chainId.toString());
+ url.searchParams.append("sellToken", args.sellToken);
+ url.searchParams.append("buyToken", args.buyToken);
+ url.searchParams.append("sellAmount", sellAmount);
+ url.searchParams.append("taker", walletProvider.getAddress());
+ url.searchParams.append("slippageBps", args.slippageBps.toString());
+ if (args.swapFeeRecipient) {
+ url.searchParams.append("swapFeeRecipient", args.swapFeeRecipient);
+ url.searchParams.append("swapFeeBps", args.swapFeeBps.toString());
+ url.searchParams.append("swapFeeToken", args.sellToken);
+ }
+
+ // Make the request
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "0x-api-key": this.#apiKey,
+ "0x-version": "v2",
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return JSON.stringify({
+ success: false,
+ error: `Error fetching swap price: ${response.status} ${response.statusText} - ${errorText}`,
+ });
+ }
+
+ const data = await response.json();
+
+ // Format the response
+ const formattedResponse = {
+ success: true,
+ sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals),
+ sellTokenName: sellTokenName,
+ sellToken: args.sellToken,
+ buyAmount: formatUnits(data.buyAmount, buyTokenDecimals),
+ minBuyAmount: data.minBuyAmount ? formatUnits(data.minBuyAmount, buyTokenDecimals) : null,
+ buyTokenName: buyTokenName,
+ buyToken: args.buyToken,
+ slippageBps: args.slippageBps,
+ liquidityAvailable: data.liquidityAvailable,
+ balanceEnough: data.issues?.balance === null,
+ priceOfBuyTokenInSellToken: (
+ Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) /
+ Number(formatUnits(data.buyAmount, buyTokenDecimals))
+ ).toString(),
+ priceOfSellTokenInBuyToken: (
+ Number(formatUnits(data.buyAmount, buyTokenDecimals)) /
+ Number(formatUnits(BigInt(sellAmount), sellTokenDecimals))
+ ).toString(),
+ };
+
+ return JSON.stringify(formattedResponse);
+ } catch (error) {
+ return JSON.stringify({
+ success: false,
+ error: `Error fetching swap price: ${error}`,
+ });
+ }
+ }
+
+ /**
+ * Executes a token swap using the 0x API.
+ *
+ * @param walletProvider - The wallet provider to use for the swap.
+ * @param args - The input arguments for the action.
+ * @returns A message containing the result of the swap.
+ */
+ @CreateAction({
+ name: "execute_swap_on_0x",
+ description: `
+This tool executes a token swap between two tokens using the 0x API.
+
+It takes the following inputs:
+- sellToken: The contract address of the token to sell
+- buyToken: The contract address of the token to buy
+- sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC)
+- slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%)
+- swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees
+- swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided
+
+Important notes:
+- The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+- This will execute an actual swap transaction that sends tokens from your wallet
+- If needed, it will automatically approve the permit2 contract to spend the sell token
+- The approval transaction is only needed once per token
+- Ensure you have sufficient balance of the sell token before executing
+- The trade size might influence the excecution price depending on available liquidity
+- First fetch a price quote and only execute swap if you are happy with the indicated price
+- Supported on all EVM networks compatible with 0x API
+- Use sellToken units exactly as provided, do not convert to wei or any other units
+`,
+ schema: ExecuteSwapSchema,
+ })
+ async executeSwap(
+ walletProvider: EvmWalletProvider,
+ args: z.infer,
+ ): Promise {
+ // Sanity checks
+ const network = walletProvider.getNetwork();
+ const chainId = network.chainId;
+ if (!chainId) throw new Error("Chain ID not available from wallet provider");
+
+ try {
+ // Get token details
+ const {
+ fromTokenDecimals: sellTokenDecimals,
+ toTokenDecimals: buyTokenDecimals,
+ fromTokenName: sellTokenName,
+ toTokenName: buyTokenName,
+ } = await getTokenDetails(walletProvider, args.sellToken, args.buyToken);
+
+ // Convert sell amount to base units
+ const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString();
+
+ // Get the wallet address
+ const walletAddress = walletProvider.getAddress();
+
+ // Fetch price quote first
+ const priceUrl = new URL("https://api.0x.org/swap/permit2/price");
+ priceUrl.searchParams.append("chainId", chainId.toString());
+ priceUrl.searchParams.append("sellToken", args.sellToken);
+ priceUrl.searchParams.append("buyToken", args.buyToken);
+ priceUrl.searchParams.append("sellAmount", sellAmount);
+ priceUrl.searchParams.append("taker", walletAddress);
+ priceUrl.searchParams.append("slippageBps", args.slippageBps.toString());
+ if (args.swapFeeRecipient) {
+ priceUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient);
+ priceUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString());
+ priceUrl.searchParams.append("swapFeeToken", args.sellToken);
+ }
+
+ const priceResponse = await fetch(priceUrl.toString(), {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "0x-api-key": this.#apiKey,
+ "0x-version": "v2",
+ },
+ });
+
+ if (!priceResponse.ok) {
+ const errorText = await priceResponse.text();
+ return JSON.stringify({
+ success: false,
+ error: `Error fetching swap price: ${priceResponse.status} ${priceResponse.statusText} - ${errorText}`,
+ });
+ }
+
+ const priceData = await priceResponse.json();
+
+ // Check if liquidity is available
+ if (priceData.liquidityAvailable === false) {
+ return JSON.stringify({
+ success: false,
+ error: "No liquidity available for this swap.",
+ });
+ }
+
+ // Check if balance of sell token is enough
+ if (priceData.balance != null) {
+ return JSON.stringify({
+ success: false,
+ error: `Insufficient balance of sell token ${priceData.balance.token}. Requested to swap ${priceData.balance.expected}, but balance is only ${priceData.balance.actual}.`,
+ });
+ }
+
+ // Check if permit2 approval is needed for ERC20 tokens
+ // Only needed once per token per address
+ let approvalTxHash: Hex | null = null;
+ if (priceData.issues?.allowance) {
+ try {
+ approvalTxHash = await walletProvider.sendTransaction({
+ to: args.sellToken as Hex,
+ data: encodeFunctionData({
+ abi: erc20Abi,
+ functionName: "approve",
+ args: [PERMIT2_ADDRESS, maxUint256],
+ }),
+ });
+
+ await walletProvider.waitForTransactionReceipt(approvalTxHash);
+ } catch (error) {
+ return JSON.stringify({
+ success: false,
+ error: `Error approving token: ${error}`,
+ });
+ }
+ }
+
+ // Fetch the swap quote
+ const quoteUrl = new URL("https://api.0x.org/swap/permit2/quote");
+ quoteUrl.searchParams.append("chainId", chainId.toString());
+ quoteUrl.searchParams.append("sellToken", args.sellToken);
+ quoteUrl.searchParams.append("buyToken", args.buyToken);
+ quoteUrl.searchParams.append("sellAmount", sellAmount);
+ quoteUrl.searchParams.append("taker", walletAddress);
+ quoteUrl.searchParams.append("slippageBps", args.slippageBps.toString());
+ if (args.swapFeeRecipient) {
+ quoteUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient);
+ quoteUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString());
+ quoteUrl.searchParams.append("swapFeeToken", args.sellToken);
+ }
+
+ const quoteResponse = await fetch(quoteUrl.toString(), {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "0x-api-key": this.#apiKey,
+ "0x-version": "v2",
+ },
+ });
+
+ if (!quoteResponse.ok) {
+ const errorText = await quoteResponse.text();
+ return JSON.stringify({
+ success: false,
+ error: `Error fetching swap quote: ${quoteResponse.status} ${quoteResponse.statusText} - ${errorText}`,
+ });
+ }
+
+ const quoteData = await quoteResponse.json();
+
+ // Sign Permit2.eip712 returned from quote
+ let signature: Hex | undefined;
+ if (quoteData.permit2?.eip712) {
+ try {
+ // For LegacyCdpWalletProvider, remove EIP712Domain to avoid ambiguous primary types
+ const types =
+ walletProvider instanceof LegacyCdpWalletProvider
+ ? (() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { EIP712Domain, ...rest } = quoteData.permit2.eip712.types;
+ return rest;
+ })()
+ : quoteData.permit2.eip712.types;
+
+ const typedData = {
+ domain: quoteData.permit2.eip712.domain,
+ types,
+ primaryType: quoteData.permit2.eip712.primaryType,
+ message: quoteData.permit2.eip712.message,
+ } as const;
+
+ signature = await walletProvider.signTypedData(typedData);
+
+ // Append sig length and sig data to transaction.data
+ if (signature && quoteData.transaction?.data) {
+ const signatureLengthInHex = numberToHex(size(signature), {
+ signed: false,
+ size: 32,
+ });
+
+ const transactionData = quoteData.transaction.data as Hex;
+ const sigLengthHex = signatureLengthInHex as Hex;
+ const sig = signature as Hex;
+
+ quoteData.transaction.data = concat([transactionData, sigLengthHex, sig]);
+ }
+ } catch (error) {
+ return JSON.stringify({
+ success: false,
+ error: `Error signing permit2 message: ${error}`,
+ });
+ }
+ }
+
+ // Execute swap
+ try {
+ // Prepare transaction parameters
+ const txParams: {
+ to: Hex;
+ data: Hex;
+ gas?: bigint;
+ value?: bigint;
+ } = {
+ to: quoteData.transaction.to as Hex,
+ data: quoteData.transaction.data as Hex,
+ ...(quoteData?.transaction.gas ? { gas: BigInt(quoteData.transaction.gas) } : {}),
+ ...(quoteData.transaction.value ? { value: BigInt(quoteData.transaction.value) } : {}),
+ };
+
+ // Send transaction
+ const txHash = await walletProvider.sendTransaction(txParams);
+ const receipt = await walletProvider.waitForTransactionReceipt(txHash);
+ if (receipt.status !== "complete" && receipt.status !== "success") {
+ return JSON.stringify({
+ success: false,
+ ...(approvalTxHash ? { approvalTxHash } : {}),
+ transactionHash: receipt.transactionHash,
+ error: `Swap transaction failed`,
+ });
+ }
+
+ // Format the response
+ const formattedResponse = {
+ success: true,
+ ...(approvalTxHash ? { approvalTxHash } : {}),
+ transactionHash: receipt.transactionHash,
+ sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals),
+ sellTokenName: sellTokenName,
+ sellToken: args.sellToken,
+ buyAmount: formatUnits(quoteData.buyAmount, buyTokenDecimals),
+ minBuyAmount: quoteData.minBuyAmount
+ ? formatUnits(quoteData.minBuyAmount, buyTokenDecimals)
+ : null,
+ buyTokenName: buyTokenName,
+ buyToken: args.buyToken,
+ slippageBps: args.slippageBps,
+ network: network.networkId,
+ };
+
+ return JSON.stringify(formattedResponse);
+ } catch (error) {
+ return JSON.stringify({
+ success: false,
+ error: `Error sending swap transaction: ${error}`,
+ ...(approvalTxHash ? { approvalTxHash } : {}),
+ });
+ }
+ } catch (error) {
+ return JSON.stringify({
+ success: false,
+ error: `Error executing swap: ${error}`,
+ });
+ }
+ }
+
+ /**
+ * Checks if the ZeroX action provider supports the given network.
+ *
+ * @param network - The network to check.
+ * @returns True if the ZeroX action provider supports the network, false otherwise.
+ */
+ supportsNetwork = (network: Network) => network.protocolFamily === "evm";
+}
+
+/**
+ * Creates a new ZeroXActionProvider with the provided configuration.
+ *
+ * @param config - Optional configuration for the provider.
+ * @returns A new ZeroXActionProvider.
+ */
+export const zeroXActionProvider = (config: ZeroXActionProviderConfig) =>
+ new ZeroXActionProvider(config);