openapi: 3.0.3
info:
title: MetaLend Rebalancing AI Agent Guide Rewards API
version: 0.1.0
description: "API for managing cross-protocol cross-chain DeFi rebalancing operations exposing: <ul>\n <li><b>Auth</b> – challenge/verify flow for JWT issuance.</li>\n <li><b>Project settings</b> – fetch current project settings.</li>\n <li><b>User configuration</b> – register or update token-level user-rebalancer settings and fetch current wallet configs.</li>\n <li><b>Deposits</b> – create deposit intents, receive tracking IDs, and poll status through PROCESSING -> BRIDGING (cross-chain only) -> SUCCESS/FAILED/EMERGENCY_WITHDRAW. New deposits may return <b>503</b> with <b>DEPOSITS_PAUSED</b> during maintenance.</li>\n <li><b>Withdrawals</b> – submit signed withdrawal requests, receive tracking IDs, and monitor status PROCESSING -> SUCCESS/FAILED.</li>\n <li><b>Balances</b> – query wallet-level balances and withdraw details, as well as balances in bridge progress.</li>\n <li><b>Rewards</b> – aggregated rewards and claim details per wallet.</li>\n <li><b>Pools</b> – list supported protocols (Aave, Morpho, Euler, etc.), their pools, APY breakdowns, and TVL snapshots.</li>\n <li><b>Transaction Costs</b> – current deposit/withdraw on-behalf transaction fees and minimum amounts by chain.</li>\n <li><b>AI Agent Guide</b> – Markdown flow guide at <code>/SKILL.md</code> with signing formats, wizard requirements, and known integration gotchas.</li>\n</ul> All endpoints require an <b>X-API-Key</b>. <b>JWT authentication</b> is required on <b>deposit, withdrawal, and config update</b> operations.\n<b>Obtain API key by <a href=\"https://developer.metalend.tech/\" target=\"_blank\" rel=\"noopener noreferrer\">creating a new project</a>.</b>\nAll requests must include the Origin header. Use Origin: https://app.metalend.tech unless you have been assigned a different allowed origin."
contact:
name: MetaLend API Support
email: support@metalend.tech
termsOfService: https://metalend-inc.gitbook.io/litepaper/terms-of-service
x-guidance: AI agents MUST read https://api.metalend.tech/SKILL.md before implementing any MetaLend operation. It contains critical signing formats, required headers, wizard flows for config setup, and known failure modes.
x-default-api-key: a7f2b8c4d9e1f3a6b5c8d2e7f1a4b9c3d6e8f2a5b7c1d4e9f3a6b8c2d5e7f1a4
x-api-key-guidance: Use x-default-api-key as X-API-Key for agent testing/integration. If you are the project owner, create or use your own project API key from https://developer.metalend.tech instead.
servers:
- url: https://api.metalend.tech
description: Production server
security:
- ApiKeyAuth: []
OriginHeader: []
tags:
- name: Rewards
description: Reward aggregation and claim data
paths:
/v1/rewards/{walletAddress}:
get:
summary: Get rewards
description: "Retrieves aggregated rewards data for the specified wallet address across \nsupported reward sources (currently Merkle-based rewards).\n\nAfter a successful claim on a specific chain, pass `reloadChain` with the human-readable chain name (e.g. `base`, `arbitrum`) to force a Merkl cache refresh for that chain.\n\n**How to claim rewards — the `claimTransaction` is pre-built, just send it.**\n\nEach `RewardItem` with claimable rewards includes a non-null `claimTransaction` \nfield. The backend has already constructed the full ABI-encoded calldata for the \n`claimRewards(address,address[],uint256[],bytes32[][])` function — including the \ndistributor address, token addresses, amounts, and merkle proofs.\n\nTo execute the claim, send a transaction from the user's wallet with exactly \nthese values:\n- `to`: `claimTransaction.to` — the rewards distributor contract address\n- `data`: `claimTransaction.calldata` — fully encoded, ready to submit as-is\n- `chainId`: `claimTransaction.chainId` — the chain to send it on\n\n**You do NOT need to encode anything yourself.** Do not use `params`, `abi`, or \n`method` to construct the calldata — those are informational only. Use `calldata` \ndirectly as the transaction `data` field.\n\nExample (viem):\nawait walletClient.sendTransaction({\n to: claimTransaction.to,\n data: claimTransaction.calldata,\n chainId: claimTransaction.chainId,\n})\n\n**Rate limiting**: 1 request per 3 seconds per client IP. Excess requests receive HTTP `429` with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers (see `RateLimitExceeded` response).\n"
operationId: getRewards
tags:
- Rewards
parameters:
- name: walletAddress
in: path
required: true
description: User's Ethereum wallet address
schema:
type: string
pattern: ^0x[a-fA-F0-9]{40}$
example: '0x1234567890123456789012345678901234567890'
- name: reloadChain
in: query
required: false
description: 'After claiming rewards on a specific chain, pass the chain name to force Merkl cache refresh for that chain. Omit for normal cached fetch.
'
schema:
type: string
enum:
- ethereum
- base
- polygon
- arbitrum
- avalanche
- optimism
- linea
example: base
responses:
'200':
description: Rewards retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/GetRewardsResponse'
'400':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
examples:
walletAddressBlank:
$ref: '#/components/examples/WalletAddressBlank'
invalidAddressFormat:
$ref: '#/components/examples/InvalidAddressFormat'
invalidAddressCheckSum:
$ref: '#/components/examples/InvalidAddressCheckSum'
'401':
$ref: '#/components/responses/UnauthorizedNoHeader'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
description: Internal error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
examples:
userDoesNotExist:
$ref: '#/components/examples/UserDoesNotExist'
rebalancerDoesNotExistForWallet:
$ref: '#/components/examples/RebalancerDoesNotExistForWallet'
x-agentcash-auth:
mode: free
components:
schemas:
RewardSource:
type: object
required:
- source
- rewardItems
properties:
source:
type: string
description: Reward source name
rewardItems:
type: array
items:
$ref: '#/components/schemas/RewardItem'
GetRewardsResponse:
type: object
required:
- walletAddress
- timestamp
- totalClaimedUsd
- totalAvailableUsd
- rewards
properties:
walletAddress:
type: string
description: Wallet address
timestamp:
type: string
description: ISO timestamp
totalClaimedUsd:
type: string
description: Total claimed rewards in USD
totalAvailableUsd:
type: string
description: Total available rewards in USD
rewards:
type: array
items:
$ref: '#/components/schemas/RewardSource'
Token:
type: object
required:
- symbol
- name
- address
- decimals
properties:
symbol:
type: string
description: Token symbol
enum:
- USDC
- MUSD
- USDT
- RLUSD
example: USDC
name:
type: string
description: Token full name
example: USD Coin
address:
type: string
pattern: ^0x[a-fA-F0-9]{40}$
example: '0x1234567890123456789012345678901234567890'
description: Token contract address
decimals:
type: integer
description: Token decimals
example: 6
version:
type: string
nullable: true
description: Token contract version
VestingInfo:
type: object
required:
- totalAllocatedRaw
- totalAllocatedFormatted
- unlockedRaw
- unlockedFormatted
- lockedRaw
- lockedFormatted
- vestingStartTime
- vestingEndTime
- vestingDuration
- vestingGranularity
- initialUnlockBps
- bpsDenominator
- progressPercentage
- daysElapsed
- daysTotal
- stepsCompleted
- stepsTotal
properties:
totalAllocatedRaw:
type: string
totalAllocatedFormatted:
type: string
unlockedRaw:
type: string
unlockedFormatted:
type: string
lockedRaw:
type: string
lockedFormatted:
type: string
vestingStartTime:
type: integer
vestingEndTime:
type: integer
vestingDuration:
type: integer
vestingGranularity:
type: integer
initialUnlockBps:
type: integer
bpsDenominator:
type: integer
progressPercentage:
type: string
daysElapsed:
type: integer
daysTotal:
type: integer
stepsCompleted:
type: integer
stepsTotal:
type: integer
RewardChainInfo:
type: object
required:
- id
- name
- icon
- endOfDisputePeriod
- explorers
properties:
id:
type: integer
name:
type: string
icon:
type: string
endOfDisputePeriod:
type: integer
explorers:
type: array
items:
$ref: '#/components/schemas/ChainExplorer'
RewardItem:
type: object
required:
- token
- chain
- claimedRaw
- claimedFormatted
- amount
- claimedUsd
- pendingRaw
- pendingFormatted
- availableRaw
- availableFormatted
- availableUsd
- distributorAddress
properties:
token:
$ref: '#/components/schemas/Token'
chain:
$ref: '#/components/schemas/RewardChainInfo'
claimedRaw:
type: string
claimedFormatted:
type: string
amount:
type: string
claimedUsd:
type: string
pendingRaw:
type: string
pendingFormatted:
type: string
availableRaw:
type: string
description: 'Available (unclaimed) reward amount in raw token units. When this is "0", claimTransaction will be null and no claim button should be shown. When > "0", claimTransaction is non-null and ready to execute.
'
availableFormatted:
type: string
availableUsd:
type: string
distributorAddress:
type: string
proof:
type: array
nullable: true
items:
type: string
vestingInfo:
$ref: '#/components/schemas/VestingInfo'
nullable: true
claimTransaction:
$ref: '#/components/schemas/ClaimTransaction'
nullable: true
description: 'Pre-built transaction to claim this reward item on-chain. Non-null when `availableRaw` > "0". To claim: send a transaction with `to = claimTransaction.to`, `data = claimTransaction.calldata`, `chainId = claimTransaction.chainId`. No additional encoding needed.
'
ClaimTransactionParams:
type: object
required:
- distributor
- tokens
- amounts
- proofs
properties:
distributor:
type: string
tokens:
type: array
items:
type: string
amounts:
type: array
items:
type: string
proofs:
type: array
items:
type: array
items:
type: string
ClaimTransaction:
type: object
required:
- to
- chainId
- chain
- method
- params
- abi
- calldata
properties:
to:
type: string
description: 'The rewards distributor contract address. Use as the `to` field of the transaction.
'
chainId:
type: integer
description: 'The EVM chain ID to send the transaction on. Ensure the wallet is on this chain before calling sendTransaction.
'
chain:
type: string
description: Human-readable chain name (informational, e.g. "ethereum", "linea").
method:
type: string
description: 'The contract function signature — informational only. The value is "claimRewards(address,address[],uint256[],bytes32[][])". Do NOT use this to build the transaction — use `calldata` directly.
'
params:
$ref: '#/components/schemas/ClaimTransactionParams'
description: 'The decoded parameters passed to claimRewards — informational only. Do NOT use these to re-encode the calldata — use `calldata` directly.
'
abi:
type: string
description: 'The ABI fragment for the claimRewards function — informational only. Do NOT use this to re-encode the calldata — use `calldata` directly.
'
calldata:
type: string
description: 'Fully ABI-encoded transaction data, ready to submit as-is. Use this as the `data` field of sendTransaction. This includes the function selector and all encoded arguments (distributor, token addresses, amounts, merkle proofs). Example: walletClient.sendTransaction({ to, data: calldata, chainId })
'
ApiError:
type: object
required:
- code
- internalCode
- message
properties:
code:
type: string
description: Error category identifier
internalCode:
type: string
description: More specific error identifier
message:
type: string
description: Human-readable error message
requestId:
type: string
format: uuid
description: Unique request identifier for tracking
retryAfterSeconds:
type: integer
description: Seconds to wait before retrying (for rate limiting)
ChainExplorer:
type: object
required:
- chainId
- id
- type
- url
properties:
chainId:
type: integer
id:
type: string
type:
type: string
url:
type: string
responses:
UnauthorizedNoHeader:
description: No authorization header provided
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
examples:
unauthorizedNoHeader:
$ref: '#/components/examples/UnauthorizedNoHeader'
RateLimitExceeded:
description: 'Too many requests for this endpoint. Limits are enforced per client IP for the public API routes backed by `RebalancerResource` (and related resources using the same filter).
Response headers (when throttled):
- `Retry-After`: seconds to wait before retrying (matches the rate-limit window duration for that endpoint).
- `X-RateLimit-Limit`: maximum requests allowed in the window (e.g. `1`).
- `X-RateLimit-Remaining`: remaining requests in the window (`0` when throttled).
- `X-RateLimit-Reset`: Unix timestamp (seconds) when the limit window resets.
`internalCode` in the JSON body is `THROTTLE_PER_IP` for IP-scoped limits (other values may apply for different scopes in the backend).
'
headers:
Retry-After:
description: Seconds to wait before retrying this endpoint.
schema:
type: integer
example: 3
X-RateLimit-Limit:
description: Maximum number of requests allowed in the current window for this endpoint.
schema:
type: integer
example: 1
X-RateLimit-Remaining:
description: Remaining requests in the current window.
schema:
type: integer
example: 0
X-RateLimit-Reset:
description: Unix timestamp (seconds) when the current limit window resets.
schema:
type: integer
format: int64
example: 1713264123
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
examples:
rateLimitExceededPerIp:
summary: Rate limit exceeded (per client IP)
value:
code: RATE_LIMIT_EXCEEDED
internalCode: THROTTLE_PER_IP
message: Rate limit exceeded. Maximum 1 requests per 3 seconds allowed. Please try again later.
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: 3
examples:
InvalidAddressCheckSum:
summary: Invalid address checksum
value:
code: ILLEGAL_ARGUMENT_ERROR
internalCode: INVALID_ADDRESS_CHECKSUM
message: Invalid Ethereum address checksum. Please ensure the address is properly checksummed (EIP-55)
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
RebalancerDoesNotExistForWallet:
summary: Rebalancer does not exist for wallet
value:
code: INTERNAL_ERROR
internalCode: REBALANCER_DOES_NOT_EXIST_FOR_WALLET
message: Rebalancer does not exist for wallet address '0x1234567890123456789012345678901234567890'
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
InvalidAddressFormat:
summary: Invalid address format
value:
code: ILLEGAL_ARGUMENT_ERROR
internalCode: INVALID_ADDRESS_FORMAT
message: 'Invalid address format. Expected: 0x followed by 40 hexadecimal characters.'
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
UnauthorizedNoHeader:
summary: No authorization header
value:
code: AUTHENTICATION_FAILED
internalCode: NO_AUTH_INFO
message: No authentication information provided
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
WalletAddressBlank:
summary: Wallet address blank
value:
code: ILLEGAL_ARGUMENT_ERROR
internalCode: BLANK_ADDRESS
message: Wallet address cannot be blank
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
UserDoesNotExist:
summary: User does not exist
value:
code: INTERNAL_ERROR
internalCode: USER_DOES_NOT_EXIST
message: User does not exist for wallet address '0x1234567890123456789012345678901234567890'
requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
retryAfterSeconds: null
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: API key required for all endpoints.
JwtAuth:
type: apiKey
in: header
name: Authorization
description: 'JWT issued by POST /v1/auth/verify. Pass as Authorization: Bearer <token>.
x-agent-hint: agentcash and other agentic wallets strip the Authorization header for their own SIWX flow. Use X-MetaLend-JWT: Bearer <token> instead — the backend accepts both headers. Affected endpoints: POST /v1/deposits, POST /v1/withdrawals, PUT /v1/config.
'
OriginHeader:
type: apiKey
in: header
name: Origin
description: 'Required for CORS requests from browsers. Must match the allowed origins configured for the API key. Example: Origin: https://app.metalend.tech. Localhost is always allowed but must be specified.
'
externalDocs:
description: Litepaper
url: https://metalend-inc.gitbook.io/litepaper