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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The following project-specific skills are available. **Read the relevant SKILL.m

- **Aave V3** → `skills/aave-agentic-wallet/SKILL.md`
- **SparkLend** → `skills/spark-agentic-wallet/SKILL.md`
- **Merkl** → `skills/merkl-agentic-wallet/SKILL.md`

## Adding a New Skill

Expand Down
49 changes: 34 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A collection of protocol-specific skills that teach AI agents (Claude, etc.) how
|----------|--------|-------|
| **Aave V3** | Available | `skills/aave-agentic-wallet/SKILL.md` |
| **SparkLend** | Available | `skills/spark-agentic-wallet/SKILL.md` |
| **Merkl** | Available | `skills/merkl-agentic-wallet/SKILL.md` |
| **Lido** | Coming soon | — |
| **Morpho** | Coming soon | — |

Expand Down Expand Up @@ -44,6 +45,21 @@ node dist/skills/spark-agentic-wallet/spark.js opportunities --network ethereum
node dist/skills/spark-agentic-wallet/spark.js position --wallet 0x... --network ethereum
```

### Merkl

Browse incentivized DeFi opportunities, check unclaimed rewards, and claim Merkl rewards across 50+ chains. Merkl is a reward distribution layer — it incentivizes positions on other protocols like Aave, SparkLend, Morpho, and more.

```bash
# Browse top opportunities by APR
node dist/skills/merkl-agentic-wallet/merkl.js opportunities --chain-id 1

# Check unclaimed rewards
node dist/skills/merkl-agentic-wallet/merkl.js check-rewards --wallet 0x...

# Encode a claim transaction
node dist/skills/merkl-agentic-wallet/merkl.js claim-rewards --wallet 0x... --chain-id 1
```

## MCP Server

This project is also available as a standalone MCP server, exposing all DeFi skills as MCP tools consumable by any MCP-compatible agent (Claude, Cursor, Windsurf, custom agents).
Expand Down Expand Up @@ -76,37 +92,36 @@ npx @bootnodedev/defi-mcp-server
npx @bootnodedev/defi-mcp-server --http --port 3000
```

### Available tools (14)
### Available tools (10)

| Tool | Description |
|------|-------------|
| `aave_get_opportunities` | Get Aave V3 supply/borrow APY rates across networks |
| `aave_check_position` | Check wallet health factor, collateral, and debt |
| `aave_get_permissions` | Get permission params for Aave V3 operations |
| `aave_encode_supply` | Encode approve + supply calldata |
| `aave_encode_borrow` | Encode borrow calldata |
| `aave_encode_repay` | Encode approve + repay calldata |
| `aave_encode_withdraw` | Encode withdraw calldata |
| `spark_get_opportunities` | Get SparkLend supply/borrow APY rates |
| `spark_check_position` | Check wallet health factor, collateral, and debt |
| `spark_get_permissions` | Get permission params for SparkLend operations |
| `spark_encode_supply` | Encode approve + supply calldata |
| `spark_encode_borrow` | Encode borrow calldata |
| `spark_encode_repay` | Encode approve + repay calldata |
| `spark_encode_withdraw` | Encode withdraw calldata |
| `defi_guide` | Overview of all tools, workflows, and key rules — call before your first DeFi operation |
| `aave_get_opportunities` | Aave V3 supply/borrow APY rates, top reserves ranked by supply APY |
| `aave_check_position` | Wallet health factor, collateral, and debt on Aave V3 |
| `aave_get_permissions` | Permission params for Aave V3 — pass result to agentic-wallet grant_permissions |
| `aave_encode` | Encode Aave V3 calldata (supply, borrow, repay, withdraw) |
| `spark_get_opportunities` | SparkLend supply/borrow APY rates |
| `spark_check_position` | Wallet health factor, collateral, and debt on SparkLend |
| `spark_get_permissions` | Permission params for SparkLend |
| `spark_encode` | Encode SparkLend calldata (supply, borrow, repay, withdraw) |
| `merkl` | Merkl reward management — browse opportunities, check rewards, claim rewards |

The `*_get_opportunities` tools return the top 10 reserves per network by default, with slim fields to minimize token usage. Optional parameters:

