openapi: 3.1.0
info:
  title: VpsAgent
  version: 0.1.0
  summary: Disposable VPS instances for AI agents — paid via x402 (USDC on Base).
  description: "Two product lines: ephemeral sandboxes (1h / 6h / 24h, $0.50–$5)
    for code execution, CI runs, browser automation; persistent servers (small /
    medium / large, 30-day or 365-day, $9.99–$359.99) for hosting agents and
    services. Pay-on-success: USDC settles only when the box is reachable on TCP
    :22. Failed provisions never settle — the EIP-3009 authorization expires
    unused, agent pays nothing. No signup, no API keys, machine-to-machine."
  license:
    name: Proprietary
  contact:
    name: VpsAgent
servers:
  - url: https://vps.phoneagent.xyz
x-x402:
  scheme: exact
  network: eip155:8453
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
  payTo: "0x8eCbd65228930060363C70854da7D37D57ffC358"
paths:
  /catalog:
    get:
      operationId: listCatalog
      summary: List rentable SKUs across both tracks.
      description: "Live storefront. Returns nine SKUs across two tracks (sandbox +
        server) with prices, spec floors, durations, and Phase-1 availability
        flags. Paid discovery: 0.01 USDC per call, settled inline. The dust
        price exists to register the resource in CDP Bazaar's discovery index."
      responses:
        "200":
          description: OK
          headers:
            X-Payment-Response:
              description: Base64-encoded `{tx, network}` of the on-chain settlement.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogList"
        "402":
          description: Payment required. Body is x402 PaymentRequirements priced at 0.01
            USDC.
          headers:
            X-Payment-Required:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentChallenge"
  /rent:
    post:
      operationId: rent
      summary: Provision a VPS instance.
      description: Submit a SKU; receive a 402 with priced paymentRequirements; sign
        an EIP-3009 transferWithAuthorization for the SKU price; resubmit with
        the X-Payment header. We verify (NOT settle), provision the box
        upstream, and persist the rental. Worker calls /settle only when the box
        is reachable on TCP :22 — typically 3–5 minutes for server-track SKUs.
        If the box never comes up, settle is never called and the EIP-3009
        authorization dies via validBefore. Agent paid nothing.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RentBody"
            examples:
              small:
                value:
                  sku: server_small_1mo
              large_with_region:
                value:
                  sku: server_large_1mo
                  region: EU
      responses:
        "200":
          description: Provisioned. Settle pending — the worker will charge once the box
            is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalCreated"
        "400":
          description: Invalid body. See the response detail for which field failed.
        "402":
          description: Payment required (or payment invalid).
        "503":
          description: SKU advertised but not yet routable in Phase 1, or upstream
            provider unavailable.
  /rentals/{rental_id}:
    get:
      operationId: getRental
      summary: Public lifecycle snapshot.
      parameters:
        - name: rental_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentalStatus"
        "404":
          description: Rental not found.
  /rentals/{rental_id}/renew:
    post:
      operationId: renew
      summary: Extend a rental's expires_at.
      description: "Token-gated. Stack cap periods ∈ [1, 12]. Same flow as /rent: 402
        → sign → resubmit → verify (no settle yet). Server-track renewals
        require a manual ops touch on the upstream provider's panel; the worker
        waits for the cancel-revoke and only then settles + re-cancels at the
        new expires_at. Crucially: expires_at does NOT change until the renewal
        fully settles — if the renewal never completes, the original expires_at
        remains in force and the rental is destroyed at the original cancelDate.
        Agent paid nothing."
      parameters:
        - name: rental_id
          in: path
          required: true
          schema:
            type: string
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: root_token from POST /rent.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenewBody"
            examples:
              default:
                value:
                  periods: 1
      responses:
        "200":
          description: Renewal verified, settle pending.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenewalCreated"
        "401":
          description: Missing or invalid token.
        "402":
          description: Payment required (or invalid).
        "404":
          description: Rental not found.
        "409":
          description: Rental not in 'active' status.
  /instance/{rental_id}:
    get:
      operationId: getInstance
      summary: "Live status: ipv4, running flag, expires_at."
      description: Token-gated. Use this to poll for `running == true` after /rent.
      parameters:
        - name: rental_id
          in: path
          required: true
          schema:
            type: string
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstanceStatus"
        "401":
          description: Missing or invalid token.
        "404":
          description: Rental not found.
        "502":
          description: Upstream provider unreachable; last-known fields returned.
  /healthz:
    get:
      operationId: healthz
      summary: Liveness probe.
      responses:
        "200":
          description: OK
