Skip to content
Open
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/real-hounds-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": minor
---

Added ATV (Aarna Tokenized Vault) action provider for DeFi yield vault interactions on Ethereum and Base.
43 changes: 43 additions & 0 deletions typescript/agentkit/src/action-providers/atv/README.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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),
);
});
});
});
Loading
Loading