Skip to content

Commit 3a06a56

Browse files
authored
Merge pull request #4 from BootNodeDev/feat/merkl-integration
feat(merkl): add Merkl rewards integration
2 parents fab1b13 + bcef4bc commit 3a06a56

20 files changed

Lines changed: 857 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ The following project-specific skills are available. **Read the relevant SKILL.m
8282

8383
- **Aave V3**`skills/aave-agentic-wallet/SKILL.md`
8484
- **SparkLend**`skills/spark-agentic-wallet/SKILL.md`
85+
- **Merkl**`skills/merkl-agentic-wallet/SKILL.md`
8586

8687
## Adding a New Skill
8788

README.md

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ A collection of protocol-specific skills that teach AI agents (Claude, etc.) how
1717
|----------|--------|-------|
1818
| **Aave V3** | Available | `skills/aave-agentic-wallet/SKILL.md` |
1919
| **SparkLend** | Available | `skills/spark-agentic-wallet/SKILL.md` |
20+
| **Merkl** | Available | `skills/merkl-agentic-wallet/SKILL.md` |
2021
| **Lido** | Coming soon ||
2122
| **Morpho** | Coming soon ||
2223

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

48+
### Merkl
49+
50+
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.
51+
52+
```bash
53+
# Browse top opportunities by APR
54+
node dist/skills/merkl-agentic-wallet/merkl.js opportunities --chain-id 1
55+
56+
# Check unclaimed rewards
57+
node dist/skills/merkl-agentic-wallet/merkl.js check-rewards --wallet 0x...
58+
59+
# Encode a claim transaction
60+
node dist/skills/merkl-agentic-wallet/merkl.js claim-rewards --wallet 0x... --chain-id 1
61+
```
62+
4763
## MCP Server
4864

4965
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).
@@ -76,37 +92,36 @@ npx @bootnodedev/defi-mcp-server
7692
npx @bootnodedev/defi-mcp-server --http --port 3000
7793
```
7894

79-
### Available tools (14)
95+
### Available tools (10)
8096

8197
| Tool | Description |
8298
|------|-------------|
83-
| `aave_get_opportunities` | Get Aave V3 supply/borrow APY rates across networks |
84-
| `aave_check_position` | Check wallet health factor, collateral, and debt |
85-
| `aave_get_permissions` | Get permission params for Aave V3 operations |
86-
| `aave_encode_supply` | Encode approve + supply calldata |
87-
| `aave_encode_borrow` | Encode borrow calldata |
88-
| `aave_encode_repay` | Encode approve + repay calldata |
89-
| `aave_encode_withdraw` | Encode withdraw calldata |
90-
| `spark_get_opportunities` | Get SparkLend supply/borrow APY rates |
91-
| `spark_check_position` | Check wallet health factor, collateral, and debt |
92-
| `spark_get_permissions` | Get permission params for SparkLend operations |
93-
| `spark_encode_supply` | Encode approve + supply calldata |
94-
| `spark_encode_borrow` | Encode borrow calldata |
95-
| `spark_encode_repay` | Encode approve + repay calldata |
96-
| `spark_encode_withdraw` | Encode withdraw calldata |
99+
| `defi_guide` | Overview of all tools, workflows, and key rules — call before your first DeFi operation |
100+
| `aave_get_opportunities` | Aave V3 supply/borrow APY rates, top reserves ranked by supply APY |
101+
| `aave_check_position` | Wallet health factor, collateral, and debt on Aave V3 |
102+
| `aave_get_permissions` | Permission params for Aave V3 — pass result to agentic-wallet grant_permissions |
103+
| `aave_encode` | Encode Aave V3 calldata (supply, borrow, repay, withdraw) |
104+
| `spark_get_opportunities` | SparkLend supply/borrow APY rates |
105+
| `spark_check_position` | Wallet health factor, collateral, and debt on SparkLend |
106+
| `spark_get_permissions` | Permission params for SparkLend |
107+
| `spark_encode` | Encode SparkLend calldata (supply, borrow, repay, withdraw) |
108+
| `merkl` | Merkl reward management — browse opportunities, check rewards, claim rewards |
97109

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

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

116+
The `merkl` tool uses subcommands: `get_opportunities`, `check_rewards`, `claim_rewards`.
117+
104118
## Claude Code Plugin
105119

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

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

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

132147
/agentic-defi:spark supply --network ethereum --asset DAI --amount 1000 --wallet 0x...
133148
/agentic-defi:spark opportunities --network ethereum
149+
150+
/agentic-defi:merkl opportunities --chain-id 1
151+
/agentic-defi:merkl check-rewards --wallet 0x...
152+
/agentic-defi:merkl claim-rewards --wallet 0x... --chain-id 1
134153
```
135154

