---
openapi: 3.0.3
info:
  title: Vibe Earning API
  description: |
    Distributed LLM grid computing platform. Clients submit AI inference jobs via this API;
    volunteer workers running the `grid-worker` agent long-poll for jobs, execute them against
    their local Ollama instance, and submit results back.

    ## Authentication
    All client API requests must include your API key in the `Authorization` header:
    ```
    Authorization: Bearer vk_live_your_api_key_here
    ```

    Worker endpoints (`/worker/*`) use a short-lived JWT obtained from `POST /worker/register`.
    The JWT is valid for 24 hours; re-register to get a new one.

    ## Quota
    Free tier: 100 requests/day, 50,000 tokens/day. Exceeded quota returns HTTP 429.

    ## Webhooks
    When a job includes a `webhook_url`, the platform POSTs the result payload to that URL
    with an `X-Vibe-Signature: sha256=<hex>` header. The signature is HMAC-SHA256 of the
    JSON body using your API key as the secret.
  version: 1.0.0
  contact:
    email: support@vibeearning.com
  license:
    name: Proprietary
servers:
- url: https://vibeearning.com/api/v1
  description: Production
- url: http://localhost:3000/api/v1
  description: Local development
tags:
- name: Jobs
  description: Submit and manage inference jobs
- name: Models
  description: Browse available grid models
- name: Usage
  description: Query token and request usage
- name: Blobs
  description: Upload binary attachments (images, documents) for jobs
- name: Worker — Auth
  description: Worker agent registration (uses client API key, returns worker JWT)
- name: Worker — Jobs
  description: Job polling and result submission (worker JWT required)
- name: Worker — Credits
  description: Credit balance and payout requests (worker JWT required)
