openapi: 3.0.3
info:
  title: Yieldbase Liquidation Alerts API
  version: 0.1.0
  description: |
    Real-time DeFi liquidation alerts. Subscribe a borrower address, choose
    health-factor thresholds, and receive signed webhooks + Telegram +
    email when crossings occur.

    v0.1 supports Aave V3 on Ethereum, Base, Polygon, Arbitrum, Optimism.
    Compound V3, Morpho Blue, and Spark land in v0.2.
  contact:
    name: Yieldbase
    url: https://yieldbase.example
servers:
  - url: http://localhost:3210
    description: Local dev
  - url: https://liq-alerts.yieldbase.example
    description: Production (placeholder)
tags:
  - name: positions
    description: Manage monitored positions
  - name: monitor
    description: Read live health-factor data
  - name: ops
    description: Service health + delivery records
security:
  - apiKeyAuth: []
paths:
  /health:
    get:
      tags: [ops]
      summary: Service health + last-tick summary
      security: []
      responses:
        '200':
          description: Service status payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /stats:
    get:
      tags: [ops]
      summary: Aggregate counts by protocol
      security: []
      responses:
        '200':
          description: Stats payload
  /check-hf:
    get:
      tags: [monitor]
      summary: One-shot health-factor read (no registration required)
      security: []
      parameters:
        - in: query
          name: user
          required: true
          schema: { type: string, pattern: '^0x[0-9a-fA-F]{40}$' }
        - in: query
          name: chainId
          schema: { type: integer, default: 1 }
      responses:
        '200':
          description: Health-factor snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthFactorSnapshot'
        '400': { description: bad address }
        '502': { description: upstream RPC failure }
  /positions:
    get:
      tags: [positions]
      summary: List positions owned by the API key
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  positions:
                    type: array
                    items: { $ref: '#/components/schemas/Position' }
    post:
      tags: [positions]
      summary: Register a position for monitoring
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PositionInput'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Position' }
        '400': { description: validation error }
        '401': { description: missing or invalid api key }
  /positions/{id}:
    delete:
      tags: [positions]
      summary: Delete a position
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '204': { description: Deleted }
        '404': { description: Not found }
  /alerts/recent:
    get:
      tags: [ops]
      summary: Recent delivery attempts (max 100)
      responses:
        '200':
          description: Delivery log
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
  schemas:
    Health:
      type: object
      properties:
        status: { type: string, example: ok }
        service: { type: string, example: yieldbase-liq-alerts }
        version: { type: string }
        lastTickAt: { type: string, format: date-time }
        totalAlerts: { type: integer }
    HealthFactorSnapshot:
      type: object
      properties:
        user: { type: string }
        chainId: { type: integer }
        protocol: { type: string }
        healthFactor: { type: number, example: 1.42 }
        healthFactorRaw: { type: string, description: raw 1e18-scaled uint as string }
        totalCollateralBase: { type: string }
        totalDebtBase: { type: string }
    Position:
      type: object
      properties:
        id: { type: string }
        protocol: { type: string, enum: [aave-v3] }
        chainId: { type: integer }
        user: { type: string }
        thresholds:
          type: array
          items: { type: number }
        channels:
          type: array
          items: { $ref: '#/components/schemas/DeliveryChannel' }
        label: { type: string }
        createdAt: { type: string, format: date-time }
    PositionInput:
      type: object
      required: [protocol, user, channels]
      properties:
        protocol: { type: string, enum: [aave-v3] }
        chainId: { type: integer, default: 1 }
        user: { type: string, pattern: '^0x[0-9a-fA-F]{40}$' }
        thresholds:
          type: array
          items: { type: number }
          default: [1.15, 1.05]
        channels:
          type: array
          items: { $ref: '#/components/schemas/DeliveryChannel' }
        label: { type: string }
    DeliveryChannel:
      oneOf:
        - type: object
          required: [kind, url]
          properties:
            kind: { type: string, enum: [webhook] }
            url: { type: string, format: uri }
        - type: object
          required: [kind, chatId]
          properties:
            kind: { type: string, enum: [telegram] }
            chatId: { type: string }
        - type: object
          required: [kind, to]
          properties:
            kind: { type: string, enum: [email] }
            to: { type: string, format: email }
    AlertEvent:
      type: object
      properties:
        id: { type: string }
        positionId: { type: string }
        user: { type: string }
        protocol: { type: string }
        chainId: { type: integer }
        threshold: { type: number }
        direction: { type: string, enum: [crossed-below, recovered-above] }
        healthFactor: { type: number }
        previousHealthFactor: { type: number }
        observedAt: { type: string, format: date-time }
        blockNumber: { type: integer }
        label: { type: string }
