openapi: 3.1.0
info:
  # Do not change the title, if the title changes, the import paths will be broken
  title: Api
  version: 0.1.0
  description: API specification
servers:
  - url: /api
    description: Base API path
tags:
  - name: health
    description: Health operations
  - name: jobs
    description: Payment-gated agent job operations (Mode 1 -- per-job)
  - name: sessions
    description: Pre-funded session balance mode (Mode 2 -- pay once, run many)
  - name: demo
    description: Free demo endpoint (rate-limited)
paths:
  /healthz:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check
      description: Returns server health status
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
  /jobs:
    post:
      operationId: createJob
      tags: [jobs]
      summary: Create a new agent job
      description: Accepts a request, creates a job row, and issues an eval fee Lightning invoice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateJobRequest"
      responses:
        "201":
          description: Job created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateJobResponse"
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /jobs/{id}:
    get:
      operationId: getJob
      tags: [jobs]
      summary: Get job status
      description: Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobStatusResponse"
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /jobs/{id}/refund:
    post:
      operationId: claimRefund
      tags: [jobs]
      summary: Claim a refund for overpayment
      description: |
        After a job completes, if the actual cost (tokens used + infra + margin) was
        less than the work invoice amount, the difference is owed back to the user.
        Submit a BOLT11 invoice for exactly `refundAmountSats` to receive the payment.
        Idempotent: returns 409 if already paid or if no refund is owed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClaimRefundRequest"
      responses:
        "200":
          description: Refund sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClaimRefundResponse"
        "400":
          description: Missing invoice, wrong amount, or invalid BOLT11
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Job not complete, refund already paid, or no refund owed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Server error (e.g. Lightning payment failure)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /sessions:
    post:
      operationId: createSession
      tags: [sessions]
      summary: Create a pre-funded session
      description: |
        Opens a new session. Pay the returned Lightning invoice to activate it.
        Once active, use the `macaroon` from GET /sessions/:id to authenticate requests.
        Deposits: 100–10,000 sats. Sessions expire after 24 h of inactivity.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
      responses:
        "201":
          description: Session created -- awaiting deposit payment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateSessionResponse"
        "400":
          description: Invalid amount
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /sessions/{id}:
    get:
      operationId: getSession
      tags: [sessions]
      summary: Get session status
      description: Returns current state, balance, and pending invoice info. Auto-advances on payment.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionStatusResponse"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /sessions/{id}/request:
    post:
      operationId: submitSessionRequest
      tags: [sessions]
      summary: Submit a request against a session balance
      description: |
        Runs eval + work and debits the actual compute cost from the session balance.
        Rejected requests still incur a small eval fee. Requires `Authorization: Bearer <macaroon>`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      security:
        - sessionMacaroon: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionRequestBody"
      responses:
        "200":
          description: Request completed (or rejected)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionRequestResponse"
        "401":
          description: Missing or invalid macaroon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: Insufficient balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Session not active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Session expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /sessions/{id}/topup:
    post:
      operationId: topupSession
      tags: [sessions]
      summary: Add sats to a session
      description: |
        Creates a new Lightning invoice to top up the session balance.
        Only one pending topup at a time. Paying it resumes a paused session.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      security:
        - sessionMacaroon: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
      responses:
        "200":
          description: Topup invoice created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TopupSessionResponse"
        "400":
          description: Invalid amount
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid macaroon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Session not active/paused, or topup already pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /demo:
    get:
      operationId: runDemo
      tags: [demo]
      summary: Free demo (rate-limited)
      description: Runs the agent without payment. Limited to 5 requests per IP per hour.
      parameters:
        - name: request
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Demo result
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Maximum requests allowed per window (always 5)
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Requests remaining in the current window
            X-RateLimit-Reset:
              schema:
                type: integer
              description: Unix epoch seconds when the rate limit window resets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DemoResponse"
        "400":
          description: Missing or invalid request param
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Reset:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
                description: Always 0 when rate limited
            X-RateLimit-Reset:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Server error
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Reset:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
components:
  schemas:
    HealthStatus:
      type: object
      properties:
        status:
          type: string
      required:
        - status
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: string
    InvoiceInfo:
      type: object
      required:
        - paymentRequest
        - amountSats
      properties:
        paymentRequest:
          type: string
        amountSats:
          type: integer
    CreateJobRequest:
      type: object
      required:
        - request
      properties:
        request:
          type: string
          minLength: 1
    CreateJobResponse:
      type: object
      required:
        - jobId
        - createdAt
        - evalInvoice
      properties:
        jobId:
          type: string
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of job creation
        evalInvoice:
          $ref: "#/components/schemas/InvoiceInfo"
    JobState:
      type: string
      enum:
        - awaiting_eval_payment
        - evaluating
        - rejected
        - awaiting_work_payment
        - executing
        - complete
        - failed
    PricingBreakdown:
      type: object
      description: Cost breakdown shown with the work invoice (estimations at invoice-creation time)
      properties:
        estimatedCostUsd:
          type: number
          description: Total estimated cost in USD (token cost + DO infra + margin)
        marginPct:
          type: number
          description: Originator margin percentage applied
        btcPriceUsd:
          type: number
          description: BTC/USD spot price used to convert the invoice to sats
    CostLedger:
      type: object
      description: Honest post-work accounting stored after the job completes
      properties:
        actualInputTokens:
          type: integer
        actualOutputTokens:
          type: integer
        totalTokens:
          type: integer
          description: Sum of actualInputTokens + actualOutputTokens
        actualCostUsd:
          type: number
          description: Raw Anthropic token cost (no infra, no margin)
        actualChargeUsd:
          type: number
          description: What we honestly charged in USD (actual token cost + DO infra + margin)
        estimatedCostUsd:
          type: number
          description: Original estimate used to create the work invoice
        actualAmountSats:
          type: integer
          description: Honest sats charge (actual cost converted at the locked BTC price)
        workAmountSats:
          type: integer
          description: Amount the user originally paid in sats
        refundAmountSats:
          type: integer
          description: Sats owed back to the user (workAmountSats - actualAmountSats, >= 0)
        refundState:
          type: string
          enum: [not_applicable, pending, paid]
          description: Lifecycle of the refund for this job
        marginPct:
          type: number
        btcPriceUsd:
          type: number
          description: BTC/USD price locked at invoice creation time
    JobStatusResponse:
      type: object
      required:
        - jobId
        - state
        - createdAt
      properties:
        jobId:
          type: string
        state:
          $ref: "#/components/schemas/JobState"
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of job creation (always present)
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 timestamp of job completion; null when not yet complete
        evalInvoice:
          $ref: "#/components/schemas/InvoiceInfo"
        workInvoice:
          $ref: "#/components/schemas/InvoiceInfo"
        pricingBreakdown:
          $ref: "#/components/schemas/PricingBreakdown"
        reason:
          type: string
        result:
          type: string
        costLedger:
          $ref: "#/components/schemas/CostLedger"
        errorMessage:
          type: string
    SessionState:
      type: string
      enum: [awaiting_payment, active, paused, expired]
    SessionInvoiceInfo:
      type: object
      properties:
        paymentRequest:
          type: string
        amountSats:
          type: integer
        paymentHash:
          type: string
          description: Only present in stub/dev mode
    CreateSessionRequest:
      type: object
      required: [amount_sats]
      properties:
        amount_sats:
          type: integer
          description: Deposit amount (100–10,000 sats)
          minimum: 100
          maximum: 10000
    CreateSessionResponse:
      type: object
      required: [sessionId, state, invoice]
      properties:
        sessionId:
          type: string
        state:
          $ref: "#/components/schemas/SessionState"
        invoice:
          $ref: "#/components/schemas/SessionInvoiceInfo"
    SessionStatusResponse:
      type: object
      required: [sessionId, state, balanceSats]
      properties:
        sessionId:
          type: string
        state:
          $ref: "#/components/schemas/SessionState"
        balanceSats:
          type: integer
        minimumBalanceSats:
          type: integer
        macaroon:
          type: string
          description: Bearer token for authenticating requests; present when active or paused
        expiresAt:
          type: string
          format: date-time
        invoice:
          $ref: "#/components/schemas/SessionInvoiceInfo"
          description: Present when state is awaiting_payment
        pendingTopup:
          $ref: "#/components/schemas/SessionInvoiceInfo"
          description: Present when a topup invoice is outstanding
    SessionRequestBody:
      type: object
      required: [request]
      properties:
        request:
          type: string
          minLength: 1
    SessionCostBreakdown:
      type: object
      properties:
        evalSats:
          type: integer
        workSats:
          type: integer
        totalSats:
          type: integer
        btcPriceUsd:
          type: number
    SessionRequestResponse:
      type: object
      required: [requestId, state, debitedSats, balanceRemaining]
      properties:
        requestId:
          type: string
        state:
          type: string
          enum: [complete, rejected, failed]
        result:
          type: string
        reason:
          type: string
        errorMessage:
          type: string
        debitedSats:
          type: integer
        balanceRemaining:
          type: integer
        cost:
          $ref: "#/components/schemas/SessionCostBreakdown"
    TopupSessionResponse:
      type: object
      required: [sessionId, topup]
      properties:
        sessionId:
          type: string
        topup:
          $ref: "#/components/schemas/SessionInvoiceInfo"
    ClaimRefundRequest:
      type: object
      required:
        - invoice
      properties:
        invoice:
          type: string
          description: BOLT11 invoice for exactly refundAmountSats
    ClaimRefundResponse:
      type: object
      required:
        - ok
        - refundAmountSats
        - paymentHash
        - message
      properties:
        ok:
          type: boolean
        refundAmountSats:
          type: integer
        paymentHash:
          type: string
        message:
          type: string
    DemoResponse:
      type: object
      required:
        - result
      properties:
        result:
          type: string
  securitySchemes:
    sessionMacaroon:
      type: http
      scheme: bearer
      description: Session macaroon issued when a session activates. Pass as `Authorization: Bearer <macaroon>`.