136155
## Setup

commands/merkl.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
description: "Merkl rewards — check unclaimed rewards, claim rewards, browse incentivized opportunities"
3+
---
4+
5+
You are executing a Merkl operation. First, invoke the `agentic-defi:merkl-agentic-wallet` skill to load the full protocol context.
6+
7+
Parse the following arguments: $ARGUMENTS
8+
9+
## Subcommands
10+
11+
| Subcommand | Required flags |
12+
|------------|---------------|
13+
| `opportunities` | none (optional: `--chain-id`, `--protocol`, `--token`) |
14+
| `check-rewards` | `--wallet` (optional: `--chain-id`) |
15+
| `claim-rewards` | `--wallet --chain-id` (optional: `--token`) |
16+
17+
## Instructions
18+
19+
1. If `$ARGUMENTS` is empty, show usage help listing all subcommands above.
20+
2. Extract the subcommand (first word) and flags from `$ARGUMENTS`.
21+
3. If any required flags are missing, ask the user for them.
22+
4. Run the script using: `node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js <subcommand> <flags>`
23+
5. Follow the SKILL.md workflow for any MCP tool calls (`execute_tx`, `grant_permissions`).

core/merkl/claim.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { encodeFunctionData } from "viem";
2+
import { DISTRIBUTOR_ABI } from "../../skills/merkl-agentic-wallet/shared/abis.js";
3+
import { getDistributor } from "../../skills/merkl-agentic-wallet/shared/addresses.js";
4+
import { getRewards } from "./rewards.js";
5+
import type { MerklClaimData } from "../types.js";
6+
7+
export async function encodeClaim(
8+
wallet: string,
9+
chainId: number,
10+
tokenFilter?: string
11+
): Promise<MerklClaimData> {
12+
const rewardsData = await getRewards(wallet, chainId);
13+
const chainRewards = rewardsData.find((r) => r.chainId === chainId);
14+
15+
if (!chainRewards || chainRewards.rewards.length === 0) {
16+
throw new Error(`No unclaimed rewards on chain ${chainId}`);
17+
}
18+
19+
let claimable = chainRewards.rewards.filter((r) => r.proofs.length > 0);
20+
21+
if (tokenFilter) {
22+
const filter = tokenFilter.toLowerCase();
23+
claimable = claimable.filter(
24+
(r) =>
25+
r.symbol.toLowerCase() === filter ||
26+
r.address.toLowerCase() === filter
27+
);
28+
if (claimable.length === 0) {
29+
throw new Error(`No unclaimed rewards for token "${tokenFilter}" on chain ${chainId}`);
30+
}
31+
}
32+
33+
// rawAmount is the cumulative amount from the merkle tree — proofs validate against this
34+
const users = claimable.map((r) => r.recipient as `0x${string}`);
35+
const tokens = claimable.map((r) => r.address as `0x${string}`);
36+
const amounts = claimable.map((r) => BigInt(r.rawAmount));
37+
const proofs = claimable.map((r) => r.proofs as `0x${string}`[]);
38+
39+
const data = encodeFunctionData({
40+
abi: DISTRIBUTOR_ABI,
41+
functionName: "claim",
42+
args: [users, tokens, amounts, proofs],
43+
});
44+
45+
return {
46+
chainId,
47+
to: getDistributor(chainId),
48+
data,
49+
};
50+
}