- `token` — filter by token symbol (e.g. `USDC`) or contract address (case-insensitive)
- `limit` — max reserves per network (default: 10)
- `detailed` — include all fields like `availableLiquidity`, `liquidationThreshold`, `canBeCollateral` (default: false)

The `merkl` tool uses subcommands: `get_opportunities`, `check_rewards`, `claim_rewards`.

## Claude Code Plugin

This project is packaged as a [Claude Code plugin](https://code.claude.com/docs/en/plugins). Install it to get:

- **`/agentic-defi:aave` slash command** — Run Aave operations directly (supply, borrow, repay, withdraw, position, opportunities, permissions)
- **`/agentic-defi:spark` slash command** — Run SparkLend operations directly (supply, borrow, repay, withdraw, position, opportunities, permissions)
- **`/agentic-defi:merkl` slash command** — Browse Merkl opportunities, check and claim rewards
- **SessionStart hook** — Automatically checks that the agentic-wallet MCP server is configured when a session starts

### Install as plugin
Expand All @@ -131,6 +146,10 @@ claude --plugin-dir /path/to/agentic-defi

/agentic-defi:spark supply --network ethereum --asset DAI --amount 1000 --wallet 0x...
/agentic-defi:spark opportunities --network ethereum

/agentic-defi:merkl opportunities --chain-id 1
/agentic-defi:merkl check-rewards --wallet 0x...
/agentic-defi:merkl claim-rewards --wallet 0x... --chain-id 1
```

## Setup
Expand Down
23 changes: 23 additions & 0 deletions commands/merkl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
description: "Merkl rewards — check unclaimed rewards, claim rewards, browse incentivized opportunities"
---

You are executing a Merkl operation. First, invoke the `agentic-defi:merkl-agentic-wallet` skill to load the full protocol context.

Parse the following arguments: $ARGUMENTS

## Subcommands

| Subcommand | Required flags |
|------------|---------------|
| `opportunities` | none (optional: `--chain-id`, `--protocol`, `--token`) |
| `check-rewards` | `--wallet` (optional: `--chain-id`) |
| `claim-rewards` | `--wallet --chain-id` (optional: `--token`) |

## Instructions

1. If `$ARGUMENTS` is empty, show usage help listing all subcommands above.
2. Extract the subcommand (first word) and flags from `$ARGUMENTS`.
3. If any required flags are missing, ask the user for them.
4. Run the script using: `node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js <subcommand> <flags>`
5. Follow the SKILL.md workflow for any MCP tool calls (`execute_tx`, `grant_permissions`).
50 changes: 50 additions & 0 deletions core/merkl/claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { encodeFunctionData } from "viem";
import { DISTRIBUTOR_ABI } from "../../skills/merkl-agentic-wallet/shared/abis.js";
import { getDistributor } from "../../skills/merkl-agentic-wallet/shared/addresses.js";
import { getRewards } from "./rewards.js";
import type { MerklClaimData } from "../types.js";

export async function encodeClaim(
wallet: string,
chainId: number,
tokenFilter?: string
): Promise<MerklClaimData> {
const rewardsData = await getRewards(wallet, chainId);
const chainRewards = rewardsData.find((r) => r.chainId === chainId);

if (!chainRewards || chainRewards.rewards.length === 0) {
throw new Error(`No unclaimed rewards on chain ${chainId}`);
}

let claimable = chainRewards.rewards.filter((r) => r.proofs.length > 0);

if (tokenFilter) {
const filter = tokenFilter.toLowerCase();
claimable = claimable.filter(
(r) =>
r.symbol.toLowerCase() === filter ||
r.address.toLowerCase() === filter
);
if (claimable.length === 0) {
throw new Error(`No unclaimed rewards for token "${tokenFilter}" on chain ${chainId}`);
}
}

// rawAmount is the cumulative amount from the merkle tree — proofs validate against this
const users = claimable.map((r) => r.recipient as `0x${string}`);
const tokens = claimable.map((r) => r.address as `0x${string}`);
const amounts = claimable.map((r) => BigInt(r.rawAmount));
const proofs = claimable.map((r) => r.proofs as `0x${string}`[]);

const data = encodeFunctionData({
abi: DISTRIBUTOR_ABI,
functionName: "claim",
args: [users, tokens, amounts, proofs],
});

return {
chainId,
to: getDistributor(chainId),
data,
};
}
48 changes: 48 additions & 0 deletions core/merkl/opportunities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { merkl } from "../../skills/merkl-agentic-wallet/shared/api.js";
import type { MerklOpportunity } from "../types.js";

export interface GetOpportunitiesOptions {
chainId?: number;
protocol?: string;
token?: string;
limit?: number;
}

export async function getOpportunities(
options: GetOpportunitiesOptions = {}
): Promise<MerklOpportunity[]> {
const { chainId, protocol, token, limit = 10 } = options;

const response = await merkl.opportunities.get({
query: {
status: "LIVE",
items: limit,
...(chainId != null && { chainId: String(chainId) }),
...(protocol != null && { mainProtocolId: protocol }),
...(token != null && { tokens: token }),
sort: "apr",
order: "desc",
},
});

if (!response.data || response.error) {
return [];
}

const opportunities = Array.isArray(response.data) ? response.data : [response.data];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return opportunities.map((opp: any) => ({
name: opp.name,
protocol: opp.protocol?.id ?? opp.mainProtocol ?? null,
action: opp.action,
status: opp.status,
chainId: opp.chainId,
chainName: opp.chain.name,
apr: opp.apr,
tvl: opp.tvl,
dailyRewards: opp.dailyRewards,
tokens: opp.tokens.map((t: { symbol: string; address: string }) => ({ symbol: t.symbol, address: t.address })),
depositUrl: opp.depositUrl ?? null,
}));
}
51 changes: 51 additions & 0 deletions core/merkl/rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { formatUnits } from "viem";
import { merkl } from "../../skills/merkl-agentic-wallet/shared/api.js";
import type { MerklRewardsData } from "../types.js";

export async function getRewards(
wallet: string,
chainId?: number
): Promise<MerklRewardsData[]> {
const chainIds = chainId != null ? [String(chainId)] : [];

const response = await merkl.users({ address: wallet }).rewards.get({
query: {
chainId: chainIds.length > 0 ? chainIds : [],
breakdownPage: 0,
type: "TOKEN",
},
});

if (!response.data || response.error) {
return [];
}

const data = Array.isArray(response.data) ? response.data : [response.data];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return data.map((chainRewards: any) => ({
chainId: chainRewards.chain.id,
chainName: chainRewards.chain.name,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rewards: chainRewards.rewards
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((r: any) => BigInt(r.amount) > BigInt(r.claimed))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((r: any) => ({
address: r.token.address,
symbol: r.token.symbol,
decimals: r.token.decimals,
chainId: r.token.chainId,
amount: formatUnits(BigInt(r.amount), r.token.decimals),
rawAmount: String(r.amount),
claimed: formatUnits(BigInt(r.claimed), r.token.decimals),
pending: formatUnits(BigInt(r.pending), r.token.decimals),
unclaimed: formatUnits(
BigInt(r.amount) - BigInt(r.claimed),
r.token.decimals
),
proofs: r.proofs,
recipient: r.recipient,
})),
}));
}
40 changes: 40 additions & 0 deletions core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,43 @@ export interface PermissionsParams {
label: string;
}>;
}

export interface MerklOpportunity {
name: string;
protocol: string | null;
action: string;
status: string;
chainId: number;
chainName: string;
apr: number;
tvl: number;
dailyRewards: number;
tokens: Array<{ symbol: string; address: string }>;
depositUrl: string | null;
}

export interface MerklRewardToken {
address: string;
symbol: string;
decimals: number;
chainId: number;
amount: string;
rawAmount: string;
claimed: string;
pending: string;
unclaimed: string;
proofs: string[];
recipient: string;
}

export interface MerklRewardsData {
chainId: number;
chainName: string;
rewards: MerklRewardToken[];
}

export interface MerklClaimData {
chainId: number;
to: `0x${string}`;
data: `0x${string}`;
}
Loading