Metalend
Parcourez, déposez, retirez et rééquilibrez les positions en stablecoins sur Aave, Morpho et Euler. Suivez les rendements, les soldes, les récompenses et les paramètres du rééquilibreur sur toutes les principales chaînes EVM.
Documentation
Getting Started
This endpoint exposes MetaLend's rebalancer as MCP tools: the same read-only discovery tools as /mcp/readonly, plus the full write flow — deposits, withdrawals, and rebalancer config updates. Discovery tools need no auth; write flows require a wallet JWT and a signature you produce yourself. This server never holds a private key.
Endpoint https://mcp.metalend.tech/mcp
Installation
Claude Code
Add MetaLend as a remote MCP server straight from the CLI.
claude mcp add --transport http metalend https://mcp.metalend.tech/mcp
Choose “No auth” at the connector level — the wallet JWT used by write tools is passed as a tool argument (see Authentication), not configured on the connector.
Claude Desktop
Add MetaLend as a custom connector from Claude's settings.
- Open Customize → Connectors, click + then Add custom connector
- Paste the URL below and click Add
https://mcp.metalend.tech/mcp
Team and Enterprise owners use Organization settings → Connectors → Add → Custom → Web instead.
Cursor
Register MetaLend as a remote MCP server in your Cursor config.
- Open (or create)
~/.cursor/mcp.json - Add the block below, then reload Cursor
{
"mcpServers": {
"metalend": {
"url": "https://mcp.metalend.tech/mcp"
}
}
}
ChatGPT
Connect MetaLend as a custom MCP connector in Developer mode.
- Turn on Developer mode (menu labels vary by rollout — look under Settings → Apps & Connectors → Advanced Settings, or Settings → Plugins)
- Click Browse plugins (or Connectors) → + to add a new connector, and paste the URL below as the MCP server URL
- For Authentication, choose “No auth” — MetaLend's MCP server doesn't require a client-supplied token
https://mcp.metalend.tech/mcp
Developer mode is a ChatGPT beta that's still rolling out, so the toggle's exact location and plan availability can vary — check your own Settings if you don't see it yet.
Authentication
Discovery tools (pool listings, balances, config, rewards, costs) need no auth — pass a walletAddress and read. Every write flow (deposit, withdrawal, config update) additionally needs a wallet JWT:
- Call
get_auth_challengewith thewalletAddressto get a Sign-In-With-Ethereummessage. - Sign that message with the wallet's own signer using
personal_sign(EIP-191). This server never sees a private key — you sign, it relays. - Call
submit_auth_verifywith the signature to exchange it for ajwt. - Pass that
jwtexplicitly on everysubmit_deposit/submit_withdrawal/submit_configcall for that same wallet. It is never cached server-side.
The jwt is a long-lived bearer credential — it is issued with roughly a 115-day lifetime (the response's expiresAt, decoded from the token's own exp claim, is the authoritative value). Do not treat it as an ephemeral per-request token and do not re-run the SIWE flow before each call: store it, protect it like a password (anyone holding it can submit writes for that wallet until it expires), and reuse the same jwt across every write call for that wallet until expiresAt passes.
A jwt is only valid for the exact walletAddress that produced the signature — a mismatched jwt / walletAddress pair is rejected with a 403. For a smart-contract wallet, SIWE, withdrawal, and config signatures are chain-scoped (ERC-1271 / ERC-6492): the wallet must have a valid signer on the relevant chain(s), and prepare_config in particular can require all of its domainIds ' chains at once. Deposit signatures accept only a raw EOA-style ECDSA signature — a smart-contract wallet must use prepare_deposit 's method: "approval" instead.
Write Flow
Deposits, withdrawals, and config updates all follow the same two-step shape: prepare_* then submit_*. This server never holds a private key.
prepare_*computes everything (EIP-712 typed data, a config signing hash, or on-chainapprove()parameters), runs every pre-flight check it can — chain minimums, pool liquidity, funded-pool invariants — and returns the exact bytes to sign. It never signs and never moves funds.- You sign the returned payload with the wallet's own signer.
submit_*takes the signature you produced plus yourjwtand relays it upstream. This is the step that moves funds or changes config.submit_depositandsubmit_withdrawalreturn atrackingIdto poll withget_deposit_status/get_withdrawal_status;submit_confighas no tracking ID and no status poller — confirm an uncertain config update by re-readingget_config.
Approval-based deposits (USDT, RLUSD, USDG, USDE, or any token from a smart-contract wallet) skip the signature entirely: prepare_deposit returns approve() parameters your wallet broadcasts on-chain itself, then submit_deposit is called with no signature fields.
After a deposit, poll get_deposit_status; after a withdrawal, get_withdrawal_status. A deposit that reaches BRIDGING is effectively done — funds are crossing chains via CCTP and you should not keep polling for the bridge itself to finish. A config update has no poller: if submit_config times out or its outcome is otherwise unclear, check get_config to see whether it landed.
Rate Limits
Each tool carries its own per-minute limit and a minimum spacing between individual calls (a “burst” gate), scoped per caller IP — see each tool's spec line below for its exact figures. A rate-limited call returns a normal tool result with isError: true and a message stating how many seconds to wait before retrying, not an HTTP error status, so a well-behaved client can read the reason directly instead of seeing a bare transport failure.
Tools
list_pools
List rebalancer pools
read‑only 20 calls/min 1 call / 3s
List all pools MetaLend's rebalancer can deposit into: protocol (Aave, Morpho, Euler), chain, APY breakdown (native/rewards/total/net-of-fee), TVL, liquidity, and the signData (protocolId, poolAddress, domainId) identifying each pool. Does not return a wallet's balances or configuration — use get_balances / get_config for that. Filter out pools where blacklisted is true.
No parameters.
get_balances
Get rebalancer balances
read‑only 5 calls/min 1 call / 12s
Get a wallet's deployed rebalancer balances broken down per token, per chain, and per protocol/pool, including net earnings and blended APY. Fails if the wallet has no rebalancer yet — use get_default_config to see what a first-time config would look like.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
tokens | string | optional | Comma-separated token symbols to restrict to (e.g. USDC,USDT). Omit for all supported tokens. |
get_bridge_balances
Get bridge balances
read‑only 20 calls/min 1 call / 3s
Get a wallet's in-transit USDC bridge balances (funds moving cross-chain toward a destination pool), including estimated completion time. Does not include already-settled rebalancer balances — use get_balances for those. Only applies to USDC.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
get_config
Get rebalancer config
read‑only 20 calls/min 1 call / 3s
Get a wallet's current rebalancer configuration per token: which protocols/pools/chains are enabled, rebalance frequency, lifetime deposits/withdrawals/fees, and spending cap. Also returns rebalancerAddress and rebalancingManagerAddress. Does not return live balances — use get_balances for that.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
get_default_config
Get default rebalancer config for a token
read‑only 20 calls/min 1 call / 3s
Get MetaLend's recommended default rebalancer configuration for a given token symbol: rebalancingManagerAddress plus recommended protocolIds/poolAddresses/domainIds. Intended for wallets with no existing config yet — for an existing wallet's current config use get_config instead.
| Parameter | Type | Description | |
|---|---|---|---|
token | string | required | Token symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD. |
get_rewards
Get rebalancer rewards
read‑only 20 calls/min 1 call / 3s
Get a wallet's aggregated rewards/earnings across all reward sources, including claimed/available USD totals. Does not include base rebalancer yield/APY earnings — those are in get_balances ' netEarning field.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
reloadChain | string | optional | Chain name to force a fresh reload for (e.g. BASE). Omit to use cached data. |
get_transaction_costs
Get transaction costs
read‑only 20 calls/min 1 call / 3s
Get constant on-behalf deposit/withdraw gas costs and minimum deposit/withdraw amounts for a token, broken down per supported chain. Check a deposit/withdrawal amount against the chain's minimum before preparing it.
| Parameter | Type | Description | |
|---|---|---|---|
token | string | required | Token symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD. |
get_token_info
Get token info
read‑only 6 calls/min 1 call / 10s
Get a token's on-chain metadata (display name, decimals, EIP-712 version) for a given chain. Also used to resolve the decimals a raw amount is expressed in.
| Parameter | Type | Description | |
|---|---|---|---|
token | string | required | Token symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD. |
chain | string | required | Chain name, e.g. BASE, ETHEREUM, POLYGON. |
get_auth_challenge
Get SIWE auth challenge
write 10 calls/min 1 call / 6s
Get a Sign-In-With-Ethereum challenge message for a wallet, required before any deposit, withdrawal, or config update. Sign the returned message with personal_sign (EIP-191) using the wallet's own signer, then pass it to submit_auth_verify. Does not authenticate anything by itself. Repeated calls for the same wallet within a few minutes return the same message, not a fresh one — the backend keeps only one pending challenge per wallet, and reissuing would invalidate whatever an earlier caller is about to sign.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
chain | string | optional | Cosmetic only — shown as the “Chain ID:” line in the SIWE message text and does not need to match the chain passed to submit_auth_verify. Defaults to ETHEREUM. |
submit_auth_verify
Verify SIWE signature and get a JWT
write 5 calls/min 1 call / 12s
Exchange a signed SIWE challenge (from get_auth_challenge) for a jwt scoped to that wallet. Pass the jwt explicitly as the jwt argument to submit_deposit / submit_withdrawal / submit_config for that same wallet — this server never stores it. The token is long-lived (issued with roughly a 115-day lifetime; the response's expiresAt, decoded from the JWT's own exp claim, is authoritative), so store and protect it like a bearer credential and reuse the same one across every write call for that wallet until expiresAt passes rather than re-running this flow. A jwt used for a different wallet's write call is rejected with a 403.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
signature | string | required | personal_sign signature of the SIWE message from get_auth_challenge. |
chain | string | optional | For an EOA this is irrelevant (recovery is chain-agnostic). For a smart-contract wallet it MUST be the chain it has (or would have, via ERC-6492 counterfactual deployment) a valid signer on — it picks which chain's RPC validates the signature (ERC-1271/6492). Defaults to ETHEREUM. |
prepare_deposit
Prepare a deposit
read‑only 6 calls/min 1 call / 10s
Build everything needed to deposit into the rebalancer, without signing. Gasless tokens (USDC, MUSD, PYUSD) get an EIP-712 ReceiveWithAuthorization payload to sign; approval-only tokens (USDT, RLUSD, USDG, USDE) — and any smart-contract wallet, for any token — get on-chain approve() parameters instead, which your wallet must broadcast itself (this server has no RPC access). Requires a signed rebalancer config for the token already (run prepare_config / submit_config first if get_config shows none) and validates the amount against the chain minimum before returning anything. A fixed on-behalf gas fee is deducted before the rebalancer credits the deposit — expectedCreditedAmountRaw in the response is what actually shows up in get_balances, not the full amount.
| Parameter | Type | Description | |
|---|---|---|---|
chain | string | required | Originating chain of the deposit: where approve() is broadcast, or the chain the EIP-712 signature must validate on. Not where the rebalancer ultimately invests the funds. |
token | string | required | Token symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD. |
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
amount | string | required | Raw token amount as a decimal-digit string (smallest denomination). Use get_token_info for decimals. |
method | "signature" | "approval" | optional | Force a flow. Defaults to signature for gasless-eligible tokens, else approval. A smart-contract wallet must pass approval — the signature flow rejects smart-wallet signatures (including ERC-6492) outright. |
submit_deposit
Submit a deposit
write 10 calls/min 1 call / 6s
Submit a deposit — moves funds. Requires a jwt from submit_auth_verify for this walletAddress, and either a signature (from prepare_deposit 's signature output) or, for approval-based deposits, no signature at all once your wallet has broadcast the approve() transaction on-chain. Always call prepare_deposit first — this tool does not validate amounts or resolve addresses. Returns a trackingId for get_deposit_status. The six signature fields must be provided all together or not at all. The signature must be a raw 65-byte ECDSA signature — smart-wallet / ERC-6492 signatures are rejected; use the approval method instead.
| Parameter | Type | Description | |
|---|---|---|---|
jwt | string | required | JWT from submit_auth_verify for this walletAddress. |
chain | string | required | Same value passed to prepare_deposit. |
token | string | required | Token symbol. |
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
amount | string | required | Raw token amount, same as passed to prepare_deposit. |
validAfter | string | optional | From prepare_deposit 's signature-method output. Omit for approval-based deposits. |
validBefore | string | optional | From prepare_deposit 's signature-method output. Omit for approval-based deposits. |
nonce | string | optional | From prepare_deposit 's signature-method output. Omit for approval-based deposits. |
signature | string | optional | Raw 65-byte ECDSA signature over prepare_deposit 's typedData. Omit for approval-based deposits. |
tokenName | string | optional | From prepare_deposit 's signature-method output. Omit for approval-based deposits. |
tokenVersion | string | optional | From prepare_deposit 's signature-method output. Omit for approval-based deposits. |
get_deposit_status
Get deposit status
read‑only 20 calls/min 1 call / 3s
Poll a deposit by trackingId (from submit_deposit). status is one of: PROCESSING — still confirming on-chain, keep polling; BRIDGING — accepted, funds crossing chains via CCTP (typically 20–30 min), treat as done and stop polling; SUCCESS — terminal, fully completed; FAILED — terminal, did not go through; EMERGENCY_WITHDRAW — terminal but not success: the destination pool could not accept the funds after bridging, so they were sent back to your own wallet address (not lost, not stuck).
| Parameter | Type | Description | |
|---|---|---|---|
trackingId | string | required | trackingId returned by submit_deposit. |
prepare_withdrawal
Prepare a withdrawal
read‑only 5 calls/min 1 call / 12s
Build the EIP-712 typed-data payload to sign for withdrawing from a specific pool, without signing. Looks up the exact withdrawRequest from your current balances, fills in the amount and a short-lived deadline, and returns it ready to sign. Omit amount (or pass "MAX") to withdraw the whole pool balance — this uses the maxUint256 convention, avoiding stale-balance/rounding failures. Refuses to build typedData when the pool's live liquidity does not exceed the amount, or when the amount is below the chain minimum. A fixed on-behalf gas fee is deducted — expectedReceivedAmountRaw is what actually arrives.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
chain | string | required | Required to disambiguate — the same pool contract address can exist on several chains. |
token | string | required | Token symbol — some pool addresses serve multiple tokens. |
poolContract | string | required | Pool contract address, from get_balances ' perPool[].poolAddress. |
amount | string | optional | Raw amount to withdraw. Omit or pass "MAX" for a full withdrawal (uses maxUint256). |
submit_withdrawal
Submit a withdrawal
write 20 calls/min 1 call / 3s
Submit a withdrawal — moves funds. Requires a jwt from submit_auth_verify for this walletAddress and a signature over prepare_withdrawal 's typedData. Always call prepare_withdrawal first — this tool does not look up pool data or validate amounts. The signature's EIP-712 domain is scoped to this one chain: a smart-contract wallet must have a valid signer on that exact chain (an EOA is chain-agnostic). Returns a trackingId for get_withdrawal_status.
| Parameter | Type | Description | |
|---|---|---|---|
jwt | string | required | JWT from submit_auth_verify for this walletAddress. |
chain | string | required | From prepare_withdrawal 's output. |
token | string | required | Token symbol. |
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
amount | string | required | From prepare_withdrawal 's output. |
deadline | string | required | From prepare_withdrawal 's output. |
signature | string | required | Signature over prepare_withdrawal 's typedData. |
poolContract | string | required | Pool contract address, same as passed to prepare_withdrawal. |
get_withdrawal_status
Get withdrawal status
read‑only 20 calls/min 1 call / 3s
Poll a withdrawal by trackingId (from submit_withdrawal). status is one of: PROCESSING — still in progress, keep polling (withdrawals have no cross-chain bridging leg); SUCCESS — terminal, completed; FAILED — terminal, did not go through.
| Parameter | Type | Description | |
|---|---|---|---|
trackingId | string | required | trackingId returned by submit_withdrawal. |
get_withdrawal_version
Get withdrawal contract version
read‑only 10 calls/min 1 call / 6s
Get the contract signing version string required for the EIP-712 domain data used when signing a withdrawal. Rarely changes; safe to cache client-side.
No parameters.
prepare_config
Prepare a rebalancer config update
read‑only 5 calls/min 1 call / 12s
Build the signing hash for updating a wallet's rebalancer configuration (which pools/protocols/chains it may move funds into), without signing. Sign the returned hashToSign with personal_sign over its raw 32 bytes (not the UTF-8 text of the hex string) and pass the signature to submit_config. Refuses to build a hash for a config that would drop a pool you still hold a nonzero balance in (withdraw first), an invalid spendingCapRaw, or any (domainId, protocolId, poolAddress) tuple that does not match a real pool in the current catalog. For a smart-contract wallet the same signature must independently verify on every chain named in domainIds; an EOA is unaffected. When reconfiguring a wallet that already has a signed config, carry every field forward from get_config unless you deliberately want to change it — omitting includeRewardsApy, requiredTvl, requiredLiquidityMultiplier, collateralExposure, or spendingCapRaw does not preserve the current value, it silently resets that filter.
| Parameter | Type | Description | |
|---|---|---|---|
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
token | string | required | Token symbol this config applies to. |
protocolIds | number[] | required | 0=Aave, 1=Morpho, 2=Euler. One entry per poolAddresses / domainIds entry, from list_pools ' signData.protocolId. |
poolAddresses | string[] | required | Pool contract addresses to allow, from list_pools ' signData.poolAddress. |
domainIds | number[] | required | Chain domain IDs, one per poolAddresses entry, from list_pools ' signData.domainId. |
includeRewardsApy | boolean | optional | Whether reward-token APY counts toward the best-pool comparison. Defaults true. When reconfiguring, copy the current value from get_config — omitting it silently resets it. |
requiredTvl | string | optional | Minimum pool TVL in USD for a pool to be an eligible rebalance target. Omitting it (default) does not keep the wallet's current floor — it silently resets it to 0 (no floor). When reconfiguring, pass the current value from get_config. |
requiredLiquidityMultiplier | string | optional | A candidate pool's liquidity must be at least this multiple of the deposit amount (USD) to be eligible. Omitting it (default) silently resets it to 0 (no margin), it does not keep the current value — when reconfiguring, pass the current value from get_config. |
collateralExposure | string[] | null | optional | Whitelist of collateral symbols — applies to Morpho vault selection only. null (default) disables it; an empty array is rejected. A non-null list also requires an Aave pool in the config. Omitting it silently disables the filter, so when reconfiguring, pass the current value from get_config. |
spendingCapRaw | string | null | optional | Raw units, USDC only ("0" /null for every other token). Nonzero sets the target balance MetaLend keeps topped up on Linea for a linked card; requires the Aave pool on Linea and a minimum of 1000000 (1 USDC). Must match exactly between prepare_config and submit_config. Omitting it resets the cap to 0 (off), so when reconfiguring, pass the current value from get_config. |
submit_config
Submit a rebalancer config update
write 10 calls/min 1 call / 6s
Submit a signed rebalancer configuration update. Requires a jwt from submit_auth_verify for this walletAddress and a signature over prepare_config 's hashToSign. Always call prepare_config first — this tool does not compute the hash or check the balance invariant itself. Forwards the same config fields as prepare_config: when reconfiguring, every omitted optional filter (requiredTvl, requiredLiquidityMultiplier, collateralExposure, spendingCapRaw, includeRewardsApy) is silently reset, not preserved — carry the current values forward from get_config. Can return a 409 if a rebalance or deposit is in progress for this wallet+token (or, USDC only, a funding-cap refill) — unrelated to the signature; wait and retry using the retryAfterSeconds hint. There is no tracking ID or status poller for a config update — if this call times out, re-read get_config to see whether it applied rather than assuming it didn't.
| Parameter | Type | Description | |
|---|---|---|---|
jwt | string | required | JWT from submit_auth_verify for this walletAddress. |
walletAddress | string | required | EVM wallet address (0x…, 40 hex chars). Any casing accepted. |
token | string | required | Token symbol, same as passed to prepare_config. |
protocolIds | number[] | required | Same array passed to prepare_config. |
poolAddresses | string[] | required | Same array passed to prepare_config. |
domainIds | number[] | required | Same array passed to prepare_config. |
signature | string | required | Signature over prepare_config 's hashToSign. |
includeRewardsApy | boolean | optional | Same value passed to prepare_config. |
requiredTvl | string | optional | Same value passed to prepare_config. |
requiredLiquidityMultiplier | string | optional | Same value passed to prepare_config. |
collateralExposure | string[] | null | optional | Same value passed to prepare_config. |
spendingCapRaw | string | null | optional | Same value passed to prepare_config — must match exactly. |