diff --git a/typescript/.changeset/real-hounds-juggle.md b/typescript/.changeset/real-hounds-juggle.md new file mode 100644 index 000000000..1404b615d --- /dev/null +++ b/typescript/.changeset/real-hounds-juggle.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": minor +--- + +Added ATV (Aarna Tokenized Vault) action provider for DeFi yield vault interactions on Ethereum and Base. diff --git a/typescript/agentkit/src/action-providers/atv/README.md b/typescript/agentkit/src/action-providers/atv/README.md new file mode 100644 index 000000000..fe39280e0 --- /dev/null +++ b/typescript/agentkit/src/action-providers/atv/README.md @@ -0,0 +1,43 @@ +# ATV Action Provider + +This action provider integrates [Aarna Tokenized Vaults (ATV)](https://aarna.ai) into AgentKit, giving AI agents access to DeFi yield vaults on Ethereum and Base. + +## Features + +- **Vault Discovery** — List all available yield vaults with metadata and deposit tokens +- **Performance Metrics** — Query real-time NAV, TVL, and APY for any vault +- **Transaction Building** — Build deposit and withdraw calldata ready for signing + +## Setup + +You need an ATV API key to use this provider. Get one at [aarna.ai](https://aarna.ai) or contact dev@aarnalab.dev. + +```typescript +import { atvActionProvider } from "./action-providers/atv"; + +const agent = new AgentKit({ + // ... + actionProviders: [atvActionProvider("your-atv-api-key")], +}); +``` + +## Tools + +| Tool | Description | +| --- | --- | +| `atv_list_vaults` | List available DeFi yield vaults (optionally filter by chain) | +| `atv_get_vault_nav` | Get current NAV (Net Asset Value) price for a vault | +| `atv_get_vault_tvl` | Get current TVL (Total Value Locked) for a vault | +| `atv_get_vault_apy` | Get APY breakdown (base + reward + total) for a vault | +| `atv_build_deposit_tx` | Build ERC-20 approve + deposit transaction calldata | +| `atv_build_withdraw_tx` | Build withdraw transaction calldata | + +## Network Support + +ATV is an API-based provider that works across all EVM networks. Vaults are currently deployed on Ethereum and Base. + +## Links + +- [ATV SDK Repository](https://github.com/aarna-ai/atv-sdk) +- [API Documentation](https://atv-api.aarna.ai/docs) +- [npm Package](https://www.npmjs.com/package/@aarna-ai/mcp-server-atv) diff --git a/typescript/agentkit/src/action-providers/atv/atvActionProvider.test.ts b/typescript/agentkit/src/action-providers/atv/atvActionProvider.test.ts new file mode 100644 index 000000000..72f04825b --- /dev/null +++ b/typescript/agentkit/src/action-providers/atv/atvActionProvider.test.ts @@ -0,0 +1,169 @@ +import { atvActionProvider, AtvActionProvider } from "./atvActionProvider"; + +// Mock fetch globally +const mockFetch = jest.fn(); +global.fetch = mockFetch; + +describe("AtvActionProvider", () => { + let provider: AtvActionProvider; + + beforeEach(() => { + provider = new AtvActionProvider("test-api-key"); + mockFetch.mockReset(); + }); + + describe("constructor", () => { + it("should create provider with default base URL", () => { + expect(provider).toBeInstanceOf(AtvActionProvider); + }); + + it("should create provider with custom base URL", () => { + const custom = new AtvActionProvider("key", "https://custom.api.com"); + expect(custom).toBeInstanceOf(AtvActionProvider); + }); + }); + + describe("factory function", () => { + it("should create provider via factory", () => { + const p = atvActionProvider("test-key"); + expect(p).toBeInstanceOf(AtvActionProvider); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for all networks", () => { + expect(provider.supportsNetwork()).toBe(true); + }); + }); + + describe("listVaults", () => { + it("should list vaults successfully", async () => { + const mockVaults = [{ address: "0x123", chain: "ethereum" }]; + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockVaults, + }); + + const result = await provider.listVaults({}); + expect(result).toContain("0x123"); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/v1/vaults"), + expect.objectContaining({ + headers: { "x-api-key": "test-api-key" }, + }), + ); + }); + + it("should pass chain filter", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => [], + }); + + await provider.listVaults({ chain: "base" }); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("chain=base"), + expect.any(Object), + ); + }); + + it("should handle API errors gracefully", async () => { + mockFetch.mockResolvedValueOnce({ ok: false, status: 401 }); + + const result = await provider.listVaults({}); + expect(result).toContain("Error calling ATV API"); + }); + }); + + describe("getVaultNav", () => { + it("should fetch NAV for a vault", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ nav: "1.05" }), + }); + + const result = await provider.getVaultNav({ address: "0xABC" }); + expect(result).toContain("1.05"); + }); + }); + + describe("getVaultTvl", () => { + it("should fetch TVL for a vault", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ tvl: "5000000" }), + }); + + const result = await provider.getVaultTvl({ address: "0xABC" }); + expect(result).toContain("5000000"); + }); + }); + + describe("getVaultApy", () => { + it("should fetch APY for a vault", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ baseApy: "8.5", totalApy: "10.2" }), + }); + + const result = await provider.getVaultApy({ address: "0xABC" }); + expect(result).toContain("10.2"); + }); + }); + + describe("buildDepositTx", () => { + it("should build deposit transaction", async () => { + const mockTx = { steps: [{ type: "approve" }, { type: "deposit" }] }; + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTx, + }); + + const result = await provider.buildDepositTx({ + userAddress: "0xUser", + vaultAddress: "0xVault", + depositTokenAddress: "0xToken", + depositAmount: "100", + }); + expect(result).toContain("approve"); + expect(result).toContain("deposit"); + }); + }); + + describe("buildWithdrawTx", () => { + it("should build withdraw transaction", async () => { + const mockTx = { steps: [{ type: "withdraw" }] }; + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTx, + }); + + const result = await provider.buildWithdrawTx({ + userAddress: "0xUser", + vaultAddress: "0xVault", + oTokenAddress: "0xToken", + sharesToWithdraw: "50", + }); + expect(result).toContain("withdraw"); + }); + + it("should include slippage when provided", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + await provider.buildWithdrawTx({ + userAddress: "0xUser", + vaultAddress: "0xVault", + oTokenAddress: "0xToken", + sharesToWithdraw: "50", + slippage: "0.5", + }); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("slippage=0.5"), + expect.any(Object), + ); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/atv/atvActionProvider.ts b/typescript/agentkit/src/action-providers/atv/atvActionProvider.ts new file mode 100644 index 000000000..0c8c55cb2 --- /dev/null +++ b/typescript/agentkit/src/action-providers/atv/atvActionProvider.ts @@ -0,0 +1,192 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { + ListVaultsSchema, + GetVaultNavSchema, + GetVaultTvlSchema, + GetVaultApySchema, + BuildDepositTxSchema, + BuildWithdrawTxSchema, +} from "./schemas"; + +const ATV_BASE_URL = "https://atv-api.aarna.ai"; + +/** + * AtvActionProvider provides access to Aarna Tokenized Vault (ATV) DeFi yield vaults. + * + * It enables AI agents to discover vaults, query performance metrics (NAV, TVL, APY), + * and build deposit/withdraw transactions on Ethereum and Base. + * + * All endpoints require an ATV API key passed via the `x-api-key` header. + * Get your key at https://aarna.ai or contact dev@aarnalab.dev. + */ +export class AtvActionProvider extends ActionProvider { + private readonly apiKey: string; + private readonly baseUrl: string; + + constructor(apiKey: string, baseUrl?: string) { + super("atv", []); + this.apiKey = apiKey; + this.baseUrl = baseUrl ?? ATV_BASE_URL; + } + + private async request(path: string, params?: Record): Promise { + try { + const url = new URL(path, this.baseUrl); + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, value); + } + } + } + + const response = await fetch(url.toString(), { + headers: { "x-api-key": this.apiKey }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return JSON.stringify(data, null, 2); + } catch (error: unknown) { + return `Error calling ATV API: ${error instanceof Error ? error.message : String(error)}`; + } + } + + @CreateAction({ + name: "atv_list_vaults", + description: `This tool lists all available ATV DeFi yield vaults from Aarna. +It takes the following inputs: +- An optional chain filter (e.g. 'ethereum', 'base') +- An optional user wallet address to include token balances + +Important notes: +- Returns vault metadata including address, chain, withdraw type, and supported deposit tokens +- If userAddress is provided, includes ERC-20 balances for each deposit token +- Vaults are available on Ethereum and Base networks`, + schema: ListVaultsSchema, + }) + async listVaults(args: z.infer): Promise { + const params: Record = {}; + if (args.chain) params.chain = args.chain; + if (args.userAddress) params.userAddress = args.userAddress; + return this.request("/v1/vaults", params); + } + + @CreateAction({ + name: "atv_get_vault_nav", + description: `This tool gets the current Net Asset Value (NAV) price for an ATV vault. +It takes the following inputs: +- The vault contract address + +Important notes: +- NAV represents the price per vault share in USD +- Reads directly from the on-chain vault contract`, + schema: GetVaultNavSchema, + }) + async getVaultNav(args: z.infer): Promise { + return this.request(`/v1/vaults/${args.address}/nav`); + } + + @CreateAction({ + name: "atv_get_vault_tvl", + description: `This tool gets the current Total Value Locked (TVL) for an ATV vault. +It takes the following inputs: +- The vault contract address + +Important notes: +- TVL is returned in USD +- Reads directly from the on-chain vault contract`, + schema: GetVaultTvlSchema, + }) + async getVaultTvl(args: z.infer): Promise { + return this.request(`/v1/vaults/${args.address}/tvl`); + } + + @CreateAction({ + name: "atv_get_vault_apy", + description: `This tool gets the current APY breakdown for an ATV vault. +It takes the following inputs: +- The vault contract address + +Important notes: +- Returns base APY, reward APY, and total APY +- APY is calculated from on-chain vault performance data`, + schema: GetVaultApySchema, + }) + async getVaultApy(args: z.infer): Promise { + return this.request(`/v1/vaults/${args.address}/apy`); + } + + @CreateAction({ + name: "atv_build_deposit_tx", + description: `This tool builds the transaction calldata to deposit tokens into an ATV vault. +It takes the following inputs: +- The depositor's EVM wallet address +- The vault contract address +- The ERC-20 token address to deposit +- The human-readable deposit amount (e.g. '100' for 100 USDC) + +Important notes: +- Returns an ordered array of transactions: first an ERC-20 approve, then the deposit +- Both transactions must be sent in order +- Check deposit status before calling to ensure deposits are not paused`, + schema: BuildDepositTxSchema, + }) + async buildDepositTx(args: z.infer): Promise { + return this.request("/v1/deposit-tx", { + userAddress: args.userAddress, + vaultAddress: args.vaultAddress, + depositTokenAddress: args.depositTokenAddress, + depositAmount: args.depositAmount, + }); + } + + @CreateAction({ + name: "atv_build_withdraw_tx", + description: `This tool builds the transaction calldata to withdraw from an ATV vault. +It takes the following inputs: +- The withdrawer's EVM wallet address +- The vault contract address +- The output token address to receive +- The number of vault shares to withdraw +- An optional slippage tolerance percentage + +Important notes: +- Returns transaction calldata ready to be signed and sent +- Check withdraw status before calling to ensure withdrawals are not paused +- Slippage defaults to 0 if not specified`, + schema: BuildWithdrawTxSchema, + }) + async buildWithdrawTx(args: z.infer): Promise { + const params: Record = { + userAddress: args.userAddress, + vaultAddress: args.vaultAddress, + oTokenAddress: args.oTokenAddress, + sharesToWithdraw: args.sharesToWithdraw, + }; + if (args.slippage) params.slippage = args.slippage; + return this.request("/v1/withdraw-tx", params); + } + + /** + * ATV is an API-based provider and works across all networks. + */ + supportsNetwork(): boolean { + return true; + } +} + +/** + * Factory function to create an ATV action provider. + * + * @param apiKey - ATV API key (get one at https://aarna.ai or contact dev@aarnalab.dev) + * @param baseUrl - Optional custom API base URL (defaults to https://atv-api.aarna.ai) + * @returns A new AtvActionProvider instance + */ +export const atvActionProvider = (apiKey: string, baseUrl?: string) => + new AtvActionProvider(apiKey, baseUrl); diff --git a/typescript/agentkit/src/action-providers/atv/index.ts b/typescript/agentkit/src/action-providers/atv/index.ts new file mode 100644 index 000000000..591e4f142 --- /dev/null +++ b/typescript/agentkit/src/action-providers/atv/index.ts @@ -0,0 +1,9 @@ +export { AtvActionProvider, atvActionProvider } from "./atvActionProvider"; +export { + ListVaultsSchema, + GetVaultNavSchema, + GetVaultTvlSchema, + GetVaultApySchema, + BuildDepositTxSchema, + BuildWithdrawTxSchema, +} from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/atv/schemas.ts b/typescript/agentkit/src/action-providers/atv/schemas.ts new file mode 100644 index 000000000..773734779 --- /dev/null +++ b/typescript/agentkit/src/action-providers/atv/schemas.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; + +export const ListVaultsSchema = z + .object({ + chain: z + .string() + .optional() + .describe("Filter by chain name or ID (e.g. 'ethereum', 'base', '8453')"), + userAddress: z + .string() + .optional() + .describe("EVM wallet address to include token balances for each deposit token"), + }) + .strict() + .describe("Parameters for listing available ATV yield vaults"); + +export const GetVaultNavSchema = z + .object({ + address: z.string().describe("Vault contract address"), + }) + .strict() + .describe("Parameters for getting vault NAV price"); + +export const GetVaultTvlSchema = z + .object({ + address: z.string().describe("Vault contract address"), + }) + .strict() + .describe("Parameters for getting vault TVL"); + +export const GetVaultApySchema = z + .object({ + address: z.string().describe("Vault contract address"), + }) + .strict() + .describe("Parameters for getting vault APY"); + +export const BuildDepositTxSchema = z + .object({ + userAddress: z.string().describe("EVM address of the depositor"), + vaultAddress: z.string().describe("Vault contract address"), + depositTokenAddress: z.string().describe("ERC-20 token address to deposit"), + depositAmount: z + .string() + .describe("Human-readable deposit amount (e.g. '100' for 100 USDC)"), + }) + .strict() + .describe("Parameters for building a vault deposit transaction"); + +export const BuildWithdrawTxSchema = z + .object({ + userAddress: z.string().describe("EVM address of the withdrawer"), + vaultAddress: z.string().describe("Vault contract address"), + oTokenAddress: z.string().describe("Output token address to receive"), + sharesToWithdraw: z + .string() + .describe("Human-readable share amount to withdraw (e.g. '100')"), + slippage: z + .string() + .optional() + .describe("Slippage tolerance as a percentage (e.g. '0.5' for 0.5%)"), + }) + .strict() + .describe("Parameters for building a vault withdraw transaction"); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..c7d329e83 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -41,3 +41,4 @@ export * from "./zerion"; export * from "./zerodev"; export * from "./zeroX"; export * from "./zora"; +export * from "./atv";