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/whole-times-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added ZeroX Action Provider to enable token swaps using the 0x Protocol API
9 changes: 9 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,15 @@ 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>
<summary><strong>ZeroX</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>get_swap_price_quote_from_0x</code></td>
<td width="768">Fetches a price quote for swapping between two tokens using the 0x API.</td>
</tr>
<tr>
<td width="200"><code>execute_swap_on_0x</code></td>
<td width="768">Executes a token swap between two tokens using the 0x API.</td>
</tr>
</table>
</details>
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export * from "./vaultsfyi";
export * from "./x402";
export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
export * from "./zora";
66 changes: 66 additions & 0 deletions typescript/agentkit/src/action-providers/zeroX/README.md
Original file line number Diff line number Diff line change
@@ -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/).
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/zeroX/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./zeroXActionProvider";
85 changes: 85 additions & 0 deletions typescript/agentkit/src/action-providers/zeroX/schemas.ts
Original file line number Diff line number Diff line change
@@ -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");
130 changes: 130 additions & 0 deletions typescript/agentkit/src/action-providers/zeroX/utils.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
Loading
Loading