MetaLend Rewards API

Reward aggregation and claim data

OpenAPI Specification

metalend-rewards-api-openapi.yml Raw ↑
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