components:
  parameters:
    JobId:
      name: id
      in: path
      required: true
      schema:
        type: integer
      description: Job ID
  responses:
    Unauthorized:
      description: Missing or invalid API key / worker JWT
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: unauthorized
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: not_found
paths:
  "/jobs":
    get:
      tags:
      - Jobs
      summary: List jobs
      description: Returns your jobs, newest first. Supports filtering by status,
        model, tag, and date range.
      security:
      - ApiKeyAuth: []
      parameters:
      - name: status
        in: query
        schema:
          type: string
          enum:
          - pending
          - claimed
          - running
          - completed
          - failed
          - cancelled
      - name: model
        in: query
        schema:
          type: string
        example: llama3:8b
      - name: tag
        in: query
        schema:
          type: string
      - name: from
        in: query
        schema:
          type: string
          format: date-time
      - name: to
        in: query
        schema:
          type: string
          format: date-time
      - name: offset
        in: query
        schema:
          type: integer
          default: 0
      responses:
        '200':
          description: Array of jobs
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/Job"
        '401':
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
      - Jobs
      summary: Submit a job
      description: |
        Enqueues a new inference job. The job enters `pending` state and will be
        dispatched to the first eligible worker. Use `GET /jobs/{id}` to poll for
        completion, or provide a `webhook_url` to receive the result via HTTP POST.
      security:
      - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/JobCreateRequest"
            examples:
              basic:
                summary: Simple prompt
                value:
                  model: llama3:8b
                  model_match: family
                  prompt: Explain quantum entanglement in one paragraph.
              with_webhook:
                summary: With webhook and tag
                value:
                  model: mistral:7b
                  model_match: exact
                  prompt: Summarize the attached document.
                  tag: doc-processor
                  webhook_url: https://yourapp.com/hooks/llm
                  blob_ids:
                  - 17
              any_model:
                summary: Any available model
                value:
                  model_match: any
                  prompt: Write a haiku about distributed computing.
      responses:
        '201':
          description: Job created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Job"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: array
                    items:
                      type: string
        '429':
          description: Quota exceeded
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/QuotaExceeded"
  "/jobs/cancel_all":
    delete:
      tags:
      - Jobs
      summary: Cancel all pending jobs
      security:
      - ApiKeyAuth: []
      responses:
        '200':
          description: Number of cancelled jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  cancelled:
                    type: integer
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/jobs/{id}":
    get:
      tags:
      - Jobs
      summary: Get job (poll for result)
      security:
      - ApiKeyAuth: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      responses:
        '200':
          description: Job detail
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Job"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
      - Jobs
      summary: Cancel a pending job
      description: Only jobs in `pending` state can be cancelled.
      security:
      - ApiKeyAuth: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      responses:
        '200':
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: cancelled
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          description: Job cannot be cancelled (not in pending state)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/jobs/{id}/priority":
    patch:
      tags:
      - Jobs
      summary: Boost job priority
      description: Increases the job's priority score by 50. Typically triggers a
        Stripe charge.
      security:
      - ApiKeyAuth: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      responses:
        '200':
          description: New priority value
          content:
            application/json:
              schema:
                type: object
                properties:
                  priority:
                    type: integer
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          description: Job not in pending state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/jobs/{id}/retry":
    post:
      tags:
      - Jobs
      summary: Retry a failed job
      description: Creates a new job with identical parameters. The original job remains
        in `failed` state.
      security:
      - ApiKeyAuth: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      responses:
        '201':
          description: New job created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Job"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          description: Job is not in failed state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/models":
    get:
      tags:
      - Models
      summary: List available grid models
      description: Returns all active models with at least one online worker. Sorted
        by worker count descending.
      security:
      - ApiKeyAuth: []
      responses:
        '200':
          description: Active models
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/GridModel"
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/usage":
    get:
      tags:
      - Usage
      summary: Get usage statistics
      security:
      - ApiKeyAuth: []
      parameters:
      - name: period
        in: query
        schema:
          type: string
          enum:
          - daily
          - weekly
          - monthly
          default: daily
      - name: tag
        in: query
        schema:
          type: string
      responses:
        '200':
          description: Usage breakdown
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/UsageResponse"
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/blobs":
    post:
      tags:
      - Blobs
      summary: Get presigned upload URL
      description: |
        Returns a presigned S3 URL. Upload your file directly to that URL with a `PUT` request,
        then call `POST /blobs/{id}/confirm` to mark it ready. Pass the blob ID in `blob_ids`
        when submitting a job.
      security:
      - ApiKeyAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                blob_type:
                  type: string
                  enum:
                  - image
                  - document
                  default: image
      responses:
        '201':
          description: Presigned URL created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/BlobCreateResponse"
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/blobs/{id}/confirm":
    post:
      tags:
      - Blobs
      summary: Confirm blob upload complete
      security:
      - ApiKeyAuth: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: Confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: confirmed
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
  "/worker/register":
    post:
      tags:
      - Worker — Auth
      summary: Register worker and get JWT
      description: |
        Authenticate with your **client API key** to register this worker instance. Returns a
        worker JWT valid for 24 hours, used as the Bearer token for all subsequent `/worker/*` calls.

        Re-register any time to refresh the token or update your model list. If a worker with
        the same `name` already exists under your account it is updated in place.
      security:
      - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/WorkerRegisterRequest"
            example:
              name: my-home-server
              agent_version: 1.0.0
              platform: darwin/arm64
              catch_all: false
              models:
              - name: llama3:8b
                serves_family: true
              - name: mistral:7b
                serves_family: false
      responses:
        '201':
          description: Worker registered
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WorkerRegisterResponse"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          description: Account disabled
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/worker/heartbeat":
    patch:
      tags:
      - Worker — Auth
      summary: Update last-seen timestamp
      description: |
        Workers that haven't sent a heartbeat in **90 seconds** are automatically marked offline
        and removed from the job routing pool. The polling loop in `grid-worker` sends this
        implicitly with every `GET /worker/jobs/next` call, so explicit heartbeats are only
        needed if the worker is idle but wants to stay online.
      security:
      - ApiKeyAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/worker/models":
    patch:
      tags:
      - Worker — Auth
      summary: Update advertised model list
      description: Replaces the worker's current model list with the supplied array
        and syncs the grid model registry.
      security:
      - WorkerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                models:
                  type: array
                  items:
                    "$ref": "#/components/schemas/WorkerModelEntry"
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: updated
                  model_count:
                    type: integer
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/worker/jobs/next":
    get:
      tags:
      - Worker — Jobs
      summary: Long-poll for next job
      description: |
        Blocks for up to **30 seconds** waiting for a matching job. Returns the job immediately
        if one is available, or `204 No Content` on timeout. Retry immediately on 204.

        The request body declares the worker's current capabilities. A job matches if:
        - `model_match = exact` and the job's model is in `models[]`
        - `model_match = family` and the job's model family is in `families[]` (or exact match)
        - `model_match = any` and `catch_all = true`

        Jobs are claimed atomically using `SELECT ... FOR UPDATE SKIP LOCKED` — a job can
        never be dispatched to two workers simultaneously.
      security:
      - WorkerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                models:
                  type: array
                  items:
                    type: string
                  example:
                  - llama3:8b
                  - mistral:7b
                families:
                  type: array
                  items:
                    type: string
                  example:
                  - llama3
                catch_all:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Job assigned
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/JobAssignment"
        '204':
          description: No job available within the poll window — retry immediately
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/worker/jobs/{id}/start":
    patch:
      tags:
      - Worker — Jobs
      summary: Mark job as running
      description: Transitions job from `claimed` → `running`. Call this just before
        invoking Ollama.
      security:
      - WorkerJWT: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      responses:
        '200':
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: running
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          description: Job not in claimed state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/worker/jobs/{id}/result":
    post:
      tags:
      - Worker — Jobs
      summary: Submit job result
      description: |
        Transitions job to `completed`, creates a `WorkerCreditEntry`, records usage in the
        `UsageLedger`, and enqueues webhook delivery if the job has a `webhook_url`.

        Credits earned = `output_tokens / 1000 × 1.0` (rounded to 4 decimal places).
      security:
      - WorkerJWT: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/JobResultRequest"
      responses:
        '200':
          description: Result accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: accepted
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          description: Job not in running state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/worker/jobs/{id}/fail":
    post:
      tags:
      - Worker — Jobs
      summary: Report job failure
      description: Transitions job to `failed` and stores the error message for the
        client.
      security:
      - WorkerJWT: []
      parameters:
      - "$ref": "#/components/parameters/JobId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - error
              properties:
                error:
                  type: string
                  example: Ollama returned empty response after 300s
      responses:
        '200':
          description: Failure recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: failed
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
  "/worker/credits":
    get:
      tags:
      - Worker — Credits
      summary: Credit balance and history
      security:
      - WorkerJWT: []
      responses:
        '200':
          description: Credit summary
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CreditBalance"
        '401':
          "$ref": "#/components/responses/Unauthorized"
  "/worker/payout_request":
    post:
      tags:
      - Worker — Credits
      summary: Request a payout
      description: |
        Creates a `PayoutBatch` for the worker's current `payout_balance`, marks all unpaid
        `WorkerCreditEntries` as paid, and resets `payout_balance` to zero. The platform
        processes payouts on a weekly/monthly cycle.
      security:
      - WorkerJWT: []
      responses:
        '201':
          description: Payout batch created
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id:
                    type: integer
                  amount_credits:
                    type: number
                    format: double
                  amount_usd:
                    type: number
                    format: double
                  status:
                    type: string
                    example: pending
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '422':
          description: Insufficient balance
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
