openapi: 3.1.0
info:
  title: SiloVPS
  version: 0.1.0
  description: |
    Self-served VPS compute billed per minute, paid with USDC on Tempo.

    All paid endpoints use the Machine Payments Protocol (MPP). Send an
    unauthenticated request to receive a 402 challenge, sign it with your
    Tempo wallet, and retry. Your wallet address is your identity.
  contact:
    url: https://silovps.com
  license:
    name: MIT

servers:
  - url: https://silovps.com
    description: Production

tags:
  - name: Catalog
    description: Browse available sizes, regions, and images. Free.
  - name: Servers
    description: Provision, manage, and destroy servers.
  - name: Keys
    description: Register and manage SSH keys.
  - name: Snapshots
    description: List snapshots from terminated servers.

paths:
  # --------------------------------------------------------------------------
  # Catalog
  # --------------------------------------------------------------------------
  /v1/catalog/sizes:
    get:
      operationId: listSizes
      tags: [Catalog]
      summary: List available server sizes with pricing
      description: Returns all available sizes, their specs, hourly and per-minute rates, create fees, and billing info.
      responses:
        "200":
          description: Available sizes
          content:
            application/json:
              schema:
                type: object
                properties:
                  sizes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Size"
                  billing:
                    $ref: "#/components/schemas/BillingInfo"

  /v1/catalog/regions:
    get:
      operationId: listRegions
      tags: [Catalog]
      summary: List available regions
      responses:
        "200":
          description: Available regions
          content:
            application/json:
              schema:
                type: object
                properties:
                  regions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Region"

  /v1/catalog/images:
    get:
      operationId: listImages
      tags: [Catalog]
      summary: List available OS images
      responses:
        "200":
          description: Available images
          content:
            application/json:
              schema:
                type: object
                properties:
                  images:
                    type: array
                    items:
                      $ref: "#/components/schemas/Image"

  # --------------------------------------------------------------------------
  # Servers
  # --------------------------------------------------------------------------
  /v1/servers:
    get:
      operationId: listServers
      tags: [Servers]
      summary: List your active servers
      description: |
        Free. Requires wallet identity via MPP proof credential (zero-amount 402 challenge).
      responses:
        "200":
          description: Servers owned by this wallet
          content:
            application/json:
              schema:
                type: object
                properties:
                  servers:
                    type: array
                    items:
                      $ref: "#/components/schemas/ServerSummary"
        "402":
          $ref: "#/components/responses/PaymentRequired"

    post:
      operationId: createServer
      tags: [Servers]
      summary: Create a new server
      description: |
        Charges create fee + (hourly rate x minutes / 60). Returns 402 on
        first request. Sign and retry to provision.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateServerRequest"
      responses:
        "201":
          description: Server provisioning
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateServerResponse"
        "400":
          description: Validation error
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "502":
          description: Provider error (refund attempted)

  /v1/servers/{id}:
    get:
      operationId: getServer
      tags: [Servers]
      summary: Get server details
      description: |
        Free. Requires wallet identity. Returns IP, root password,
        remaining time, and status.
      parameters:
        - $ref: "#/components/parameters/ServerId"
      responses:
        "200":
          description: Server details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServerDetail"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Not your server
        "404":
          description: Server not found

    delete:
      operationId: deleteServer
      tags: [Servers]
      summary: Destroy a server
      description: |
        Free. Requires wallet identity. Snapshots the server before
        destroying it. Snapshot is retained for 72 hours.
      parameters:
        - $ref: "#/components/parameters/ServerId"
      responses:
        "200":
          description: Server destroyed, snapshot saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                  snapshot:
                    $ref: "#/components/schemas/SnapshotInfo"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Not your server
        "404":
          description: Server not found

  /v1/servers/{id}/extend:
    post:
      operationId: extendServer
      tags: [Servers]
      summary: Add prepaid time to a server
      description: |
        Charges hourly rate x minutes / 60, with a $0.01 minimum.
      parameters:
        - $ref: "#/components/parameters/ServerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [minutes]
              properties:
                minutes:
                  type: integer
                  minimum: 1
                  description: Minutes to add
      responses:
        "200":
          description: Time extended
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  paid_until:
                    type: string
                    format: date-time
                  minutes_added:
                    type: integer
                  charged:
                    type: string
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Not your server (refund attempted)

  /v1/servers/{id}/reboot:
    post:
      operationId: rebootServer
      tags: [Servers]
      summary: Reboot a server
      description: "Charge: $0.005"
      parameters:
        - $ref: "#/components/parameters/ServerId"
      responses:
        "200":
          description: Rebooting
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
                    example: rebooting
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "502":
          description: Reboot failed (refund attempted)

  /v1/servers/{id}/power:
    post:
      operationId: powerServer
      tags: [Servers]
      summary: Power on or off
      description: "Charge: $0.005"
      parameters:
        - $ref: "#/components/parameters/ServerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [on, off]
      responses:
        "200":
          description: Power action initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
                    enum: [powering_on, powering_off]
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "502":
          description: Power action failed (refund attempted)

  # --------------------------------------------------------------------------
  # Snapshots
  # --------------------------------------------------------------------------
  /v1/servers/snapshots/list:
    get:
      operationId: listSnapshots
      tags: [Snapshots]
      summary: List your snapshots
      description: |
        Free. Requires wallet identity. Snapshots expire 72 hours after creation.
      responses:
        "200":
          description: Available snapshots
          content:
            application/json:
              schema:
                type: object
                properties:
                  snapshots:
                    type: array
                    items:
                      $ref: "#/components/schemas/Snapshot"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  # --------------------------------------------------------------------------
  # Keys
  # --------------------------------------------------------------------------
  /v1/keys:
    get:
      operationId: listKeys
      tags: [Keys]
      summary: List your SSH keys
      description: Free. Requires wallet identity.
      responses:
        "200":
          description: Registered keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/KeySummary"
        "402":
          $ref: "#/components/responses/PaymentRequired"

    post:
      operationId: createKey
      tags: [Keys]
      summary: Register an SSH key
      description: "Charge: $0.001"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, public_key]
              properties:
                name:
                  type: string
                  description: Display name for the key
                public_key:
                  type: string
                  description: "Full public key (e.g. ssh-ed25519 AAAA...)"
      responses:
        "200":
          description: Key registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeySummary"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "502":
          description: Registration failed (refund attempted)

  /v1/keys/{id}:
    delete:
      operationId: deleteKey
      tags: [Keys]
      summary: Remove an SSH key
      description: Free. Requires wallet identity.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Key ID (key_...)
      responses:
        "200":
          description: Key removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Not your key

