openapi: 3.1.0
info:
  title: DCP Platform API
  version: 5.0.0
  description: |
    DCP is a Saudi-hosted GPU compute marketplace for AI workloads.
    Providers register NVIDIA GPUs, install the Python daemon, and earn SAR.
    Renters submit compute jobs (LLM inference, image generation, training) that
    run on provider hardware. DCP takes 25% fee; providers earn 75%.

    ## Currency
    All monetary amounts are in **halala** (1 SAR = 100 halala) unless the field
    name ends in `_sar`.

    ## Auth
    Three auth schemes are used depending on the caller role:

    | Role | Scheme | How |
    |------|--------|-----|
    | Provider | `ProviderApiKey` | `?key=` query param or `x-provider-key` header |
    | Renter | `RenterApiKey` | `?key=` query param or `x-renter-key` header |
    | Admin | `AdminToken` | `x-admin-token` header |

    ## Rate Limits
    | Endpoint | Limit |
    |----------|-------|
    | `POST /providers/register` | 5 / IP / hour |
    | `POST /providers/heartbeat` | 4 / IP / minute |
    | `POST /renters/register` | 5 / IP / hour |
    | `POST /jobs/submit` | 10 / renter API key / minute |
    | `POST /payments/topup` | 10 / IP / minute |
    | `GET /api/providers/marketplace` | 30 / IP / minute |
    | `GET /api/admin/*` | 100 / IP / minute |
    | All other `/api/*` | 300 / IP / minute |

  contact:
    name: DCP DevRel
    url: https://dcp.sa/docs
  x-env-vars:
    DC1_ADMIN_TOKEN:
      description: Secret token for admin endpoint auth. Set in PM2 env.
      required: true
    DC1_HMAC_SECRET:
      description: HMAC-SHA256 secret for signing task_spec payloads. Must persist across restarts.
      required: true
    MOYASAR_SECRET_KEY:
      description: Moyasar payment gateway API key (Basic auth). Enables real SAR payments.
      required: false
    MOYASAR_WEBHOOK_SECRET:
      description: HMAC secret for verifying Moyasar webhook signatures. Defaults to MOYASAR_SECRET_KEY.
      required: false
    RESEND_API_KEY:
      description: Resend.com API key for transactional email delivery.
      required: false
    FRONTEND_URL:
      description: Frontend base URL injected into payment callback links. Defaults to https://dcp.sa.
      required: false
    BACKEND_URL:
      description: Backend URL injected into installer scripts. Defaults to https://api.dcp.sa.
      required: false
    CORS_ORIGINS:
      description: Comma-separated extra origins to allow in addition to the hardcoded allowlist.
      required: false

servers:
  - url: https://api.dcp.sa
    description: Production API
  - url: http://localhost:8083
    description: Local development

tags:
  - name: Providers
    description: GPU provider registration, daemon management, and earnings
  - name: Renters
    description: Renter registration, balance, and account management
  - name: Jobs
    description: Job submission, polling, result reporting, and status tracking
  - name: Payments
    description: Moyasar SAR payment initiation, webhook handling, and history
  - name: Admin
    description: Platform administration — providers, renters, jobs, finance, security
  - name: Templates
    description: Docker compute templates — available job types and approved image whitelist
  - name: Containers
    description: Container registry — approved images, custom image requests, security scans
  - name: vLLM
    description: Managed vLLM inference — synchronous and streaming completions
  - name: System
    description: Health checks and API info

components:
  securitySchemes:
    ProviderApiKey:
      type: apiKey
      in: query
      name: key
      description: Provider API key. Prefix `dc1-provider-`. Also accepted as `x-provider-key` header.
    RenterApiKey:
      type: apiKey
      in: query
      name: key
      description: Renter API key. Prefix `dc1-renter-`. Also accepted as `x-renter-key` header.
    AdminToken:
      type: apiKey
      in: header
      name: x-admin-token
      description: Admin token set via DC1_ADMIN_TOKEN env var. Also accepted as Bearer token.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: Missing required fields

    Template:
      type: object
      description: A Docker compute template defining how a job type is executed
      properties:
        id:
          type: string
          example: llm-inference-v1
        name:
          type: string
          example: LLM Inference (vLLM)
        description:
          type: string
          example: Run LLM inference jobs using vLLM with OpenAI-compatible API
        image:
          type: string
          example: dc1/llm-worker:latest
        tags:
          type: array
          items:
            type: string
          example: [llm, inference]
        sort_order:
          type: integer
          example: 1
        params_schema:
          type: object
          description: JSON Schema for the job params field
        resource_requirements:
          type: object
          properties:
            min_vram_gb:
              type: integer
              example: 16
            gpu_required:
              type: boolean
              example: true

    Provider:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        gpu_model:
          type: string
          example: RTX 3090
        status:
          type: string
          enum: [registered, connected, online, offline, paused, suspended]
        last_heartbeat:
          type: string
          format: date-time
          nullable: true
        daemon_version:
          type: string
          nullable: true
        run_mode:
          type: string
          enum: [always-on, manual, scheduled]
        uptime_percent:
          type: number
        total_earnings_halala:
          type: integer
        total_jobs:
          type: integer
        gpu_vram_mib:
          type: integer
          nullable: true
        gpu_count_reported:
          type: integer
        gpu_compute_capability:
          type: string
          nullable: true
        gpu_cuda_version:
          type: string
          nullable: true
        is_paused:
          type: boolean
        gpu_usage_cap_pct:
          type: integer
          description: Max GPU utilization cap (0–100)
        vram_reserve_gb:
          type: number
          description: VRAM reserved for OS/other apps (0–16 GB)
        temp_limit_c:
          type: integer
          description: Max GPU temperature limit (50–100°C)

    Renter:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        organization:
          type: string
          nullable: true
        balance_halala:
          type: integer
        total_spent_halala:
          type: integer
        total_jobs:
          type: integer
        created_at:
          type: string
          format: date-time

    Job:
      type: object
      properties:
        id:
          type: integer
        job_id:
          type: string
          description: UUID job identifier
        job_type:
          type: string
          enum: [llm-inference, llm_inference, training, rendering, image_generation, vllm_serve, custom]
        status:
          type: string
          enum: [queued, pending, running, completed, failed, cancelled]
        submitted_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        cost_halala:
          type: integer
          description: Estimated/reserved cost at submit time
        actual_cost_halala:
          type: integer
          nullable: true
          description: Actual cost settled at completion
        provider_earned_halala:
          type: integer
          nullable: true
        dc1_fee_halala:
          type: integer
          nullable: true

    Payment:
      type: object
      properties:
        payment_id:
          type: string
        amount_sar:
          type: number
        amount_halala:
          type: integer
        status:
          type: string
          enum: [initiated, paid, failed, refunded]
        source_type:
          type: string
          enum: [creditcard, mada, applepay, sandbox]
        description:
          type: string
        created_at:
          type: string
          format: date-time
        confirmed_at:
          type: string
          format: date-time
          nullable: true

