diff --git a/CLAUDE.md b/CLAUDE.md index 5c23f8d..15e87d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index a72b9d6..47c5eea 100644 --- a/README.md +++ b/README.md @@ -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 | — | @@ -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). @@ -76,24 +92,20 @@ 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: @@ -101,12 +113,15 @@ The `*_get_opportunities` tools return the top 10 reserves per network by defaul - `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 @@ -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 diff --git a/commands/merkl.md b/commands/merkl.md new file mode 100644 index 0000000..03ef89e --- /dev/null +++ b/commands/merkl.md @@ -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 ` +5. Follow the SKILL.md workflow for any MCP tool calls (`execute_tx`, `grant_permissions`). diff --git a/core/merkl/claim.ts b/core/merkl/claim.ts new file mode 100644 index 0000000..5a12d7d --- /dev/null +++ b/core/merkl/claim.ts @@ -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 { + 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, + }; +} diff --git a/core/merkl/opportunities.ts b/core/merkl/opportunities.ts new file mode 100644 index 0000000..9ed688f --- /dev/null +++ b/core/merkl/opportunities.ts @@ -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 { + 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, + })); +} diff --git a/core/merkl/rewards.ts b/core/merkl/rewards.ts new file mode 100644 index 0000000..75524f4 --- /dev/null +++ b/core/merkl/rewards.ts @@ -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 { + 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, + })), + })); +} diff --git a/core/types.ts b/core/types.ts index 3c7f096..a8ff59b 100644 --- a/core/types.ts +++ b/core/types.ts @@ -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}`; +} diff --git a/mcp-server/prompts.ts b/mcp-server/prompts.ts index 718dd36..200b7a5 100644 --- a/mcp-server/prompts.ts +++ b/mcp-server/prompts.ts @@ -5,28 +5,43 @@ export const DEFI_GUIDE = `DeFi MCP Server — Quick Reference PROTOCOLS: - Aave V3 (aave_*): ethereum, arbitrum, optimism, base, polygon - SparkLend (spark_*): ethereum, gnosis -Do NOT mix — use aave_* tools for Aave, spark_* tools for SparkLend. +- Merkl (merkl): reward discovery and claiming across 50+ chains +Do NOT mix — use aave_* tools for Aave, spark_* tools for SparkLend, merkl tool for rewards. -TOOLS (same pattern for both protocols): +TOOLS (Aave/SparkLend — same pattern for both): - *_get_opportunities → compare supply/borrow APY rates - *_check_position → health factor, collateral, debt (call BEFORE borrow/withdraw) - *_get_permissions → generate params for agentic-wallet grant_permissions - *_encode → build transaction calldata for supply/borrow/repay/withdraw -WORKFLOW for any operation: +MERKL TOOL (single tool, 3 subcommands): +- merkl get_opportunities → browse incentivized opportunities across protocols +- merkl check_rewards → view unclaimed Merkl rewards for a wallet +- merkl claim_rewards → encode claim tx, submit via agentic-wallet execute_tx + +WORKFLOW for Aave/SparkLend operations: 1. *_check_position → know current state and health factor 2. *_get_opportunities → find the right asset (if needed) 3. *_get_permissions → grant agentic-wallet permission to act 4. *_encode → get calldata, submit via agentic-wallet +WORKFLOW for Merkl rewards: +1. merkl check_rewards → see what's claimable +2. merkl claim_rewards → encode claim, submit via agentic-wallet execute_tx + +CROSS-PROTOCOL: Merkl incentivizes positions on Aave, SparkLend, and many other protocols. +If a Merkl opportunity is on Aave or SparkLend, use those skills to act on it. + TRANSACTION TYPES: - supply/repay → returns {calls: [...]} array → use execute_batch_tx (includes approve) - borrow/withdraw → returns single {to, data, value} → use execute_tx +- Merkl claim → returns single {to, data} → use execute_tx (no approve needed) KEY RULES: - Health factor < 1.0 = liquidation. Keep above 1.5. - "max" on repay = full debt. "max" on withdraw = full balance. -- Always approve before supply/repay (handled automatically by encode).`; +- Always approve before supply/repay (handled automatically by encode). +- Merkl proofs expire ~4 hours — always fetch fresh before claiming.`; function supplyWorkflow(protocol: string, prefix: string) { return `How to supply assets on ${protocol}: @@ -123,6 +138,44 @@ function withdrawWorkflow(protocol: string, prefix: string) { → Confirm health factor is still safe if debt exists`; } +function merklCheckRewardsWorkflow() { + return `How to check and claim Merkl rewards: + +1. Check unclaimed rewards: + → merkl(subcommand: "check_rewards", wallet, chainId?) + → Shows unclaimed rewards per token per chain + +2. Claim rewards: + → merkl(subcommand: "claim_rewards", wallet, chainId, token?) + → Returns {to, data} for the Distributor contract + → Submit via agentic-wallet execute_tx + +3. Verify: + → merkl(subcommand: "check_rewards", wallet, chainId) + → Confirm rewards were claimed + +Notes: +- Proofs expire ~4 hours — always encode + submit promptly +- Omit chainId on check_rewards to scan all chains +- Omit token on claim_rewards to claim all tokens at once`; +} + +function merklOpportunitiesWorkflow() { + return `How to find Merkl-incentivized opportunities: + +1. Browse opportunities: + → merkl(subcommand: "get_opportunities", chainId?, protocol?, token?, limit?) + → Returns opportunities ranked by APR + +2. If opportunity is on Aave or SparkLend: + → Use the corresponding aave_* or spark_* tools to supply/lend + → The Merkl rewards accrue automatically once you have the position + +3. If opportunity is on another protocol: + → Report the opportunity details to the user + → Include the depositUrl for manual action`; +} + export function registerPrompts(server: McpServer) { server.registerPrompt("defi_guide", { description: "Overview of all DeFi tools, workflows, and key rules", @@ -155,4 +208,16 @@ export function registerPrompts(server: McpServer) { messages: [{ role: "user", content: { type: "text", text: withdrawWorkflow(protocol, prefix) } }], })); } + + server.registerPrompt("merkl_check_rewards_workflow", { + description: "Step-by-step guide to check and claim Merkl rewards", + }, async () => ({ + messages: [{ role: "user", content: { type: "text", text: merklCheckRewardsWorkflow() } }], + })); + + server.registerPrompt("merkl_opportunities_workflow", { + description: "Step-by-step guide to find Merkl-incentivized opportunities", + }, async () => ({ + messages: [{ role: "user", content: { type: "text", text: merklOpportunitiesWorkflow() } }], + })); } diff --git a/mcp-server/server.ts b/mcp-server/server.ts index 58445e9..d507e19 100644 --- a/mcp-server/server.ts +++ b/mcp-server/server.ts @@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import * as z from "zod/v4"; import { registerAaveTools } from "./tools/aave.js"; import { registerSparkTools } from "./tools/spark.js"; +import { registerMerklTools } from "./tools/merkl.js"; import { registerPrompts, DEFI_GUIDE } from "./prompts.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -30,6 +31,7 @@ export function createServer(): McpServer { registerAaveTools(server); registerSparkTools(server); + registerMerklTools(server); registerPrompts(server); return server; diff --git a/mcp-server/tools/merkl.ts b/mcp-server/tools/merkl.ts new file mode 100644 index 0000000..ae23ee1 --- /dev/null +++ b/mcp-server/tools/merkl.ts @@ -0,0 +1,55 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as z from "zod/v4"; +import { getOpportunities } from "../../core/merkl/opportunities.js"; +import { getRewards } from "../../core/merkl/rewards.js"; +import { encodeClaim } from "../../core/merkl/claim.js"; +import { address, json, DEFAULT_LIMIT } from "./shared.js"; + +const subcommand = z.enum(["get_opportunities", "check_rewards", "claim_rewards"]); + +export function registerMerklTools(server: McpServer) { + server.registerTool( + "merkl", + { + description: + "Merkl reward management and opportunity discovery. " + + "Subcommands: get_opportunities (browse incentivized DeFi opportunities), " + + "check_rewards (view unclaimed rewards for a wallet), " + + "claim_rewards (encode claim transaction for unclaimed rewards).", + inputSchema: z.object({ + subcommand, + wallet: address.optional().describe("Wallet address (required for check_rewards and claim_rewards)"), + chainId: z.number().optional().describe("Chain ID to filter by (required for claim_rewards)"), + protocol: z.string().optional().describe("Protocol ID filter for get_opportunities (e.g. 'aave', 'morpho')"), + token: z.string().optional().describe("Token symbol or address filter"), + limit: z.number().optional().describe("Max results for get_opportunities (default: 10)"), + }), + }, + async ({ subcommand: cmd, wallet, chainId, protocol, token, limit }) => { + switch (cmd) { + case "get_opportunities": { + const results = await getOpportunities({ + chainId, + protocol, + token, + limit: limit ?? DEFAULT_LIMIT, + }); + return json(results); + } + + case "check_rewards": { + if (!wallet) throw new Error("wallet is required for check_rewards"); + const results = await getRewards(wallet, chainId); + return json(results); + } + + case "claim_rewards": { + if (!wallet) throw new Error("wallet is required for claim_rewards"); + if (chainId == null) throw new Error("chainId is required for claim_rewards"); + const result = await encodeClaim(wallet, chainId, token); + return json(result); + } + } + } + ); +} diff --git a/package-lock.json b/package-lock.json index 5d404d9..911fd30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,23 @@ { - "name": "@bootnodedev/agentic-defi", - "version": "1.1.0", + "name": "@bootnodedev/defi-mcp-server", + "version": "0.1.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@bootnodedev/agentic-defi", - "version": "1.1.0", + "name": "@bootnodedev/defi-mcp-server", + "version": "0.1.0-beta.3", "license": "ISC", "dependencies": { + "@merkl/api": "^1.17.6", "@modelcontextprotocol/sdk": "^1.28.0", "@types/node": "^22.0.0", "typescript": "^5.7.0", "viem": "^2.47.6", "zod": "^4.3.6" + }, + "bin": { + "defi-mcp-server": "dist/mcp-server/index.js" } }, "node_modules/@adraffy/ens-normalize": { @@ -22,6 +26,24 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@elysiajs/eden": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.4.0.tgz", + "integrity": "sha512-Elubsibe0mGK1TLsCrG+fXHorxVfS/YvWP/uDv/8bniideMgREH3yp4Hua5zQkejM3HG9nv2EfajsJ/wfqszuA==", + "peerDependencies": { + "elysia": ">= 1.4.0-exp.0" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -33,6 +55,15 @@ "hono": "^4" } }, + "node_modules/@merkl/api": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/@merkl/api/-/api-1.17.6.tgz", + "integrity": "sha512-dZez0YmsFkAo0obs/gOum7MBW5Pfd+Lm2AEf87iU05yoSrYTqVsj1+O6zOejH9hyNUF0QpLgB8c4qSUc+pcBkQ==", + "dependencies": { + "@elysiajs/eden": "1.4.0", + "elysia": "1.4.19" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", @@ -147,6 +178,35 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "peer": true + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "peer": true + }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", @@ -385,6 +445,45 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/elysia": { + "version": "1.4.19", + "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.4.19.tgz", + "integrity": "sha512-DZb9y8FnWyX5IuqY44SvqAV0DjJ15NeCWHrLdgXrKgTPDPsl3VNwWHqrEr9bmnOCpg1vh6QUvAX/tcxNj88jLA==", + "dependencies": { + "cookie": "^1.1.1", + "exact-mirror": "0.2.5", + "fast-decode-uri-component": "^1.0.1", + "memoirist": "^0.4.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">= 0.34.0 < 1", + "@types/bun": ">= 1.2.0", + "exact-mirror": ">= 0.0.9", + "file-type": ">= 20.0.0", + "openapi-types": ">= 12.0.0", + "typescript": ">= 5.0.0" + }, + "peerDependenciesMeta": { + "@types/bun": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/elysia/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -458,6 +557,19 @@ "node": ">=18.0.0" } }, + "node_modules/exact-mirror": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.2.5.tgz", + "integrity": "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ==", + "peerDependencies": { + "@sinclair/typebox": "^0.34.15" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + } + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -517,6 +629,11 @@ "express": ">= 4.11" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -537,6 +654,24 @@ } ] }, + "node_modules/file-type": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.0.tgz", + "integrity": "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw==", + "peer": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -691,6 +826,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -771,6 +926,11 @@ "node": ">= 0.8" } }, + "node_modules/memoirist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.4.0.tgz", + "integrity": "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -856,6 +1016,12 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, "node_modules/ox": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.7.tgz", @@ -1138,6 +1304,22 @@ "node": ">= 0.8" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1146,6 +1328,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "peer": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1172,6 +1372,18 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index fbccf1a..ddae361 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build": "tsc --project \"${CLAUDE_PLUGIN_ROOT:-.}/tsconfig.json\" --outDir dist" }, "dependencies": { + "@merkl/api": "^1.17.6", "@modelcontextprotocol/sdk": "^1.28.0", "@types/node": "^22.0.0", "typescript": "^5.7.0", diff --git a/skills/merkl-agentic-wallet/SKILL.md b/skills/merkl-agentic-wallet/SKILL.md new file mode 100644 index 0000000..881647d --- /dev/null +++ b/skills/merkl-agentic-wallet/SKILL.md @@ -0,0 +1,104 @@ +# Merkl — Reward Discovery & Claiming + +Merkl is a DeFi incentive platform that distributes rewards across 50+ chains. Unlike Aave or SparkLend, Merkl is **not a lending protocol** — it's a reward distribution layer that incentivizes positions on other protocols. + +## What You Can Do + +1. **Browse incentivized opportunities** — find DeFi positions earning Merkl rewards +2. **Check unclaimed rewards** — see pending/claimable rewards for a wallet +3. **Claim rewards** — encode and submit claim transactions + +## Transaction Tools + +- `execute_tx` — submit a single claim transaction (no approve needed) + +## CLI Scripts + +### Browse Opportunities + +```bash +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js opportunities [--chain-id ] [--protocol ] [--token ] +``` + +Examples: +```bash +# All live opportunities +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js opportunities + +# Aave opportunities on Ethereum +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js opportunities --chain-id 1 --protocol aave + +# USDC opportunities +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js opportunities --token USDC +``` + +### Check Rewards + +```bash +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js check-rewards --wallet [--chain-id ] +``` + +Examples: +```bash +# All chains +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js check-rewards --wallet 0x1234... + +# Specific chain +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js check-rewards --wallet 0x1234... --chain-id 1 +``` + +### Claim Rewards + +```bash +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js claim-rewards --wallet --chain-id [--token ] +``` + +Examples: +```bash +# Claim all rewards on Ethereum +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js claim-rewards --wallet 0x1234... --chain-id 1 + +# Claim specific token +node ${CLAUDE_PLUGIN_DATA}/dist/skills/merkl-agentic-wallet/merkl.js claim-rewards --wallet 0x1234... --chain-id 1 --token USDC +``` + +## Distributor Contract + +**Address:** `0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae` (same proxy on most chains) + +### Claim ABI + +```typescript +claim(address[] users, address[] tokens, uint256[] amounts, bytes32[][] proofs) +``` + +- `users` — recipient addresses (from API response) +- `tokens` — reward token addresses +- `amounts` — cumulative amounts (raw bigint, not human-readable) +- `proofs` — merkle proofs per token (from API response) + +## Cross-Protocol Guidance + +Merkl incentivizes positions on many protocols. When an opportunity shows: + +- **Protocol: aave** → Use the `/aave` skill to supply/lend. Merkl rewards accrue automatically. +- **Protocol: spark** → Use the `/spark` skill. Same behavior. +- **Other protocols** → Report the opportunity to the user with the deposit URL. + +## Workflow + +1. Check wallet via `list_wallets` to get sessionId and address +2. Check rewards: `merkl check-rewards --wallet ` +3. If rewards exist, claim: `merkl claim-rewards --wallet --chain-id ` +4. Submit claim via `execute_tx` with the returned {to, data, value: "0"} +5. Verify: `merkl check-rewards --wallet --chain-id ` again + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Merkle proofs expired | Proofs are valid ~4 hours. Always encode + submit promptly. | +| Missing `--chain-id` on claim | Chain ID is required for claiming — the Distributor is per-chain. | +| Trying to approve before claim | No approve needed — the Distributor sends tokens directly. | +| Using `execute_batch_tx` for claim | Use `execute_tx` — claim is a single transaction. | +| Claiming with no unclaimed rewards | Always `check-rewards` first to verify there are claimable amounts. | diff --git a/skills/merkl-agentic-wallet/merkl.ts b/skills/merkl-agentic-wallet/merkl.ts new file mode 100644 index 0000000..bc2365e --- /dev/null +++ b/skills/merkl-agentic-wallet/merkl.ts @@ -0,0 +1,64 @@ +// Usage: node ${process.env.CLAUDE_PLUGIN_DATA ?? "."}/dist/skills/merkl-agentic-wallet/merkl.js [options] + +import { getOpportunities } from "./scripts/get-opportunities.js"; +import { checkRewards } from "./scripts/check-rewards.js"; +import { claimRewards } from "./scripts/claim-rewards.js"; + +function parseArgs(argv: string[]): Record { + const args: Record = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i].startsWith("--") && i + 1 < argv.length) { + args[argv[i].slice(2)] = argv[i + 1]; + i++; + } + } + return args; +} + +function usage() { + console.log(` +Merkl CLI — node ${process.env.CLAUDE_PLUGIN_DATA ?? "."}/dist/skills/merkl-agentic-wallet/merkl.js [options] + +Subcommands: + opportunities [--chain-id ] [--protocol ] [--token ] + Browse Merkl-incentivized opportunities. + + check-rewards --wallet [--chain-id ] + Show unclaimed Merkl rewards for a wallet. + + claim-rewards --wallet --chain-id [--token ] + Encode a claim transaction for unclaimed Merkl rewards. +`); +} + +const [subcommand, ...rest] = process.argv.slice(2); +const args = parseArgs(rest); + +switch (subcommand) { + case "opportunities": + await getOpportunities( + args["chain-id"] ? Number(args["chain-id"]) : undefined, + args.protocol, + args.token + ); + break; + + case "check-rewards": + if (!args.wallet) { console.error("Missing --wallet"); process.exit(1); } + await checkRewards( + args.wallet, + args["chain-id"] ? Number(args["chain-id"]) : undefined + ); + break; + + case "claim-rewards": + if (!args.wallet || !args["chain-id"]) { + console.error("Missing --wallet or --chain-id"); + process.exit(1); + } + await claimRewards(args.wallet, Number(args["chain-id"]), args.token); + break; + + default: + usage(); +} diff --git a/skills/merkl-agentic-wallet/scripts/check-rewards.ts b/skills/merkl-agentic-wallet/scripts/check-rewards.ts new file mode 100644 index 0000000..949c823 --- /dev/null +++ b/skills/merkl-agentic-wallet/scripts/check-rewards.ts @@ -0,0 +1,21 @@ +import { getRewards } from "../../../core/merkl/rewards.js"; + +export async function checkRewards(wallet: string, chainId?: number) { + const results = await getRewards(wallet, chainId); + + if (results.length === 0) { + console.log("\nNo unclaimed Merkl rewards found."); + return; + } + + for (const { chainId: cid, chainName, rewards } of results) { + console.log(`\n=== ${chainName} (chain ${cid}) ===`); + for (const r of rewards) { + console.log( + ` ${r.symbol.padEnd(12)} Unclaimed: ${r.unclaimed.padStart(20)}` + + ` | Pending: ${r.pending.padStart(15)}` + + ` | Total: ${r.amount.padStart(20)}` + ); + } + } +} diff --git a/skills/merkl-agentic-wallet/scripts/claim-rewards.ts b/skills/merkl-agentic-wallet/scripts/claim-rewards.ts new file mode 100644 index 0000000..ff8b67e --- /dev/null +++ b/skills/merkl-agentic-wallet/scripts/claim-rewards.ts @@ -0,0 +1,24 @@ +import { encodeClaim } from "../../../core/merkl/claim.js"; + +export async function claimRewards( + wallet: string, + chainId: number, + token?: string +) { + const result = await encodeClaim(wallet, chainId, token); + + console.log(`\nClaim Merkl rewards on chain ${chainId}`); + console.log("Call execute_tx with (use sessionId from list_wallets):"); + console.log( + JSON.stringify( + { + sessionId: "", + to: result.to, + data: result.data, + value: "0", + }, + null, + 2 + ) + ); +} diff --git a/skills/merkl-agentic-wallet/scripts/get-opportunities.ts b/skills/merkl-agentic-wallet/scripts/get-opportunities.ts new file mode 100644 index 0000000..dedb3ac --- /dev/null +++ b/skills/merkl-agentic-wallet/scripts/get-opportunities.ts @@ -0,0 +1,25 @@ +import { getOpportunities as _getOpportunities } from "../../../core/merkl/opportunities.js"; + +export async function getOpportunities( + chainId?: number, + protocol?: string, + token?: string +) { + const results = await _getOpportunities({ chainId, protocol, token }); + + if (results.length === 0) { + console.log("\nNo Merkl opportunities found for the given filters."); + return; + } + + console.log(`\n=== MERKL OPPORTUNITIES (${results.length}) ===`); + for (const opp of results) { + const proto = opp.protocol ?? "unknown"; + const tokens = opp.tokens.map((t) => t.symbol).join(", "); + console.log( + ` ${opp.name.slice(0, 40).padEnd(42)} APR: ${opp.apr.toFixed(2).padStart(8)}%` + + ` | TVL: $${(opp.tvl / 1e6).toFixed(1)}M` + + ` | ${opp.chainName} | ${proto} | ${tokens}` + ); + } +} diff --git a/skills/merkl-agentic-wallet/shared/abis.ts b/skills/merkl-agentic-wallet/shared/abis.ts new file mode 100644 index 0000000..b05a5c2 --- /dev/null +++ b/skills/merkl-agentic-wallet/shared/abis.ts @@ -0,0 +1,14 @@ +export const DISTRIBUTOR_ABI = [ + { + name: "claim", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "users", type: "address[]" }, + { name: "tokens", type: "address[]" }, + { name: "amounts", type: "uint256[]" }, + { name: "proofs", type: "bytes32[][]" }, + ], + outputs: [], + }, +] as const; diff --git a/skills/merkl-agentic-wallet/shared/addresses.ts b/skills/merkl-agentic-wallet/shared/addresses.ts new file mode 100644 index 0000000..04c7af4 --- /dev/null +++ b/skills/merkl-agentic-wallet/shared/addresses.ts @@ -0,0 +1,10 @@ +// Distributor proxy address — same on the vast majority of chains. +const DEFAULT_DISTRIBUTOR: `0x${string}` = "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"; + +const DISTRIBUTOR_OVERRIDES: Record = { + // Add chain-specific overrides here if needed +}; + +export function getDistributor(chainId: number): `0x${string}` { + return DISTRIBUTOR_OVERRIDES[chainId] ?? DEFAULT_DISTRIBUTOR; +} diff --git a/skills/merkl-agentic-wallet/shared/api.ts b/skills/merkl-agentic-wallet/shared/api.ts new file mode 100644 index 0000000..b3bc249 --- /dev/null +++ b/skills/merkl-agentic-wallet/shared/api.ts @@ -0,0 +1,5 @@ +import { MerklApi } from "@merkl/api"; + +const merkl = (MerklApi("https://api.merkl.xyz") as any).v4; + +export { merkl };