# ============================================================================
# Components
# ============================================================================
components:
  parameters:
    ServerId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Server ID (srv_...)

  responses:
    PaymentRequired:
      description: |
        MPP payment challenge. Sign with your Tempo wallet and retry.
        Free endpoints issue a zero-amount proof challenge (no gas, no cost).
        Paid endpoints include the charge amount.
      headers:
        WWW-Authenticate:
          description: MPP payment challenge header
          schema:
            type: string

  schemas:
    Size:
      type: object
      properties:
        name:
          type: string
          enum: [small, medium, large]
        vcpus:
          type: integer
        memory_mb:
          type: integer
        disk_gb:
          type: integer
        hourly_rate:
          type: number
          description: USD per hour
        per_minute_rate:
          type: number
          description: USD per minute
        create_fee:
          type: string
          description: One-time fee to create a server of this size

    BillingInfo:
      type: object
      properties:
        unit:
          type: string
          example: minute
        min_minutes:
          type: integer
          example: 1
        min_charge:
          type: string
          example: "0.01"
        note:
          type: string

    Region:
      type: object
      properties:
        slug:
          type: string
          enum: [nyc1, sfo3, ams3]
        name:
          type: string
        city:
          type: string

    Image:
      type: object
      properties:
        slug:
          type: string
        name:
          type: string
        distribution:
          type: string

    CreateServerRequest:
      type: object
      required: [size, region]
      properties:
        size:
          type: string
          description: "Size name or slug (small, medium, large)"
        region:
          type: string
          description: "Region slug (nyc1, sfo3, ams3)"
        image:
          type: string
          default: ubuntu-24-04-x64
          description: OS image slug
        minutes:
          type: integer
          minimum: 1
          default: 1
          description: Prepaid minutes
        ssh_key_ids:
          type: array
          items:
            type: string
          description: Key IDs to install on the server
        snapshot_id:
          type: string
          description: Restore from a snapshot instead of a fresh image
        name:
          type: string
          description: Optional hostname

    CreateServerResponse:
      type: object
      properties:
        id:
          type: string
          example: srv_f06a96e9832d
        status:
          type: string
          example: provisioning
        size:
          type: string
        region:
          type: string
        hourly_rate:
          type: number
        per_minute_rate:
          type: number
        paid_until:
          type: string
          format: date-time
        minutes_purchased:
          type: integer
        total_charged:
          type: string
        message:
          type: string

    ServerSummary:
      type: object
      properties:
        id:
          type: string
        ip:
          type: string
          nullable: true
        size:
          type: string
        region:
          type: string
        image:
          type: string
        status:
          type: string
          enum: [provisioning, active, error, terminated]
        hourly_rate:
          type: number
        paid_until:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    ServerDetail:
      type: object
      properties:
        id:
          type: string
        ip:
          type: string
          nullable: true
        root_password:
          type: string
          nullable: true
        size:
          type: string
        region:
          type: string
        image:
          type: string
        status:
          type: string
        hourly_rate:
          type: number
        per_minute_rate:
          type: number
        paid_until:
          type: string
          format: date-time
        remaining_minutes:
          type: number
        remaining_hours:
          type: number
        total_paid:
          type: number
        created_at:
          type: string
          format: date-time

    SnapshotInfo:
      type: object
      nullable: true
      properties:
        id:
          type: string
        expires_in_hours:
          type: integer
          example: 72
        message:
          type: string

    Snapshot:
      type: object
      properties:
        id:
          type: string
        server_id:
          type: string
        size:
          type: string
        region:
          type: string
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    KeySummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        fingerprint:
          type: string