paths:

  # ─── Providers ──────────────────────────────────────────────────────────────

  /api/providers/register:
    post:
      tags: [Providers]
      summary: Register a new GPU provider
      description: |
        Creates a new provider account and returns an API key and installer URL.
        Rate limited to 5 requests per IP per hour.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, gpu_model, os]
              properties:
                name:
                  type: string
                  example: Ahmed Al-Rashidi
                email:
                  type: string
                  format: email
                  example: ahmed@example.sa
                gpu_model:
                  type: string
                  example: NVIDIA RTX 3090
                os:
                  type: string
                  enum: [windows, linux, mac, darwin]
                  example: linux
                phone:
                  type: string
                  nullable: true
                resource_spec:
                  type: object
                  nullable: true
                  description: Ocean-style resource specification (cpu_count, ram_gb, etc.)
            example:
              name: Ahmed Al-Rashidi
              email: ahmed@example.sa
              gpu_model: NVIDIA RTX 3090
              os: linux
              phone: "+966500000000"
      responses:
        '200':
          description: Registration successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  provider_id:
                    type: integer
                  api_key:
                    type: string
                    example: dc1-provider-a1b2c3d4e5f6...
                  installer_url:
                    type: string
                  message:
                    type: string
              example:
                success: true
                provider_id: 42
                api_key: dc1-provider-a1b2c3d4e5f6...
                installer_url: /api/providers/installer?key=dc1-provider-a1b2c3d4e5f6...&os=linux
                message: Welcome Ahmed Al-Rashidi! Your API key is ready. Download the installer to get started.
        '400':
          description: Missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Missing required fields
        '409':
          description: A provider with this email already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: A provider with this email already exists
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/providers/login-email:
    post:
      tags: [Providers]
      summary: Retrieve API key by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: API key returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  api_key:
                    type: string
                  provider:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      email:
                        type: string
                      gpu_model:
                        type: string
                      status:
                        type: string
        '404':
          description: No provider account found with this email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/providers/me:
    get:
      tags: [Providers]
      summary: Provider self-service dashboard
      description: Returns full provider profile, GPU metrics, earnings, and recent jobs.
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
          description: Provider API key
      responses:
        '200':
          description: Provider dashboard data
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider:
                    allOf:
                      - $ref: '#/components/schemas/Provider'
                      - type: object
                        properties:
                          gpu_metrics:
                            type: object
                            properties:
                              utilization_pct:
                                type: number
                              vram_used_mib:
                                type: integer
                              temperature_c:
                                type: number
                          today_earnings_halala:
                            type: integer
                          week_earnings_halala:
                            type: integer
                          active_job:
                            $ref: '#/components/schemas/Job'
                            nullable: true
                  recent_jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/Job'
              example:
                provider:
                  id: 42
                  name: Ahmed Al-Rashidi
                  status: online
                  gpu_model: NVIDIA RTX 3090
                  gpu_vram_mib: 24576
                  gpu_count_reported: 1
                  gpu_compute_capability: "8.6"
                  gpu_cuda_version: "12.2"
                  run_mode: always-on
                  uptime_percent: 99.2
                  total_earnings_halala: 185000
                  total_jobs: 92
                  is_paused: false
                  gpu_usage_cap_pct: 80
                  vram_reserve_gb: 1
                  temp_limit_c: 85
                  gpu_metrics:
                    utilization_pct: 41
                    vram_used_mib: 12112
                    temperature_c: 68
                  today_earnings_halala: 4200
                  week_earnings_halala: 26800
                  active_job: null
                recent_jobs: []
        '400':
          description: API key required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/providers/heartbeat:
    post:
      tags: [Providers]
      summary: Provider daemon heartbeat
      description: |
        Sent every 30 seconds by the daemon. Updates GPU telemetry, sets provider
        status to `online`, recomputes reputation score, and returns job assignment
        signals and update availability.

        Rate limited to 4 per IP per minute.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key]
              properties:
                api_key:
                  type: string
                  example: dc1-provider-a1b2c3d4e5f6...
                gpu_status:
                  type: object
                  description: GPU telemetry snapshot from nvidia-smi
                  properties:
                    gpu_name:
                      type: string
                    gpu_vram_mib:
                      type: integer
                    driver_version:
                      type: string
                    gpu_util_pct:
                      type: number
                    temp_c:
                      type: number
                    power_w:
                      type: number
                    free_vram_mib:
                      type: integer
                    daemon_version:
                      type: string
                    python_version:
                      type: string
                    os_info:
                      type: string
                    compute_capability:
                      type: string
                    cuda_version:
                      type: string
                    gpu_count:
                      type: integer
                    all_gpus:
                      type: array
                      items:
                        type: object
                uptime:
                  type: number
                  description: Daemon uptime in seconds
                provider_ip:
                  type: string
                provider_hostname:
                  type: string
                cached_models:
                  type: array
                  items:
                    type: string
                resource_spec:
                  type: object
      responses:
        '200':
          description: Heartbeat acknowledged
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  timestamp:
                    type: string
                    format: date-time
                  update_available:
                    type: boolean
                    description: True if daemon version is below minimum required
                  needs_update:
                    type: boolean
                  latest_version:
                    type: string
                  min_version:
                    type: string
                    example: "3.3.0"
                  approval_status:
                    type: string
                  approved:
                    type: boolean
                  preload_model:
                    type: object
                    nullable: true
                    properties:
                      model_name:
                        type: string
                      status:
                        type: string
                        enum: [downloading]
              example:
                success: true
                message: Heartbeat received
                timestamp: "2026-03-22T19:30:00.000Z"
                needs_update: false
                latest_version: "3.3.0"
                update_available: false
                min_version: "3.3.0"
                approval_status: approved
                approved: true
                preload_model: null
        '400':
          description: Bad request payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Provider not approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Heartbeat failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/providers/daemon-event:
    post:
      tags: [Providers]
      summary: Log daemon events (crashes, errors, job lifecycle)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key, event_type]
              properties:
                api_key:
                  type: string
                event_type:
                  type: string
                  example: crash
                severity:
                  type: string
                  enum: [info, warning, error, critical]
                  default: info
                daemon_version:
                  type: string
                timestamp:
                  type: string
                  format: date-time
                hostname:
                  type: string
                os_info:
                  type: string
                python_version:
                  type: string
                details:
                  type: string
                  maxLength: 5000
                job_id:
                  type: string
      responses:
        '200':
          description: Event logged
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  event_type:
                    type: string
                  provider_id:
                    type: integer
        '401':
          description: Invalid API key

  /api/providers/readiness:
    post:
      tags: [Providers]
      summary: Daemon reports system readiness check
      description: Called once at daemon startup. Stores CUDA/PyTorch check results.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key]
              properties:
                api_key:
                  type: string
                daemon_version:
                  type: string
                checks:
                  type: object
                  properties:
                    cuda:
                      type: boolean
                    pytorch:
                      type: boolean
                    vram_gb:
                      type: number
                    driver:
                      type: string
      responses:
        '200':
          description: Readiness recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  readiness_status:
                    type: string
                    enum: [ready, failed]
                  checks:
                    type: object

  /api/providers/pause:
    post:
      tags: [Providers]
      summary: Pause provider (stop accepting new jobs)
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key:
                  type: string
      responses:
        '200':
          description: Provider paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  status:
                    type: string
                    example: paused

  /api/providers/resume:
    post:
      tags: [Providers]
      summary: Resume provider (accept jobs again)
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key:
                  type: string
      responses:
        '200':
          description: Provider resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  status:
                    type: string
                    enum: [online, connected]

  /api/providers/preferences:
    post:
      tags: [Providers]
      summary: Update provider preferences
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key:
                  type: string
                run_mode:
                  type: string
                  enum: [always-on, manual, scheduled]
                scheduled_start:
                  type: string
                  example: "23:00"
                  description: HH:MM — start time for scheduled mode
                scheduled_end:
                  type: string
                  example: "07:00"
                  description: HH:MM — end time for scheduled mode
                gpu_usage_cap_pct:
                  type: integer
                  minimum: 0
                  maximum: 100
                vram_reserve_gb:
                  type: number
                  minimum: 0
                  maximum: 16
                temp_limit_c:
                  type: integer
                  minimum: 50
                  maximum: 100
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  preferences:
                    type: object

  /api/providers/setup:
    get:
      tags: [Providers]
      summary: Download daemon.sh with injected API key (Linux/Mac)
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Shell script with API key pre-filled
          content:
            text/x-shellscript:
              schema:
                type: string

  /api/providers/setup-windows:
    get:
      tags: [Providers]
      summary: Download daemon.ps1 with injected API key (Windows)
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
      responses:
        '200':
          description: PowerShell script with API key pre-filled
          content:
            text/plain:
              schema:
                type: string

  /api/providers/download:
    get:
      tags: [Providers]
      summary: Download installer script with injected API key
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
        - in: query
          name: platform
          schema:
            type: string
            enum: [linux, mac, darwin, windows]
      responses:
        '200':
          description: Installer script
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

  /api/providers/download/daemon:
    get:
      tags: [Providers]
      summary: Download dc1_daemon.py
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
        - in: query
          name: check_only
          schema:
            type: string
            enum: ["true", "false"]
          description: If "true", return version info only without downloading
      responses:
        '200':
          description: Daemon Python script or version info
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: string
                  min_version:
                    type: string
                  download_url:
                    type: string

  /api/providers/download-windows-exe:
    get:
      tags: [Providers]
      summary: Download generic Windows .exe installer
      responses:
        '200':
          description: Windows installer binary
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '404':
          description: Installer not yet built

  /api/providers/installer:
    get:
      tags: [Providers]
      summary: Download platform-specific installer binary
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
        - in: query
          name: os
          required: true
          schema:
            type: string
            enum: [Windows, Linux, Mac]
      responses:
        '200':
          description: Installer binary
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

  /api/providers/status/{api_key}:
    get:
      tags: [Providers]
      summary: Get provider status by API key (daemon-facing)
      parameters:
        - in: path
          name: api_key
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Provider status
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider_id:
                    type: integer
                  name:
                    type: string
                  status:
                    type: string
                  gpu_model:
                    type: string
                  last_heartbeat:
                    type: string
                    format: date-time
                  total_earnings:
                    type: number
                  total_jobs:
                    type: integer
                  uptime_percent:
                    type: number

  /api/providers/{api_key}/jobs:
    get:
      tags: [Providers]
      summary: Daemon polls for assigned pending jobs
      description: Returns the oldest pending job assigned to this provider, or `null` if none.
      parameters:
        - in: path
          name: api_key
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Next job or null
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    nullable: true
                    type: object
                    properties:
                      id:
                        type: integer
                      job_id:
                        type: string
                      job_type:
                        type: string
                      task_spec:
                        type: object
                      task_spec_hmac:
                        type: string
                        description: HMAC-SHA256 of task_spec for integrity verification
                      gpu_requirements:
                        type: object
                        nullable: true
                      duration_minutes:
                        type: integer
                      max_duration_seconds:
                        type: integer

  /api/providers/job-result:
    post:
      tags: [Providers]
      summary: Daemon submits completed job result
      description: |
        Called by daemon after job completion or failure. Triggers billing settlement:
        - On success: provider earns 75%, DCP takes 25%, renter balance reconciled.
        - On failure: renter balance refunded.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key, job_id]
              properties:
                api_key:
                  type: string
                job_id:
                  type: string
                success:
                  type: boolean
                result:
                  description: Job output (JSON or string)
                error:
                  type: string
                  nullable: true
                metrics:
                  type: object
                  nullable: true
      responses:
        '200':
          description: Result recorded and billing settled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    enum: [completed, failed]
                  actual_minutes:
                    type: integer
                  cost_halala:
                    type: integer
                  provider_earned_halala:
                    type: integer
                  dc1_fee_halala:
                    type: integer

  # ─── Renters ────────────────────────────────────────────────────────────────

  /api/renters/register:
    post:
      tags: [Renters]
      summary: Register a new renter account
      description: |
        Creates a renter account.
        Bonus, welcome credits, and rollout-specific limits are defined by deployment policy.
        Rate limited to 5 per IP per hour.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name:
                  type: string
                  example: Fatima Al-Saud
                email:
                  type: string
                  format: email
                  example: fatima@example.sa
                organization:
                  type: string
                  nullable: true
                use_case:
                  type: string
                  nullable: true
                phone:
                  type: string
                  nullable: true
            example:
              name: Fatima Al-Saud
              email: fatima@example.sa
              organization: Riyadh AI Lab
              use_case: llm_inference
              phone: "+966500000000"
      responses:
        '201':
          description: Registration successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  renter_id:
                    type: integer
                  api_key:
                    type: string
                    example: dc1-renter-a1b2c3d4e5f6...
                  message:
                    type: string
              example:
                success: true
                renter_id: 101
                api_key: dc1-renter-a1b2c3d4e5f6...
                message: Welcome Fatima Al-Saud! Save your API key — it won't be shown again.
        '400':
          description: Missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/renters/me:
    get:
      tags: [Renters]
      summary: Renter self-service profile and recent jobs
      security:
        - RenterApiKey: []
      parameters:
        - in: query
          name: key
          required: false
          schema:
            type: string
          description: Renter API key via query param. Use `x-renter-key` header when possible.
        - in: header
          name: x-renter-key
          required: false
          schema:
            type: string
          description: Preferred renter API key header.
      responses:
        '200':
          description: Renter profile and recent jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  renter:
                    $ref: '#/components/schemas/Renter'
                  recent_jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/Job'
              example:
                renter:
                  id: 101
                  name: Fatima Al-Saud
                  email: fatima@example.sa
                  organization: Riyadh AI Lab
                  use_case: llm_inference
                  phone: "+966500000000"
                  webhook_url: null
                  balance_halala: 955
                  total_spent_halala: 12045
                  total_jobs: 34
                  created_at: "2026-03-22T10:12:00.000Z"
                recent_jobs: []
        '400':
          description: API key required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Renter not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Failed to fetch renter data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/renters/available-providers:
    get:
      tags: [Renters]
      summary: Browse online GPU providers
      description: Public endpoint. Returns all providers with `status=online` and `is_paused=false`, sorted by VRAM descending.
      responses:
        '200':
          description: Available providers
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        gpu_model:
                          type: string
                        vram_gb:
                          type: integer
                        vram_mib:
                          type: integer
                        gpu_count:
                          type: integer
                        driver_version:
                          type: string
                        compute_capability:
                          type: string
                        cuda_version:
                          type: string
                        status:
                          type: string
                        is_live:
                          type: boolean
                          description: True if heartbeat received within the last 120 seconds
                        location:
                          type: string
                          nullable: true
                        reliability_score:
                          type: number
                          nullable: true
                        cached_models:
                          type: array
                          items:
                            type: string
                        discovery_source:
                          type: string
                          enum: [sqlite, dht]
                        discovered_at:
                          type: string
                          nullable: true
                        addrs:
                          type: array
                          items:
                            type: string
                        stale:
                          type: boolean
                  total:
                    type: integer
                  discovery_mode:
                    type: string
                    enum: [sqlite, shadow, p2p-primary]
                  discovery_health:
                    type: object
                    properties:
                      mode:
                        type: string
                      enabled:
                        type: boolean
                      announcement_enabled:
                        type: boolean
                      bootstrap_configured:
                        type: boolean

  /api/renters/balance:
    get:
      tags: [Renters]
      summary: Quick balance check
      security:
        - RenterApiKey: []
      parameters:
        - name: key
          in: query
          schema:
            type: string
        - name: x-renter-key
          in: header
          schema:
            type: string
      responses:
        '200':
          description: Balance details
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance_halala:
                    type: integer
                  balance_sar:
                    type: number
                  held_halala:
                    type: integer
                    description: Estimated cost held for currently running jobs
                  held_sar:
                    type: number
                  available_halala:
                    type: integer
                  total_spent_halala:
                    type: integer
                  total_spent_sar:
                    type: number
                  total_jobs:
                    type: integer

  /api/renters/topup:
    post:
      tags: [Renters]
      summary: Development wallet top-up endpoint (not production checkout flow)
      description: |
        Directly credits halala to renter balance. Accepts either `amount_halala`
        or `amount_sar`. This endpoint is intended for non-production flows and
        is rate-limited to 10/min.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_halala:
                  type: integer
                  description: Amount in halala (mutually exclusive with amount_sar)
                amount_sar:
                  type: number
                  description: Amount in SAR (mutually exclusive with amount_halala)
      responses:
        '200':
          description: Balance credited
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  topped_up_halala:
                    type: integer
                  topped_up_sar:
                    type: number
                  new_balance_halala:
                    type: integer
                  new_balance_sar:
                    type: number

  /api/renters/login-email:
    post:
      tags: [Renters]
      summary: Retrieve API key by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: API key returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  api_key:
                    type: string
                  renter:
                    $ref: '#/components/schemas/Renter'

  /api/renters/rotate-key:
    post:
      tags: [Renters]
      summary: Rotate renter API key
      security:
        - RenterApiKey: []
      responses:
        '200':
          description: New API key issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  api_key:
                    type: string
                  renter_id:
                    type: integer

  # ─── Jobs ───────────────────────────────────────────────────────────────────

  /api/jobs/submit:
    post:
      tags: [Jobs]
      summary: Submit a compute job
      description: |
        Validates renter balance, deducts estimated cost as a pre-pay hold, assigns
        provider, and queues the job.

        Enforced controls:
        - Rate limit: **10 submissions/min per renter API key**
        - Balance gate: renter `balance_halala` must be `> 0`
        - Quotas: `daily_jobs_limit` and `monthly_spend_limit_halala` (from `renter_quota`)

        **Supported job types and cost rates (halala/min):**
        | Type | Rate |
        |------|------|
        | `llm-inference` | 15 |
        | `training` | 25 |
        | `rendering` | 20 |
        | `image_generation` | 20 |
        | `vllm_serve` | 20 |
        | `benchmark` | 10 |
        | `custom_container` | 10 |
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_type, duration_minutes]
              properties:
                provider_id:
                  type: integer
                  minimum: 1
                  nullable: true
                job_type:
                  type: string
                  enum: [image_generation, llm-inference, llm_inference, rendering, training, benchmark, custom_container, vllm_serve]
                  example: image_generation
                duration_minutes:
                  type: number
                  minimum: 0.01
                  maximum: 1440
                  default: 10
                  description: Estimated runtime. Used for pre-pay cost calculation.
                params:
                  type: object
                  description: |
                    Job-specific parameters. For supported types, the API auto-generates
                    the task payload from these params (no raw Python task required).

                    **image_generation params:**
                    - `prompt` (string)
                    - `negative_prompt` (string)
                    - `steps` (int, 5–100)
                    - `width` / `height` (int, 256–1024)
                    - `seed` (int)
                    - `model` (enum — see allowed SD models)

                    **llm-inference params:**
                    - `prompt` (string)
                    - `max_tokens` (int, 32–4096)
                    - `temperature` (float, 0.1–2.0)
                    - `model` (enum — see allowed LLM models)
                container_spec:
                  type: object
                  nullable: true
                  description: Container execution profile. `image_type` is required after normalization.
                gpu_requirements:
                  type: object
                  nullable: true
                  description: Optional GPU filter (min_vram_gb, gpu_model, etc.)
                max_duration_seconds:
                  type: integer
                  default: 1800
                  description: Hard timeout. Job is killed if it exceeds this.
                priority:
                  type: integer
                  enum: [1, 2, 3]
                  default: 2
                  description: "1=high, 2=normal, 3=low"
            example:
              provider_id: 42
              job_type: llm_inference
              duration_minutes: 3
              max_duration_seconds: 180
              container_spec:
                image_type: vllm-serve
              params:
                model: TinyLlama/TinyLlama-1.1B-Chat-v1.0
                prompt: Summarize DCP in three bullets.
      responses:
        '201':
          description: Job submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job:
                    type: object
                    properties:
                      id:
                        type: integer
                      job_id:
                        type: string
                      provider_id:
                        type: integer
                        nullable: true
                      renter_id:
                        type: integer
                      job_type:
                        type: string
                      status:
                        type: string
                        enum: [pending, queued]
                      submitted_at:
                        type: string
                        format: date-time
                      duration_minutes:
                        type: number
                      cost_halala:
                        type: integer
                      max_duration_seconds:
                        type: integer
                      timeout_at:
                        type: string
                        nullable: true
                      gpu_requirements:
                        type: object
                        nullable: true
                      container_spec:
                        type: object
                      workspace_volume_name:
                        type: string
                      checkpoint_enabled:
                        type: boolean
                      task_spec_signed:
                        type: boolean
                      priority:
                        type: integer
                      pricing_class:
                        type: string
                      prewarm_requested:
                        type: boolean
                      queue_position:
                        type: integer
                        nullable: true
                  queued:
                    type: boolean
                  message:
                    type: string
              example:
                success: true
                job:
                  id: 9001
                  job_id: job-1742671800000-ab12cd
                  provider_id: 42
                  renter_id: 101
                  job_type: llm_inference
                  model: TinyLlama/TinyLlama-1.1B-Chat-v1.0
                  status: pending
                  submitted_at: "2026-03-22T19:30:00.000Z"
                  duration_minutes: 3
                  cost_halala: 45
                  max_duration_seconds: 180
                  timeout_at: "2026-03-22 20:00:00.000"
                  gpu_requirements: null
                  container_spec:
                    image_type: vllm-serve
                    pricing_class: standard
                  workspace_volume_name: dcp-job-job-1742671800000-ab12cd
                  checkpoint_enabled: false
                  task_spec_signed: true
                  priority: 2
                  pricing_class: standard
                  prewarm_requested: false
                  queue_position: null
        '400':
          description: Invalid request (missing job_type, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient balance or zero wallet balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Renter submission rate limit or quota exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: No providers available

  /api/jobs/{job_id}:
    get:
      tags: [Jobs]
      summary: Get job status and details
      security:
        - RenterApiKey: []
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
          description: Job UUID
      responses:
        '200':
          description: Job details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '403':
          description: Access denied (not your job)
        '404':
          description: Job not found

  /api/jobs/{job_id}/output:
    get:
      tags: [Jobs]
      summary: Fetch job output
      description: Returns the job result payload. For `image_generation`, includes base64-encoded PNG.
      security:
        - RenterApiKey: []
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '202':
          description: Job still running or queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
                  progress_phase:
                    type: string
                    nullable: true
                  job_id:
                    type: string
        '200':
          description: Job output
          content:
            application/json:
              schema:
                type: object
                description: Varies by job_type. For images contains `data` (base64 PNG). For text contains `response`.
        '410':
          description: Job failed or was cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [failed, cancelled]
                  error:
                    type: string
                  job_id:
                    type: string
        '404':
          description: Job not found or no output yet

  /api/jobs/{job_id}/output/{format}:
    get:
      tags: [Jobs]
      summary: Fetch job output in a specific format
      security:
        - RenterApiKey: []
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
        - in: path
          name: format
          required: true
          schema:
            type: string
            enum: [json, png, raw]
      responses:
        '200':
          description: Job output in requested format

  /api/jobs/{job_id}/logs:
    get:
      tags: [Jobs]
      summary: Get job execution logs
      description: Returns stored log entries. Use `?attempt=N` to retrieve logs from a specific execution attempt (see `/api/jobs/{id}/history`).
      security:
        - RenterApiKey: []
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
        - in: query
          name: attempt
          required: false
          schema:
            type: integer
            minimum: 1
          description: Attempt number. Omit for latest attempt.
      responses:
        '200':
          description: Log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  logs:
                    type: array
                    items:
                      type: object
                  attempt:
                    type: integer
                    description: Attempt number these logs belong to
    post:
      tags: [Jobs]
      summary: Daemon appends log entries for a job
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                logs:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Logs appended

  /api/jobs/{job_id}/progress:
    post:
      tags: [Jobs]
      summary: Daemon reports job progress
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                progress_pct:
                  type: number
                  minimum: 0
                  maximum: 100
                phase:
                  type: string
                  description: Phase marker (e.g. downloading_model, generating)
      responses:
        '200':
          description: Progress recorded

  /api/jobs/{job_id}/endpoint-ready:
    post:
      tags: [Jobs]
      summary: Daemon signals that a long-running endpoint is ready
      description: Used by `vllm_serve` jobs to announce the inference endpoint URL.
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                endpoint_url:
                  type: string
                  format: uri
      responses:
        '200':
          description: Endpoint URL stored

  /api/jobs/{job_id}/complete:
    post:
      tags: [Jobs]
      summary: Mark job as complete (daemon)
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                result:
                  description: Job output payload
      responses:
        '200':
          description: Job completed, billing settled

  /api/jobs/{job_id}/fail:
    post:
      tags: [Jobs]
      summary: Mark job as failed (daemon)
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                error:
                  type: string
      responses:
        '200':
          description: Job failed, renter refunded

  /api/jobs/{job_id}/cancel:
    post:
      tags: [Jobs]
      summary: Cancel a job
      security:
        - RenterApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job cancelled

  /api/jobs/history:
    get:
      tags: [Jobs]
      summary: Job history for a renter
      description: Requires `x-renter-key` header. This endpoint does not use `?key=`.
      security:
        - RenterApiKey: []
      parameters:
        - in: header
          name: x-renter-key
          required: true
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Paginated job history
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/Job'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      total:
                        type: integer

  /api/jobs/assigned:
    get:
      tags: [Jobs]
      summary: Get jobs assigned to a provider (daemon polling)
      security:
        - ProviderApiKey: []
      responses:
        '200':
          description: Assigned jobs list

  /api/jobs/active:
    get:
      tags: [Jobs]
      summary: Get currently running jobs
      security:
        - AdminToken: []
      responses:
        '200':
          description: Active jobs

  /api/jobs/queue/{provider_id}:
    get:
      tags: [Jobs]
      summary: View queue for a specific provider
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: provider_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Queued jobs for provider

  # ─── Payments ───────────────────────────────────────────────────────────────

  /api/payments/topup:
    post:
      tags: [Payments]
      summary: Initiate a Moyasar SAR payment
      description: |
        Creates a Moyasar payment and returns a hosted checkout URL. Renter must
        be redirected to `checkout_url` to complete payment. The webhook confirms
        and credits the balance. Rate limited to 10/min.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_sar]
              properties:
                amount_sar:
                  type: number
                  minimum: 1
                  maximum: 10000
                  example: 100
                source_type:
                  type: string
                  enum: [creditcard, mada, applepay]
                  default: creditcard
                callback_url:
                  type: string
                  format: uri
                  nullable: true
                  description: Override default redirect URL after payment
      responses:
        '200':
          description: Payment initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  payment_id:
                    type: string
                  amount_sar:
                    type: number
                  amount_halala:
                    type: integer
                  status:
                    type: string
                  checkout_url:
                    type: string
                    format: uri
                  message:
                    type: string
        '503':
          description: Payment gateway not configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  sandbox_hint:
                    type: string

  /api/payments/topup-sandbox:
    post:
      tags: [Payments]
      summary: Dev-only sandbox balance top-up
      description: |
        Directly credits balance without Moyasar. Only available when
        `MOYASAR_SECRET_KEY` is **not** set (dev/staging environments). Disabled in production.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_sar]
              properties:
                amount_sar:
                  type: number
                  minimum: 0.01
                  maximum: 10000
      responses:
        '200':
          description: Balance credited (sandbox)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sandbox:
                    type: boolean
                  payment_id:
                    type: string
                  amount_sar:
                    type: number
                  credited_halala:
                    type: integer
                  new_balance_sar:
                    type: number
                  new_balance_halala:
                    type: integer
        '403':
          description: Sandbox disabled in production

  /api/payments/webhook:
    post:
      tags: [Payments]
      summary: Moyasar payment webhook
      description: |
        Receives payment events from Moyasar. Verifies HMAC-SHA256 signature
        (`x-moyasar-signature` header). Credits balance on `paid` event.
        Must return 2xx or Moyasar will retry.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: string
                  description: Moyasar payment ID
                status:
                  type: string
                  enum: [initiated, paid, failed, refunded]
                amount:
                  type: integer
                  description: Amount in halala
                amount_refunded:
                  type: integer
                  nullable: true
      responses:
        '200':
          description: Webhook acknowledged
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                  action:
                    type: string
                    enum: [balance_credited, marked_failed, refund_processed, already_processed, ignored_unknown, status_updated]
        '401':
          description: Invalid HMAC signature

  /api/payments/verify/{paymentId}:
    get:
      tags: [Payments]
      summary: Verify payment status (post-redirect polling)
      description: Fetches live status from Moyasar. Used by frontend to poll after redirect back from checkout.
      security:
        - RenterApiKey: []
      parameters:
        - in: path
          name: paymentId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Payment status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
        '404':
          description: Payment not found or not owned by this renter

  /api/payments/history:
    get:
      tags: [Payments]
      summary: Renter payment history
      security:
        - RenterApiKey: []
      parameters:
        - in: query
          name: key
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 20
            maximum: 100
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated payment history
          content:
            application/json:
              schema:
                type: object
                properties:
                  payments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Payment'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      total:
                        type: integer
                  summary:
                    type: object
                    properties:
                      total_paid_sar:
                        type: number
                      total_paid_halala:
                        type: integer

  # ─── Admin ──────────────────────────────────────────────────────────────────

  /api/admin/dashboard:
    get:
      tags: [Admin]
      summary: Platform overview stats
      security:
        - AdminToken: []
      responses:
        '200':
          description: Dashboard stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      total_providers:
                        type: integer
                      online_now:
                        type: integer
                      total_renters:
                        type: integer
                      active_renters:
                        type: integer
                      total_jobs:
                        type: integer
                      completed_jobs:
                        type: integer
                      failed_jobs:
                        type: integer
                      active_jobs:
                        type: integer
                      total_revenue_halala:
                        type: integer
                      total_dc1_fees_halala:
                        type: integer
                      today_revenue_halala:
                        type: integer
                      today_jobs:
                        type: integer
                  gpu_breakdown:
                    type: array
                    items:
                      type: object
                  recent_signups:
                    type: array
                    items:
                      type: object
                  recent_heartbeats:
                    type: array
                    items:
                      type: object
        '401':
          description: Admin access denied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Admin token not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/admin/providers:
    get:
      tags: [Admin]
      summary: List all providers
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: page
          schema:
            type: integer
            default: 0
          description: Page number (0 = all, 1+ = paginated)
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
            maximum: 200
        - in: query
          name: search
          schema:
            type: string
          description: Filter by name, email, or GPU model
        - in: query
          name: status
          schema:
            type: string
            enum: [online, offline, registered, suspended]
      responses:
        '200':
          description: Providers list
    post:
      tags: [Admin]
      summary: Bulk provider operations
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Bulk operation result

  /api/admin/providers/{id}:
    get:
      tags: [Admin]
      summary: Full provider detail with heartbeat metrics
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Provider detail with uptime, metrics, and job history

  /api/admin/providers/{id}/suspend:
    post:
      tags: [Admin]
      summary: Suspend a provider
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Provider suspended

  /api/admin/providers/{id}/unsuspend:
    post:
      tags: [Admin]
      summary: Unsuspend a provider
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Provider unsuspended

  /api/admin/providers/{id}/rotate-key:
    post:
      tags: [Admin]
      summary: Rotate a provider API key (admin override)
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: New API key issued

  /api/admin/renters:
    get:
      tags: [Admin]
      summary: List all renters
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: search
          schema:
            type: string
      responses:
        '200':
          description: Renters list

  /api/admin/renters/{id}:
    get:
      tags: [Admin]
      summary: Full renter detail
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Renter detail with jobs and payment history

  /api/admin/renters/{id}/suspend:
    post:
      tags: [Admin]
      summary: Suspend a renter
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Renter suspended

  /api/admin/renters/{id}/unsuspend:
    post:
      tags: [Admin]
      summary: Unsuspend a renter
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Renter unsuspended

  /api/admin/renters/{id}/balance:
    post:
      tags: [Admin]
      summary: Admin balance adjustment for a renter
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_halala:
                  type: integer
                reason:
                  type: string
      responses:
        '200':
          description: Balance adjusted

  /api/admin/renters/{id}/rotate-key:
    post:
      tags: [Admin]
      summary: Rotate a renter API key (admin override)
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: New API key issued

  /api/admin/jobs:
    get:
      tags: [Admin]
      summary: List all jobs
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Jobs list

  /api/admin/jobs/{id}:
    get:
      tags: [Admin]
      summary: Full job detail with billing split
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Job ID or job_id UUID
      responses:
        '200':
          description: Job detail with provider info and billing breakdown

  /api/admin/jobs/{id}/cancel:
    post:
      tags: [Admin]
      summary: Force cancel a job
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job cancelled

  /api/admin/daemon-health:
    get:
      tags: [Admin]
      summary: Daemon health report across all providers
      security:
        - AdminToken: []
      responses:
        '200':
          description: Per-provider daemon health metrics

  /api/admin/finance/summary:
    get:
      tags: [Admin]
      summary: Platform financial summary
      security:
        - AdminToken: []
      responses:
        '200':
          description: Revenue, fees, and balance breakdown

  /api/admin/finance/transactions:
    get:
      tags: [Admin]
      summary: Financial transactions ledger
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: offset
          schema:
            type: integer
      responses:
        '200':
          description: Transaction records

  /api/admin/finance/reconciliation:
    get:
      tags: [Admin]
      summary: Financial reconciliation report
      security:
        - AdminToken: []
      responses:
        '200':
          description: Reconciliation data

  /api/admin/withdrawals:
    get:
      tags: [Admin]
      summary: List provider withdrawal requests
      security:
        - AdminToken: []
      responses:
        '200':
          description: Withdrawal requests

  /api/admin/withdrawals/{id}/approve:
    post:
      tags: [Admin]
      summary: Approve a withdrawal request
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Withdrawal approved

  /api/admin/withdrawals/{id}/reject:
    post:
      tags: [Admin]
      summary: Reject a withdrawal request
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Withdrawal rejected

  /api/admin/withdrawals/{id}/complete:
    post:
      tags: [Admin]
      summary: Mark withdrawal as completed (payment sent)
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Withdrawal completed

  /api/admin/payments:
    get:
      tags: [Admin]
      summary: List all platform payments
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
      responses:
        '200':
          description: Payments list

  /api/admin/payments/revenue:
    get:
      tags: [Admin]
      summary: Payment revenue summary
      security:
        - AdminToken: []
      responses:
        '200':
          description: Revenue from Moyasar payments

  /api/admin/payments/{paymentId}/refund:
    post:
      tags: [Admin]
      summary: Initiate a refund via Moyasar
      security:
        - AdminToken: []
      parameters:
        - in: path
          name: paymentId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_halala:
                  type: integer
                  description: Partial refund amount (omit for full refund)
                reason:
                  type: string
      responses:
        '200':
          description: Refund initiated

  /api/admin/escrow:
    get:
      tags: [Admin]
      summary: Escrow holds overview
      security:
        - AdminToken: []
      responses:
        '200':
          description: Escrow balances and status

  /api/admin/security/events:
    get:
      tags: [Admin]
      summary: Security event log
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: severity
          schema:
            type: string
      responses:
        '200':
          description: Security events

  /api/admin/security/summary:
    get:
      tags: [Admin]
      summary: Security events summary
      security:
        - AdminToken: []
      responses:
        '200':
          description: Security summary stats

  /api/admin/audit:
    get:
      tags: [Admin]
      summary: Admin audit log
      security:
        - AdminToken: []
      responses:
        '200':
          description: Audit entries

  /api/admin/notifications/config:
    get:
      tags: [Admin]
      summary: Get notification config (Telegram, email)
      security:
        - AdminToken: []
      responses:
        '200':
          description: Current notification config
    post:
      tags: [Admin]
      summary: Update notification config
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                telegram_bot_token:
                  type: string
                telegram_chat_id:
                  type: string
                email_to:
                  type: string
      responses:
        '200':
          description: Config updated

  /api/admin/notifications/test:
    post:
      tags: [Admin]
      summary: Send a test notification
      security:
        - AdminToken: []
      responses:
        '200':
          description: Test notification sent

  /api/admin/health:
    get:
      tags: [Admin]
      summary: Detailed system health (DB, sync, queues)
      security:
        - AdminToken: []
      responses:
        '200':
          description: System health report

  /api/admin/cleanup/stats:
    get:
      tags: [Admin]
      summary: Data retention cleanup statistics
      security:
        - AdminToken: []
      responses:
        '200':
          description: Cleanup stats (rows purged, last run, etc.)

  # ─── System ─────────────────────────────────────────────────────────────────

  /api/health:
    get:
      tags: [System]
      summary: API health check
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  service:
                    type: string
                    example: dcp-platform-api
                  mode:
                    type: string
                    example: headless
                  timestamp:
                    type: string
                    format: date-time

  # ──────────────────────────────────────────────────────────────────────────────
  # Templates
  # ──────────────────────────────────────────────────────────────────────────────

  /api/templates:
    get:
      tags: [Templates]
      summary: List compute templates
      description: |
        Returns all available Docker compute templates, sorted by `sort_order`.
        Optionally filter by tag (e.g. `llm`, `image-gen`, `training`).
        The `approved_images` field is omitted from list responses for security —
        use `/api/templates/whitelist` for the daemon image whitelist.
      parameters:
        - name: tag
          in: query
          required: false
          schema:
            type: string
            example: llm
          description: Filter templates by tag
      responses:
        '200':
          description: Template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/Template'
                  count:
                    type: integer
                    example: 4

  /api/templates/whitelist:
    get:
      tags: [Templates]
      summary: Approved Docker image whitelist
      description: |
        Returns the full list of approved Docker images that the dc1_daemon.py
        will accept for job execution. Used by the daemon on startup and on each
        job pull to validate the image field before launching a container.
        No authentication required — this is a public endpoint.
      responses:
        '200':
          description: Approved image list
          content:
            application/json:
              schema:
                type: object
                properties:
                  approved_images:
                    type: array
                    items:
                      type: string
                    example:
                      - dc1/llm-worker:latest
                      - dc1/sd-worker:latest
                      - pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime

  /api/templates/{template_id}:
    get:
      tags: [Templates]
      summary: Get a single compute template
      parameters:
        - name: template_id
          in: path
          required: true
          schema:
            type: string
            example: llm-inference-v1
      responses:
        '200':
          description: Template detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ──────────────────────────────────────────────────────────────────────────────
  # Provider — Withdrawals
  # ──────────────────────────────────────────────────────────────────────────────

  /api/providers/withdraw:
    post:
      tags: [Providers]
      summary: Request an earnings withdrawal
      description: |
        Provider submits a withdrawal request for a portion of their claimable
        earnings. Payout method, timing, and minimum threshold are
        configured in platform payout settings.

        The request is created with status `pending` and appears in the admin
        withdrawal queue at `/api/admin/withdrawals`.
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key, amount_sar]
              properties:
                api_key:
                  type: string
                  description: Provider API key
                  example: dc1-provider-abcdef1234567890
                amount_sar:
                  type: number
                  format: float
                  description: Amount to withdraw in SAR (platform-configured minimum)
                  example: 250.00
                payout_method:
                  type: string
                  enum: [bank_transfer, stc_pay, crypto]
                  default: bank_transfer
                  example: bank_transfer
                payout_details:
                  type: object
                  description: Method-specific payout details (IBAN, phone, wallet address)
                  example:
                    iban: SA4420000001234567891234
                    account_name: Ahmed Al-Rashidi
      responses:
        '201':
          description: Withdrawal request created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  withdrawal_id:
                    type: string
                    example: wd-1710843600000-ab3f
                  amount_sar:
                    type: number
                    example: 250.00
                  status:
                    type: string
                    example: pending
                  message:
                    type: string
                    example: Withdrawal request submitted.
        '400':
          description: Missing or invalid fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient available earnings
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Insufficient available earnings
                  available_sar:
                    type: string
                    example: "120.50"
                  available_halala:
                    type: integer
                    example: 12050
                  requested_sar:
                    type: number
                    example: 250.00

  /api/providers/withdrawal-history:
    get:
      tags: [Providers]
      summary: Provider withdrawal history
      description: |
        Returns the provider's last 50 withdrawal requests with status and timestamps.
      security:
        - ProviderApiKey: []
      parameters:
        - name: key
          in: query
          schema:
            type: string
          description: Provider API key (alternative to x-provider-key header)
      responses:
        '200':
          description: Withdrawal history
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider_id:
                    type: integer
                    example: 7
                  withdrawals:
                    type: array
                    items:
                      type: object
                      properties:
                        withdrawal_id:
                          type: string
                          example: wd-1710843600000-ab3f
                        amount_sar:
                          type: number
                          example: 250.00
                        payout_method:
                          type: string
                          example: bank_transfer
                        status:
                          type: string
                          enum: [pending, approved, rejected, completed]
                          example: completed
                        requested_at:
                          type: string
                          format: date-time
                        processed_at:
                          type: string
                          format: date-time
                          nullable: true
        '400':
          description: API key required
        '401':
          description: Invalid API key

  # ──────────────────────────────────────────────────────────────────────────────
  # Admin — On-chain Escrow
  # ──────────────────────────────────────────────────────────────────────────────

  /api/admin/escrow-chain/status:
    get:
      tags: [Admin]
      summary: On-chain escrow service status
      description: |
        Returns the current status of the on-chain escrow service:
        contract address, network, oracle address, and whether the service
        is enabled. Used to monitor the blockchain escrow integration (DCP-75).
      security:
        - AdminToken: []
      responses:
        '200':
          description: Escrow chain status
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                    example: true
                  network:
                    type: string
                    example: base-sepolia
                  contract_address:
                    type: string
                    example: "0x1234567890abcdef1234567890abcdef12345678"
                  oracle_address:
                    type: string
                    example: "0xabcdef1234567890abcdef1234567890abcdef12"
                  chain_id:
                    type: integer
                    example: 84532
        '401':
          description: Admin token required
        '500':
          description: Failed to fetch escrow-chain status

  /api/providers/download/setup:
    get:
      tags: [Providers]
      summary: Download OS-specific setup script with injected key
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
        - in: query
          name: os
          schema:
            type: string
            enum: [windows, linux, mac]
      responses:
        '200':
          description: Setup script file (PowerShell or shell script)
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/providers/daemon/windows:
    get:
      tags: [Providers]
      summary: Download Windows provider installer EXE
      responses:
        '200':
          description: Windows installer binary
        '404':
          description: Installer not built

  /api/providers/daemon/linux:
    get:
      tags: [Providers]
      summary: Download Linux install script
      responses:
        '200':
          description: install.sh script
        '404':
          description: Linux install script not found

  /api/providers/earnings:
    get:
      tags: [Providers]
      summary: Provider earnings and escrow balance
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          schema:
            type: string
          description: Provider API key (alternative to x-provider-key header)
      responses:
        '200':
          description: Earnings summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider_id:
                    type: integer
                  total_earned_halala:
                    type: integer
                  claimable_earnings_halala:
                    type: integer
                  available_halala:
                    type: integer
                  available_sar:
                    type: string
                    example: "120.50"

  /api/providers/job-history:
    get:
      tags: [Providers]
      summary: Provider job history and totals
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
            maximum: 200
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Provider job history

  /api/providers/earnings-daily:
    get:
      tags: [Providers]
      summary: Daily earnings breakdown
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: days
          schema:
            type: integer
            default: 30
            maximum: 90
      responses:
        '200':
          description: Daily earnings rows in halala and SAR

  /api/providers/daemon-logs:
    get:
      tags: [Providers]
      summary: Provider daemon events and runtime info
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
            maximum: 200
        - in: query
          name: severity
          schema:
            type: string
      responses:
        '200':
          description: Daemon events list

  /api/providers/available:
    get:
      tags: [Providers]
      summary: Public marketplace list of live online providers
      description: Returns online, unpaused providers with GPU specs and indicative SAR rates.
      responses:
        '200':
          description: Provider marketplace data

  /api/providers/marketplace:
    get:
      tags: [Providers]
      summary: Public lightweight online-provider marketplace list
      description: Returns online providers for public marketplace cards. No auth required.
      responses:
        '200':
          description: Array of online provider cards
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                      example: 42
                    gpu_model:
                      type: string
                      example: NVIDIA RTX 4090
                    vram_gb:
                      type: number
                      nullable: true
                      example: 24
                    price_per_min_halala:
                      type: integer
                      example: 10
                    uptime_pct:
                      type: number
                      example: 99.2
                    jobs_completed:
                      type: integer
                      example: 318

  /api/providers/{id}/gpu-metrics:
    get:
      tags: [Providers]
      summary: GPU metrics history for charts
      security:
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Numeric provider id or `me`
        - in: query
          name: limit
          schema:
            type: integer
            default: 60
            maximum: 1440
        - in: query
          name: since
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Heartbeat metric samples

  /api/providers/rotate-key:
    post:
      tags: [Providers]
      summary: Rotate provider API key
      security:
        - ProviderApiKey: []
      responses:
        '200':
          description: New API key generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  api_key:
                    type: string

  /api/jobs/verify-hmac:
    get:
      tags: [Jobs]
      summary: Verify task HMAC for provider-owned job
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: job_id
          required: true
          schema:
            type: string
        - in: query
          name: hmac
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean

  /api/jobs/verify-hmac-local:
    get:
      tags: [Jobs]
      summary: Legacy daemon HMAC format validator
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: hmac
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Validation response

  /api/jobs/{job_id}/result:
    post:
      tags: [Jobs]
      summary: Provider daemon submits final result and settlement
      security:
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                result:
                  oneOf:
                    - type: string
                    - type: object
                error:
                  type: string
                duration_seconds:
                  type: integer
                transient:
                  type: boolean
      responses:
        '200':
          description: Job settled and billing applied

  /api/jobs/test:
    post:
      tags: [Jobs]
      summary: Admin-only benchmark job creation
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider_id]
              properties:
                provider_id:
                  type: integer
                matrix_size:
                  type: integer
                iterations:
                  type: integer
      responses:
        '200':
          description: Test job created

  /api/admin/bulk/providers:
    post:
      tags: [Admin]
      summary: Bulk suspend or unsuspend providers
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, action]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                action:
                  type: string
                  enum: [suspend, unsuspend]
      responses:
        '200':
          description: Bulk action results

  /api/admin/bulk/renters:
    post:
      tags: [Admin]
      summary: Bulk renter actions (suspend, unsuspend, credit)
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, action]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                action:
                  type: string
                  enum: [suspend, unsuspend, credit]
                amount_halala:
                  type: integer
                reason:
                  type: string
      responses:
        '200':
          description: Bulk action results
  /api/docs:
    get:
      tags: [System]
      summary: OpenAPI 3.0 spec (this document)
      responses:
        '200':
          description: OpenAPI YAML spec
          content:
            application/yaml:
              schema:
                type: string

  /api/docs/ui:
    get:
      tags: [System]
      summary: Swagger UI — interactive API explorer
      responses:
        '200':
          description: Swagger UI HTML page
          content:
            text/html:
              schema:
                type: string

  # ──────────────────────────────────────────────────────────────────────────────
  # Container Registry (DCP-315/319)
  # ──────────────────────────────────────────────────────────────────────────────

  /api/containers/registry:
    get:
      tags: [Containers]
      summary: List approved Docker images
      description: |
        Returns all images approved for use in DCP jobs. Each entry includes
        the image tag, description, associated job type, and Trivy scan status.
        No authentication required — public endpoint.
      responses:
        '200':
          description: Approved image registry
          content:
            application/json:
              schema:
                type: object
                properties:
                  images:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        image:
                          type: string
                          example: dc1/llm-worker:latest
                        description:
                          type: string
                        job_type:
                          type: string
                          example: llm_inference
                        scan_status:
                          type: string
                          enum: [pending, clean, flagged, failed]
                        scan_at:
                          type: string
                          format: date-time
                          nullable: true
                        approved_at:
                          type: string
                          format: date-time
                  total:
                    type: integer

  /api/admin/containers/approve-image:
    post:
      tags: [Containers, Admin]
      summary: Approve a custom Docker image
      description: |
        Adds a Docker image to the approved registry. Optionally triggers a
        Trivy security scan before final approval. Once approved, the image
        becomes available for `custom_container` jobs.
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  example: myorg/custom-worker:v1.2
                  description: Fully-qualified Docker image reference
                description:
                  type: string
                  description: Human-readable description of the image
                job_type:
                  type: string
                  description: Job type this image is intended for
                scan_first:
                  type: boolean
                  default: true
                  description: Run Trivy scan before approval. If scan finds CRITICAL vulnerabilities, approval is blocked.
      responses:
        '201':
          description: Image approved (or queued for scan)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  image:
                    type: string
                  status:
                    type: string
                    enum: [approved, scan_pending]
                  message:
                    type: string
        '400':
          description: Missing image field
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Image already in registry

  /api/admin/containers/scan-image:
    post:
      tags: [Containers, Admin]
      summary: Trigger a Trivy security scan on an image
      description: |
        Queues a Trivy vulnerability scan for the specified image. Results are
        stored and accessible via `/api/admin/containers/security-status`.
        Scan runs asynchronously — poll the security-status endpoint for results.
      security:
        - AdminToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  example: dc1/llm-worker:latest
      responses:
        '202':
          description: Scan queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  image:
                    type: string
                  scan_id:
                    type: string
                  message:
                    type: string
                    example: Trivy scan queued. Check /api/admin/containers/security-status for results.
        '400':
          description: Missing image field

  /api/admin/containers/security-status:
    get:
      tags: [Containers, Admin]
      summary: Container security scan dashboard
      description: |
        Returns scan results for all registry images. Highlights images with
        CRITICAL or HIGH severity CVEs. Use this to audit the registry before
        approving new images.
      security:
        - AdminToken: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum: [pending, clean, flagged, failed]
          description: Filter by scan status
        - in: query
          name: severity
          schema:
            type: string
            enum: [CRITICAL, HIGH, MEDIUM, LOW]
          description: Return only images with findings at or above this severity
      responses:
        '200':
          description: Security scan summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total_images:
                        type: integer
                      clean:
                        type: integer
                      flagged:
                        type: integer
                      pending:
                        type: integer
                  images:
                    type: array
                    items:
                      type: object
                      properties:
                        image:
                          type: string
                        scan_status:
                          type: string
                        scanned_at:
                          type: string
                          format: date-time
                          nullable: true
                        critical_cves:
                          type: integer
                        high_cves:
                          type: integer
                        findings_summary:
                          type: string
                          nullable: true

  # ──────────────────────────────────────────────────────────────────────────────
  # Job Execution — History, Streaming Logs, Queue Status, Retry, Pause/Resume
  # (DCP-314, DCP-316, DCP-317, Sprint 20)
  # ──────────────────────────────────────────────────────────────────────────────

  /api/jobs/{job_id}/history:
    get:
      tags: [Jobs]
      summary: Job execution attempt history
      description: |
        Returns a log of all execution attempts for a job. Each entry records
        the attempt number, start/end times, exit status, and error message.
        Useful for debugging retry scenarios.
      security:
        - RenterApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
          description: Job UUID or numeric ID
      responses:
        '200':
          description: Execution attempt history
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                  attempts:
                    type: array
                    items:
                      type: object
                      properties:
                        attempt:
                          type: integer
                          description: Attempt number (1-based)
                        provider_id:
                          type: integer
                        started_at:
                          type: string
                          format: date-time
                        ended_at:
                          type: string
                          format: date-time
                          nullable: true
                        status:
                          type: string
                          enum: [running, completed, failed, timeout]
                        error:
                          type: string
                          nullable: true
                        duration_seconds:
                          type: number
                          nullable: true
        '404':
          description: Job not found

  /api/jobs/{job_id}/logs/stream:
    get:
      tags: [Jobs]
      summary: Stream job execution logs (SSE)
      description: |
        Opens a Server-Sent Events stream for live job log output. Each event
        contains a single log line. The stream closes when the job reaches a
        terminal state (`completed`, `failed`, `cancelled`).

        **Client usage:**
        ```javascript
        const source = new EventSource(
          `https://api.dcp.sa/api/jobs/${jobId}/logs/stream`,
          { headers: { 'x-renter-key': apiKey } }
        );
        source.onmessage = (e) => console.log(e.data);
        ```
      security:
        - RenterApiKey: []
        - ProviderApiKey: []
        - AdminToken: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: SSE stream of log lines
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Each event: `data: <log line>\n\n`
                  Terminal event: `data: [DONE]\n\n`
        '404':
          description: Job not found

  /api/jobs/queue/status:
    get:
      tags: [Jobs]
      summary: Job queue depth by compute type
      description: |
        Returns the current number of queued and running jobs broken down by
        `job_type`. Use this to estimate wait times before submitting a job.
        No authentication required — public endpoint.
      responses:
        '200':
          description: Queue depth per job type
          content:
            application/json:
              schema:
                type: object
                properties:
                  queue:
                    type: array
                    items:
                      type: object
                      properties:
                        job_type:
                          type: string
                          example: llm_inference
                        queued:
                          type: integer
                          description: Jobs waiting for a provider
                        running:
                          type: integer
                          description: Jobs currently executing
                        avg_wait_seconds:
                          type: number
                          nullable: true
                          description: Rolling average queue wait time
                  total_queued:
                    type: integer
                  total_running:
                    type: integer
                  updated_at:
                    type: string
                    format: date-time

  /api/jobs/{job_id}/retry:
    post:
      tags: [Jobs]
      summary: Retry a failed job
      description: |
        Re-queues a `failed` job for execution. The renter's balance is checked
        against the original estimated cost — a new hold is placed. The job
        retains its original `job_id` but increments its attempt counter (visible
        in `/api/jobs/{id}/history`).

        Only jobs in `failed` status can be retried. Max 3 retry attempts.
      security:
        - RenterApiKey: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job re-queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  attempt:
                    type: integer
                    description: New attempt number
                  status:
                    type: string
                    enum: [pending, queued]
                  cost_halala:
                    type: integer
                  new_balance_halala:
                    type: integer
        '400':
          description: Job is not in failed status, or max retry attempts reached
        '402':
          description: Insufficient balance for retry
        '404':
          description: Job not found

  /api/jobs/{job_id}/pause:
    post:
      tags: [Jobs]
      summary: Pause a running job (checkpoint container)
      description: |
        Sends a pause signal to the provider daemon, which uses `docker checkpoint`
        to snapshot the container state to disk. The job transitions to `paused`.
        Resume with `POST /api/jobs/{id}/resume`.

        Only supported for jobs using checkpoint-compatible images.
      security:
        - RenterApiKey: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Pause signal sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    example: pausing
                  message:
                    type: string
        '400':
          description: Job is not in running status or image does not support checkpointing
        '404':
          description: Job not found

  /api/jobs/{job_id}/resume:
    post:
      tags: [Jobs]
      summary: Resume a paused job (restore from checkpoint)
      description: |
        Sends a resume signal to the provider daemon, which restores the container
        from its checkpoint and continues execution. The job transitions back to
        `running`.
      security:
        - RenterApiKey: []
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Resume signal sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    example: resuming
                  message:
                    type: string
        '400':
          description: Job is not in paused status
        '404':
          description: Job not found

  # ──────────────────────────────────────────────────────────────────────────────
  # vLLM Managed Inference (DCP-333)
  # Note: vLLM router is mounted at /v1 in Express (not /api/vllm).
  # Next.js rewrites /v1/* → backend /v1/* for external access.
  # ──────────────────────────────────────────────────────────────────────────────

  /v1/models:
    get:
      tags: [vLLM]
      summary: List available vLLM models
      description: |
        Returns the list of LLM models available for managed inference via
        `/v1/complete` or `/v1/chat/completions`. These models are pre-loaded on DCP inference
        nodes and do not require a separate job submission.

        No authentication required.
      responses:
        '200':
          description: Available model list
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          example: mistralai/Mistral-7B-Instruct-v0.2
                        object:
                          type: string
                          example: model
                        context_length:
                          type: integer
                          example: 32768
                        max_tokens:
                          type: integer
                          example: 4096
                        vram_required_gb:
                          type: number
                          example: 14
                        available:
                          type: boolean
                          description: True if a provider is currently serving this model

  /v1/complete:
    post:
      tags: [vLLM]
      summary: Synchronous LLM completion
      description: |
        Submits a prompt to a managed vLLM inference endpoint and returns the
        full response synchronously. Internally submits an `llm_inference` job
        and waits for completion (max 120s timeout).

        For streaming output, use `POST /v1/complete/stream`.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, prompt]
              properties:
                model:
                  type: string
                  example: mistralai/Mistral-7B-Instruct-v0.2
                  description: Model ID from `/v1/models`
                prompt:
                  type: string
                  example: Explain quantum computing in simple terms.
                max_tokens:
                  type: integer
                  default: 512
                  minimum: 1
                  maximum: 4096
                temperature:
                  type: number
                  default: 0.7
                  minimum: 0
                  maximum: 2
                top_p:
                  type: number
                  default: 1
                  minimum: 0
                  maximum: 1
                stop:
                  type: array
                  items:
                    type: string
                  description: Stop sequences
                provider_id:
                  type: integer
                  nullable: true
                  description: Pin to a specific provider. Omit to auto-select.
      responses:
        '200':
          description: Completion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Completion ID (same as job_id)
                  model:
                    type: string
                  choices:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        text:
                          type: string
                        finish_reason:
                          type: string
                          enum: [stop, length, timeout]
                  usage:
                    type: object
                    properties:
                      prompt_tokens:
                        type: integer
                      completion_tokens:
                        type: integer
                      total_tokens:
                        type: integer
                  cost_halala:
                    type: integer
                  latency_ms:
                    type: integer
        '402':
          description: Insufficient balance
        '503':
          description: No providers available for this model

  /v1/complete/stream:
    post:
      tags: [vLLM]
      summary: Streaming LLM completion (SSE)
      description: |
        Submits a prompt and streams the response token-by-token as Server-Sent
        Events. Each event carries a partial `text` delta. The final event
        carries `finish_reason` and billing metadata.

        **Client usage:**
        ```javascript
        const resp = await fetch('https://api.dcp.sa/v1/complete/stream', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-renter-key': apiKey
          },
          body: JSON.stringify({ model: 'mistralai/Mistral-7B-Instruct-v0.2', prompt: 'Hello!' })
        });
        const reader = resp.body.getReader();
        // read SSE frames...
        ```
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, prompt]
              properties:
                model:
                  type: string
                  example: mistralai/Mistral-7B-Instruct-v0.2
                prompt:
                  type: string
                max_tokens:
                  type: integer
                  default: 512
                temperature:
                  type: number
                  default: 0.7
                stop:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: SSE token stream
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Token delta events: `data: {"text": "...", "index": 0}\n\n`
                  Final event: `data: {"finish_reason": "stop", "usage": {...}, "cost_halala": N}\n\n`
                  Terminator: `data: [DONE]\n\n`
        '402':
          description: Insufficient balance
        '503':
          description: No providers available

  /v1/chat/completions:
    post:
      tags: [vLLM]
      summary: OpenAI-compatible chat completions endpoint
      description: |
        OpenAI-compatible chat completions endpoint. Supports both streaming and
        non-streaming responses via the `stream` field in the request body.

        When `stream: true`, returns Server-Sent Events (SSE) with `chat.completion.chunk`
        objects. When `stream: false` or omitted, returns a standard
        `chat.completion` object synchronously.

        Uses the same authentication, rate limiting, and billing as `/v1/complete`.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, messages]
              properties:
                model:
                  type: string
                  example: mistralai/Mistral-7B-Instruct-v0.2
                  description: Model ID from `/v1/models`
                messages:
                  type: array
                  description: Array of message objects with `role` and `content`
                  items:
                    type: object
                    properties:
                      role:
                        type: string
                        enum: [system, user, assistant]
                        example: user
                      content:
                        type: string
                        example: Explain quantum computing in simple terms.
                stream:
                  type: boolean
                  default: false
                  description: If true, returns SSE stream of token deltas
                max_tokens:
                  type: integer
                  default: 512
                  minimum: 1
                  maximum: 4096
                temperature:
                  type: number
                  default: 0.7
                  minimum: 0
                  maximum: 2
                top_p:
                  type: number
                  default: 1
                  minimum: 0
                  maximum: 1
                stop:
                  type: array
                  items:
                    type: string
                  description: Stop sequences
                provider_id:
                  type: integer
                  nullable: true
                  description: Pin to a specific provider. Omit to auto-select.
      responses:
        '200':
          description: Chat completion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Completion ID
                  object:
                    type: string
                    example: chat.completion
                  created:
                    type: integer
                    description: Unix timestamp
                  model:
                    type: string
                  choices:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        message:
                          type: object
                          properties:
                            role:
                              type: string
                            content:
                              type: string
                        finish_reason:
                          type: string
                          enum: [stop, length, timeout]
                  usage:
                    type: object
                    properties:
                      prompt_tokens:
                        type: integer
                      completion_tokens:
                        type: integer
                      total_tokens:
                        type: integer
                  cost_halala:
                    type: integer
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: No providers available for this model
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ──────────────────────────────────────────────────────────────────────────────
  # Provider — GPU Profile, Withdrawals, Public Marketplace (DCP-322/332)
  # ──────────────────────────────────────────────────────────────────────────────

  /api/providers/me/gpu-profile:
    patch:
      tags: [Providers]
      summary: Update provider GPU capabilities
      description: |
        Allows a provider to update their GPU profile metadata. Useful after
        hardware upgrades or configuration changes. The daemon also updates
        this automatically on each heartbeat, but this endpoint lets providers
        correct stale data manually.
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                gpu_model:
                  type: string
                  example: NVIDIA RTX 4090
                vram_gb:
                  type: number
                  example: 24
                gpu_count:
                  type: integer
                  example: 2
                compute_capability:
                  type: string
                  example: "8.9"
                cuda_version:
                  type: string
                  example: "12.2"
                driver_version:
                  type: string
                  example: "535.54.03"
                location:
                  type: string
                  example: SA
      responses:
        '200':
          description: GPU profile updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  provider_id:
                    type: integer
                  updated_fields:
                    type: array
                    items:
                      type: string
        '401':
          description: Invalid API key

  /api/providers/me/withdraw:
    post:
      tags: [Providers]
      summary: Request an earnings payout (provider self-service)
      description: |
        Alias for `POST /api/providers/withdraw`. Submits a withdrawal request
        for a portion of the provider's claimable earnings. Payout timing and
        thresholds are configured in provider payout settings.
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_sar]
              properties:
                amount_sar:
                  type: number
                  example: 100.00
                payout_method:
                  type: string
                  enum: [bank_transfer, stc_pay, crypto]
                  default: bank_transfer
                payout_details:
                  type: object
                  description: IBAN, phone number, or wallet address depending on method
                  example:
                    iban: SA4420000001234567891234
                    account_name: Ahmed Al-Rashidi
      responses:
        '201':
          description: Withdrawal request created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  withdrawal_id:
                    type: string
                  amount_sar:
                    type: number
                  status:
                    type: string
                    example: pending
                  message:
                    type: string
        '402':
          description: Insufficient available earnings

  /api/providers/me/withdrawals:
    get:
      tags: [Providers]
      summary: Provider withdrawal history (self-service)
      description: |
        Returns the authenticated provider's withdrawal request history.
        Alias for `GET /api/providers/withdrawal-history`.
      security:
        - ProviderApiKey: []
      parameters:
        - in: query
          name: key
          schema:
            type: string
          description: Provider API key (alternative to x-provider-key header)
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
            maximum: 200
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Withdrawal history
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider_id:
                    type: integer
                  withdrawals:
                    type: array
                    items:
                      type: object
                      properties:
                        withdrawal_id:
                          type: string
                        amount_sar:
                          type: number
                        payout_method:
                          type: string
                        status:
                          type: string
                          enum: [pending, approved, rejected, completed]
                        requested_at:
                          type: string
                          format: date-time
                        processed_at:
                          type: string
                          format: date-time
                          nullable: true

  /api/providers/public:
    get:
      tags: [Providers]
      summary: Public GPU marketplace — live providers
      description: |
        Returns online, unpaused GPU providers with specs and indicative pricing.
        No authentication required. Used by the public-facing marketplace page
        at `dcp.sa/marketplace`.

        Equivalent to `GET /api/providers/marketplace` — prefer this URL in new code.
      responses:
        '200':
          description: Online providers for the public marketplace
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        gpu_model:
                          type: string
                          example: NVIDIA RTX 4090
                        vram_gb:
                          type: number
                          example: 24
                        gpu_count:
                          type: integer
                          example: 1
                        location:
                          type: string
                          example: SA
                        price_per_min_halala:
                          type: integer
                          example: 15
                        uptime_pct:
                          type: number
                          example: 98.5
                        jobs_completed:
                          type: integer
                        is_live:
                          type: boolean
                          description: Heartbeat received within last 120 seconds
                        cached_models:
                          type: array
                          items:
                            type: string
                  total:
                    type: integer

  # ──────────────────────────────────────────────────────────────────────────────
  # PDPL Compliance — Data Export & Account Deletion (DCP-328)
  # ──────────────────────────────────────────────────────────────────────────────

  /api/renters/me/export:
    get:
      tags: [Renters]
      summary: Export all personal data (PDPL data portability)
      description: |
        Returns a JSON document containing all personal data held for the
        authenticated renter — profile, payment history, job history, and
        account metadata. Required under Saudi Arabia's Personal Data
        Protection Law (PDPL).

        The response is a downloadable JSON file.
      security:
        - RenterApiKey: []
      responses:
        '200':
          description: Personal data export
          content:
            application/json:
              schema:
                type: object
                properties:
                  exported_at:
                    type: string
                    format: date-time
                  renter:
                    $ref: '#/components/schemas/Renter'
                  jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/Job'
                  payments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Payment'
                  data_retention_policy:
                    type: string
                    example: Data is retained for 7 years per Saudi financial regulations.

  /api/renters/me:
    delete:
      tags: [Renters]
      summary: Delete renter account (PDPL right to erasure)
      description: |
        Permanently deletes the authenticated renter's account and associated
        personal data. Required under Saudi Arabia's Personal Data Protection
        Law (PDPL).

        **Preconditions:**
        - Account balance must be zero (withdraw or transfer first)
        - No jobs may be in `running` or `queued` status

        This action is **irreversible**. The API key is immediately invalidated.
      security:
        - RenterApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm]
              properties:
                confirm:
                  type: string
                  enum: ["DELETE_MY_ACCOUNT"]
                  description: Must be the literal string `DELETE_MY_ACCOUNT`
                reason:
                  type: string
                  description: Optional deletion reason (retained anonymously for regulatory reporting)
      responses:
        '200':
          description: Account deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Account permanently deleted. All personal data erased.
                  deleted_at:
                    type: string
                    format: date-time
        '400':
          description: Confirmation string missing or incorrect
        '409':
          description: Account has active jobs or non-zero balance. Resolve before deletion.

  /api/providers/me:
    delete:
      tags: [Providers]
      summary: Delete provider account (PDPL right to erasure)
      description: |
        Permanently deletes the authenticated provider's account and personal
        data. Required under Saudi Arabia's Personal Data Protection Law (PDPL).

        **Preconditions:**
        - No jobs may be in `running` status (daemon must be stopped)
        - No pending withdrawal requests

        Earnings history is anonymised (provider ID replaced with `[deleted]`)
        rather than erased to preserve billing audit trail required by law.
        This action is **irreversible**.
      security:
        - ProviderApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm]
              properties:
                confirm:
                  type: string
                  enum: ["DELETE_MY_ACCOUNT"]
                  description: Must be the literal string `DELETE_MY_ACCOUNT`
                reason:
                  type: string
      responses:
        '200':
          description: Account deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Provider account permanently deleted. Earnings history anonymised.
                  deleted_at:
                    type: string
                    format: date-time
        '400':
          description: Confirmation string missing or incorrect
        '409':
          description: Active jobs running or pending withdrawals exist
