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