components:
  schemas:
    CatalogList:
      type: array
      items:
        type: object
        properties:
          sku:
            type: string
            enum: &a1
              - sandbox_1h
              - sandbox_6h
              - sandbox_24h
              - server_small_1mo
              - server_small_12mo
              - server_medium_1mo
              - server_medium_12mo
              - server_large_1mo
              - server_large_12mo
          track:
            type: string
            enum:
              - sandbox
              - server
          price_usdc:
            type: number
          duration_minutes:
            type: integer
          spec_floor:
            type: object
            properties:
              vcpu:
                type: integer
              memory_mb:
                type: integer
          available:
            type: boolean
          description:
            type: string
    PaymentChallenge:
      type: object
      properties:
        x402Version:
          type: integer
          const: 1
        accepts:
          type: array
          items:
            $ref: "#/components/schemas/PaymentRequirements"
        error:
          type:
            - "null"
            - object
    PaymentRequirements:
      type: object
      properties:
        scheme:
          type: string
          const: exact
        network:
          type: string
          example: eip155:8453
        maxAmountRequired:
          type: string
          description: USDC base units (6 decimals).
        amount:
          type: string
          description: Same as maxAmountRequired (CDP requires both fields).
        resource:
          type: string
          format: uri
        description:
          type: string
        mimeType:
          type: string
          const: application/json
        payTo:
          type: string
        maxTimeoutSeconds:
          type: integer
        asset:
          type: string
        extra:
          type: object
        outputSchema:
          type: object
    RentBody:
      type: object
      required:
        - sku
      properties:
        sku:
          type: string
          enum: *a1
        region:
          type: string
          description: Optional region preference; provider picks default if absent.
    RentalCreated:
      type: object
      properties:
        rental_id:
          type: string
          format: uuid
        sku:
          type: string
          enum: *a1
        ipv4:
          type:
            - string
            - "null"
          description: Box IPv4. May be null at first; poll GET /instance/{id}.
        expires_at:
          type: integer
          description: Unix ms.
        root_token:
          type: string
          description: Bearer for GET /instance/{id} and POST /rentals/{id}/renew. Treat
            as a credential.
        root_user:
          type: string
          const: root
          description: SSH username.
        root_password:
          type: string
          description: SSH password — set via cloud-init at first boot. Re-fetchable via
            GET /instance/{id}?token=…
        status:
          type: string
          enum:
            - pending_settlement
        note:
          type: string
    RentalStatus:
      type: object
      properties:
        rental_id:
          type: string
          format: uuid
        sku:
          type: string
          enum: *a1
        track:
          type: string
          enum:
            - sandbox
            - server
        status:
          type: string
          enum:
            - pending_settlement
            - active
            - renewing
            - expired
        created_at:
          type: integer
        expires_at:
          type: integer
        grace_until:
          type:
            - integer
            - "null"
        settle_tx_hash:
          type:
            - string
            - "null"
        renewal_count:
          type: integer
        last_renewal_tx_hash:
          type:
            - string
            - "null"
    RenewBody:
      type: object
      properties:
        periods:
          type: integer
          minimum: 1
          maximum: 12
          default: 1
    RenewalCreated:
      type: object
      properties:
        rental_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - renewing
        current_expires_at:
          type: integer
        predicted_expires_at:
          type: integer
        manual_step:
          type: string
    InstanceStatus:
      type: object
      properties:
        rental_id:
          type: string
          format: uuid
        ipv4:
          type:
            - string
            - "null"
        running:
          type: boolean
        expires_at:
          type: integer
        status:
          type: string
          enum:
            - pending_settlement
            - active
            - renewing
            - expired
        root_user:
          type: string
          const: root
        root_password:
          type: string
        note:
          type: string
