MetaLend Auth API

Get challenge / verify challenge authentication

OpenAPI Specification

metalend-auth-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: MetaLend Rebalancing AI Agent Guide Auth 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: Auth
  description: Get challenge / verify challenge authentication
paths:
  /v1/auth/challenge:
    post:
      summary: Create wallet challenge
      description: 'Issues a challenge message that must be signed by the wallet owner.


        **Rate limiting**: 1 request per 6 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).

        '
      operationId: createAuthChallenge
      tags:
      - Auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetChallengeRequest'
            example:
              walletAddress: '0x14848abF0125De79e0A5F3caA7F4EA9664a988e8'
              siweDomain: app.metalend.tech
              siweUri: https://app.metalend.tech
              siweStatement: Sign in with Ethereum to MetaLend
              chain: ETHEREUM
      responses:
        '200':
          description: Challenge issued successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetChallengeResponse'
        '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'
                invalidSiweDetails:
                  $ref: '#/components/examples/InvalidSiweDetails'
                chainNotSupported:
                  $ref: '#/components/examples/ChainNotSupported'
                chainBlank:
                  $ref: '#/components/examples/ChainBlank'
        '401':
          $ref: '#/components/responses/UnauthorizedNoHeader'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
      x-agentcash-auth:
        mode: free
  /v1/auth/verify:
    post:
      summary: Verify wallet challenge
      description: 'Verifies the signed challenge and issues a JWT for authenticated requests.


        **Rate limiting**: 1 request per 12 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).

        '
      operationId: verifyAuthChallenge
      tags:
      - Auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyChallengeRequest'
            example:
              walletAddress: '0x14848abF0125De79e0A5F3caA7F4EA9664a988e8'
              signature: '0x9c1efc293e476948580195863a66203816a3d437a04a92df95c1be979010031c35dbd856110eb161aa59f72cbfe49ff3a64fda3bdb094c2c321edfb13c8215161b'
              chain: ETHEREUM
      responses:
        '200':
          description: Challenge verified successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyChallengeResponse'
        '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'
                chainNotSupported:
                  $ref: '#/components/examples/ChainNotSupported'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              examples:
                noAuthHeader:
                  $ref: '#/components/examples/UnauthorizedNoHeader'
                invalidSignature:
                  $ref: '#/components/examples/UnauthorizedInvalidSignature'
                notAnEvmAddress:
                  $ref: '#/components/examples/NotAnEvmAddress'
                authExpiredOrNotCreated:
                  $ref: '#/components/examples/AuthExpiredOrNotCreated'
                chainBlank:
                  $ref: '#/components/examples/ChainBlank'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
      x-agentcash-auth:
        mode: free
components:
  schemas:
    VerifyChallengeResponse:
      type: object
      required:
      - jwt
      properties:
        jwt:
          type: string
          description: Issued JWT for authenticated requests
    GetChallengeRequest:
      type: object
      required:
      - walletAddress
      - siweDomain
      - siweUri
      - siweStatement
      properties:
        walletAddress:
          type: string
          pattern: ^0x[a-fA-F0-9]{40}$
          description: User's Ethereum wallet address (must be checksummed per EIP-55)
          example: '0x14848abF0125De79e0A5F3caA7F4EA9664a988e8'
        siweDomain:
          type: string
          description: The domain requesting the SIWE signature (e.g. "app.metalend.tech")
          example: app.metalend.tech
        siweUri:
          type: string
          description: The URI for the SIWE message (e.g. "https://app.metalend.tech")
          example: https://app.metalend.tech
        siweStatement:
          type: string
          description: The human-readable statement shown to the user in the SIWE signing prompt
          example: Sign in with Ethereum to MetaLend
        chain:
          type: string
          description: The blockchain network. Defaults to "ETHEREUM" if omitted.
          nullable: true
          default: ETHEREUM
          enum:
          - ETHEREUM
          - BASE
          - POLYGON
          - ARBITRUM
          - OPTIMISM
          - AVALANCHE
          - LINEA
          example: ETHEREUM
    GetChallengeResponse:
      type: object
      required:
      - message
      properties:
        message:
          type: string
          description: Challenge message to sign using personal_sign method
    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)
    VerifyChallengeRequest:
      type: object
      required:
      - walletAddress
      - signature
      properties:
        walletAddress:
          type: string
          pattern: ^0x[a-fA-F0-9]{40}$
          description: User's Ethereum wallet address (must be checksummed per EIP-55)
          example: '0x14848abF0125De79e0A5F3caA7F4EA9664a988e8'
        signature:
          type: string
          description: Signed challenge message using personal_sign method
          example: '0x9c1efc293e476948580195863a66203816a3d437a04a92df95c1be979010031c35dbd856110eb161aa59f72cbfe49ff3a64fda3bdb094c2c321edfb13c8215161b'
        chain:
          type: string
          enum:
          - ETHEREUM
          - BASE
          - POLYGON
          - ARBITRUM
          - OPTIMISM
          - AVALANCHE
          - LINEA
          nullable: true
          default: ETHEREUM
          description: The blockchain network for signature verification. Defaults to "ETHEREUM" if omitted. Required for Smart Contract Wallets to specify the chain on which verification is performed.
          example: ETHEREUM
  responses:
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          examples:
            Internal Error:
              value:
                code: INTERNAL_ERROR
                internalCode: INTERNAL_ERROR
                message: An unexpected error occurred while processing your request
                requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
                retryAfterSeconds: null
    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:
    UnauthorizedInvalidSignature:
      summary: Invalid signature
      value:
        code: AUTHENTICATION_FAILED
        internalCode: INVALID_SIGNATURE
        message: Invalid signature for address '0x14848abf0125de79e0a5f3caa7f4ea9664a988e8'
        requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
        retryAfterSeconds: null
    ChainNotSupported:
      summary: Chain not supported
      value:
        code: ILLEGAL_ARGUMENT_ERROR
        internalCode: CHAIN_NOT_SUPPORTED
        message: 'Chain ''STX'' is not supported. Supported chains: eth, base, polygon, arbitrum, optimism, avax, linea'
        requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
        retryAfterSeconds: null
    InvalidSiweDetails:
      summary: Invalid SIWE details
      value:
        code: ILLEGAL_ARGUMENT_ERROR
        internalCode: INVALID_SIWE_DETAILS
        message: 'Invalid SIWE details: SIWE domain cannot be blank'
        requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
        retryAfterSeconds: null
    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
    NotAnEvmAddress:
      summary: Not an EVM address
      value:
        code: AUTHENTICATION_FAILED
        internalCode: INVALID_EVM_ADDRESS
        message: Invalid wallet address
        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
    AuthExpiredOrNotCreated:
      summary: Authentication expired or not created
      value:
        code: AUTHENTICATION_FAILED
        internalCode: AUTH_MESSAGE_EXPIRED_OR_NOT_CREATED
        message: Authentication message has expired or was not created
        requestId: 2c2f3dc1-d19a-49cb-b695-37d025e551b3
        retryAfterSeconds: null
    ChainBlank:
      summary: Chain blank
      value:
        code: ILLEGAL_ARGUMENT_ERROR
        internalCode: CHAIN_BLANK
        message: Chain cannot be blank
        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
  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