Metalend

Browse, deposit, withdraw & rebalance stablecoin positions on Aave, Morpho & Euler. Track yields, balances, rewards & rebalancer settings across all major EVM chains.

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.

  1. Open Customize → Connectors, click + then Add custom connector
  2. 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.

  1. Open (or create) ~/.cursor/mcp.json
  2. 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.

  1. Turn on Developer mode (menu labels vary by rollout — look under Settings → Apps & Connectors → Advanced Settings, or Settings → Plugins)
  2. Click Browse plugins (or Connectors) → + to add a new connector, and paste the URL below as the MCP server URL
  3. 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:

  1. Call get_auth_challenge with the walletAddress to get a Sign-In-With-Ethereum message.
  2. 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.
  3. Call submit_auth_verify with the signature to exchange it for a jwt.
  4. Pass that jwt explicitly on every submit_deposit / submit_withdrawal / submit_config call 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.

  1. prepare_* computes everything (EIP-712 typed data, a config signing hash, or on-chain approve() 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.
  2. You sign the returned payload with the wallet's own signer.
  3. submit_* takes the signature you produced plus your jwt and relays it upstream. This is the step that moves funds or changes config. submit_deposit and submit_withdrawal return a trackingId to poll with get_deposit_status / get_withdrawal_status; submit_config has no tracking ID and no status poller — confirm an uncertain config update by re-reading get_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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
tokensstringoptionalComma-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.

ParameterTypeDescription
walletAddressstringrequiredEVM 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.

ParameterTypeDescription
walletAddressstringrequiredEVM 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.

ParameterTypeDescription
tokenstringrequiredToken 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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
reloadChainstringoptionalChain 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.

ParameterTypeDescription
tokenstringrequiredToken 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.

ParameterTypeDescription
tokenstringrequiredToken symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD.
chainstringrequiredChain 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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
chainstringoptionalCosmetic 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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
signaturestringrequiredpersonal_sign signature of the SIWE message from get_auth_challenge.
chainstringoptionalFor 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.

ParameterTypeDescription
chainstringrequiredOriginating 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.
tokenstringrequiredToken symbol, e.g. USDC, USDT, MUSD, RLUSD, USDG, USDE, PYUSD.
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
amountstringrequiredRaw token amount as a decimal-digit string (smallest denomination). Use get_token_info for decimals.
method"signature" | "approval"optionalForce 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.

ParameterTypeDescription
jwtstringrequiredJWT from submit_auth_verify for this walletAddress.
chainstringrequiredSame value passed to prepare_deposit.
tokenstringrequiredToken symbol.
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
amountstringrequiredRaw token amount, same as passed to prepare_deposit.
validAfterstringoptionalFrom prepare_deposit 's signature-method output. Omit for approval-based deposits.
validBeforestringoptionalFrom prepare_deposit 's signature-method output. Omit for approval-based deposits.
noncestringoptionalFrom prepare_deposit 's signature-method output. Omit for approval-based deposits.
signaturestringoptionalRaw 65-byte ECDSA signature over prepare_deposit 's typedData. Omit for approval-based deposits.
tokenNamestringoptionalFrom prepare_deposit 's signature-method output. Omit for approval-based deposits.
tokenVersionstringoptionalFrom 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).

ParameterTypeDescription
trackingIdstringrequiredtrackingId 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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
chainstringrequiredRequired to disambiguate — the same pool contract address can exist on several chains.
tokenstringrequiredToken symbol — some pool addresses serve multiple tokens.
poolContractstringrequiredPool contract address, from get_balances ' perPool[].poolAddress.
amountstringoptionalRaw 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.

ParameterTypeDescription
jwtstringrequiredJWT from submit_auth_verify for this walletAddress.
chainstringrequiredFrom prepare_withdrawal 's output.
tokenstringrequiredToken symbol.
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
amountstringrequiredFrom prepare_withdrawal 's output.
deadlinestringrequiredFrom prepare_withdrawal 's output.
signaturestringrequiredSignature over prepare_withdrawal 's typedData.
poolContractstringrequiredPool 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.

ParameterTypeDescription
trackingIdstringrequiredtrackingId 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.

ParameterTypeDescription
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
tokenstringrequiredToken symbol this config applies to.
protocolIdsnumber[]required0=Aave, 1=Morpho, 2=Euler. One entry per poolAddresses / domainIds entry, from list_pools ' signData.protocolId.
poolAddressesstring[]requiredPool contract addresses to allow, from list_pools ' signData.poolAddress.
domainIdsnumber[]requiredChain domain IDs, one per poolAddresses entry, from list_pools ' signData.domainId.
includeRewardsApybooleanoptionalWhether 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.
requiredTvlstringoptionalMinimum 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.
requiredLiquidityMultiplierstringoptionalA 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.
collateralExposurestring[] | nulloptionalWhitelist 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.
spendingCapRawstring | nulloptionalRaw 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.

ParameterTypeDescription
jwtstringrequiredJWT from submit_auth_verify for this walletAddress.
walletAddressstringrequiredEVM wallet address (0x…, 40 hex chars). Any casing accepted.
tokenstringrequiredToken symbol, same as passed to prepare_config.
protocolIdsnumber[]requiredSame array passed to prepare_config.
poolAddressesstring[]requiredSame array passed to prepare_config.
domainIdsnumber[]requiredSame array passed to prepare_config.
signaturestringrequiredSignature over prepare_config 's hashToSign.
includeRewardsApybooleanoptionalSame value passed to prepare_config.
requiredTvlstringoptionalSame value passed to prepare_config.
requiredLiquidityMultiplierstringoptionalSame value passed to prepare_config.
collateralExposurestring[] | nulloptionalSame value passed to prepare_config.
spendingCapRawstring | nulloptionalSame value passed to prepare_config — must match exactly.