core/merkl/opportunities.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { merkl } from "../../skills/merkl-agentic-wallet/shared/api.js";
2+
import type { MerklOpportunity } from "../types.js";
3+
4+
export interface GetOpportunitiesOptions {
5+
chainId?: number;
6+
protocol?: string;
7+
token?: string;
8+
limit?: number;
9+
}
10+
11+
export async function getOpportunities(
12+
options: GetOpportunitiesOptions = {}
13+
): Promise<MerklOpportunity[]> {
14+
const { chainId, protocol, token, limit = 10 } = options;
15+
16+
const response = await merkl.opportunities.get({
17+
query: {
18+
status: "LIVE",
19+
items: limit,
20+
...(chainId != null && { chainId: String(chainId) }),
21+
...(protocol != null && { mainProtocolId: protocol }),
22+
...(token != null && { tokens: token }),
23+
sort: "apr",
24+
order: "desc",
25+
},
26+
});
27+
28+
if (!response.data || response.error) {
29+
return [];
30+
}
31+
32+
const opportunities = Array.isArray(response.data) ? response.data : [response.data];
33+
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
return opportunities.map((opp: any) => ({
36+
name: opp.name,
37+
protocol: opp.protocol?.id ?? opp.mainProtocol ?? null,
38+
action: opp.action,
39+
status: opp.status,
40+
chainId: opp.chainId,
41+
chainName: opp.chain.name,
42+
apr: opp.apr,
43+
tvl: opp.tvl,
44+
dailyRewards: opp.dailyRewards,
45+
tokens: opp.tokens.map((t: { symbol: string; address: string }) => ({ symbol: t.symbol, address: t.address })),
46+
depositUrl: opp.depositUrl ?? null,
47+
}));
48+
}

core/merkl/rewards.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { formatUnits } from "viem";
2+
import { merkl } from "../../skills/merkl-agentic-wallet/shared/api.js";
3+
import type { MerklRewardsData } from "../types.js";
4+
5+
export async function getRewards(
6+
wallet: string,
7+
chainId?: number
8+
): Promise<MerklRewardsData[]> {
9+
const chainIds = chainId != null ? [String(chainId)] : [];
10+
11+
const response = await merkl.users({ address: wallet }).rewards.get({
12+
query: {
13+
chainId: chainIds.length > 0 ? chainIds : [],
14+
breakdownPage: 0,
15+
type: "TOKEN",
16+
},
17+
});
18+
19+
if (!response.data || response.error) {
20+
return [];
21+
}
22+
23+
const data = Array.isArray(response.data) ? response.data : [response.data];
24+
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
return data.map((chainRewards: any) => ({
27+
chainId: chainRewards.chain.id,
28+
chainName: chainRewards.chain.name,
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
rewards: chainRewards.rewards
31+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32+
.filter((r: any) => BigInt(r.amount) > BigInt(r.claimed))
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
.map((r: any) => ({
35+
address: r.token.address,
36+
symbol: r.token.symbol,
37+
decimals: r.token.decimals,
38+
chainId: r.token.chainId,
39+
amount: formatUnits(BigInt(r.amount), r.token.decimals),
40+
rawAmount: String(r.amount),
41+
claimed: formatUnits(BigInt(r.claimed), r.token.decimals),
42+
pending: formatUnits(BigInt(r.pending), r.token.decimals),
43+
unclaimed: formatUnits(
44+
BigInt(r.amount) - BigInt(r.claimed),
45+
r.token.decimals
46+
),
47+
proofs: r.proofs,
48+
recipient: r.recipient,
49+
})),
50+
}));
51+
}

core/types.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,43 @@ export interface PermissionsParams {
5353
label: string;
5454
}>;
5555
}
56+
57+
export interface MerklOpportunity {
58+
name: string;
59+
protocol: string | null;
60+
action: string;
61+
status: string;
62+
chainId: number;
63+
chainName: string;
64+
apr: number;
65+
tvl: number;
66+
dailyRewards: number;
67+
tokens: Array<{ symbol: string; address: string }>;
68+
depositUrl: string | null;
69+
}
70+
71+
export interface MerklRewardToken {
72+
address: string;
73+
symbol: string;
74+
decimals: number;
75+
chainId: number;
76+
amount: string;
77+
rawAmount: string;
78+
claimed: string;
79+
pending: string;
80+
unclaimed: string;
81+
proofs: string[];
82+
recipient: string;
83+
}
84+
85+
export interface MerklRewardsData {
86+
chainId: number;
87+
chainName: string;
88+
rewards: MerklRewardToken[];
89+
}
90+
91+
export interface MerklClaimData {
92+
chainId: number;
93+
to: `0x${string}`;
94+
data: `0x${string}`;
95+
}

0 commit comments

Comments
 (0)