openapi: 3.1.0
info:
  title: MakerWorld Estimator API
  description: |
    Centralized estimation, metadata parsing, and leaderboard service for
    3D printing cost calculations.

    The API fetches model metadata from MakerWorld's JSON API, caches results
    in a PocketBase database, and exposes endpoints for scraping, file upload
    estimation, directory browsing, leaderboards, and configuration presets.

    **Authentication** is optional. If the `API_KEY` environment variable is
    set on the server, the `/api/estimate/upload` endpoint requires a key
    supplied via `Authorization: Bearer <key>` or `x-api-key: <key>`.

    **Rate limits** apply only to `/api/estimate/upload`: 5 requests per IP
    per 60 seconds.
  version: 1.0.0
  contact:
    email: contact@tryar.in
  license:
    name: Proprietary

servers:
  - url: https://api.tryar.in
    description: Production (Cloudflare Workers)
  - url: http://localhost:3000
    description: Local development (Hono Node server)

tags:
  - name: Service
    description: Health check and API discovery
  - name: Configuration
    description: Presets and currency detection
  - name: Models
    description: MakerWorld model scraping and directory
  - name: Estimation
    description: Cost breakdown for uploaded local files
  - name: Leaderboard
    description: Popularity and trending rankings
  - name: Notifications
    description: Gotify push notification relay
  - name: Payments & Credits
    description: User credit balances, credit deduction, and Dodo Payments checkout/webhooks
  - name: Authentication
    description: Email verification for password-based accounts (Listmonk transactional email)
  - name: MCP
    description: Model Context Protocol (JSON-RPC 2.0) streamable HTTP endpoints for AI assistants

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Pass your API key as a Bearer token. Only required on /api/estimate/upload when the server is configured with an API_KEY.
    ApiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: Alternative to Bearer auth for /api/estimate/upload.
    UserSessionAuth:
      type: http
      scheme: bearer
      bearerFormat: PocketBase
      description: |
        The user's PocketBase session token (obtained by signing in on the
        estimator website). Required on all user-scoped endpoints
        (`/api/credits*`, `/api/user/*`) and on the calculation endpoints
        `/api/scrape` and `/api/scrape/collection`. The server verifies that
        the token is valid AND belongs to the exact `userId` being referenced
        (IDOR protection) — a valid token for a different user is rejected.

  parameters:
    UserSessionToken:
      name: Authorization
      in: header
      required: true
      description: "PocketBase session token as `Bearer <token>` (the `x-pb-token` header is also accepted)."
      schema:
        type: string
        example: "Bearer pb-session-token"

  responses:
    Unauthorized:
      description: |
        Missing or invalid session token, or the token does not belong to the
        referenced `userId`. Sign in again and retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Unauthorized. A valid user session token is required."
    InsufficientCredits:
      description: The user's credit balance is below the cost of this calculation.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: "Insufficient credits. Purchase a credit pack on the Pricing page."
              requiredCredits:
                type: integer
                example: 1
              currentCredits:
                type: integer
                example: 0
              plan:
                type: string
                example: "free"
    EmailNotVerified:
      description: |
        The signed-in account has not verified its email address yet (soft gate).
        The frontend opens the "Verify your email" flow; call `/api/auth/send-verification`
        to resend the link.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: "Please verify your email address to calculate. Check your inbox for the verification link."
              code:
                type: string
                example: "email_not_verified"

  schemas:
    FilamentEntry:
      type: object
      description: Per-filament slot data for a print profile.
      properties:
        type:
          type: string
          example: PLA
          description: Filament material type (e.g. PLA, PETG, TPU).
        color:
          type: string
          example: "#DC2626"
          description: Filament hex color code.
        usedG:
          type: string
          example: "14.32"
          description: Filament used in grams (string from MakerWorld API).
        usedM:
          type: string
          example: "4.82"
          description: Filament used in metres (string from MakerWorld API).

    PlateFilamentEntry:
      type: object
      description: Per-filament slot data scoped to a single build plate.
      properties:
        type:
          type: string
          example: PLA
        color:
          type: string
          example: "#2563EB"
        usedG:
          type: string
          example: "7.41"

    PlateEntry:
      type: object
      description: Per-build-plate breakdown for a print profile.
      properties:
        index:
          type: integer
          example: 1
          description: 1-based plate index.
        name:
          type: string
          example: Plate 1
        thumbnail:
          type: string
          format: uri
          example: "https://makerworld.bblmakeworld.com/md5/xxx.png"
          description: Plate thumbnail URL.
        prediction:
          type: integer
          example: 7320
          description: Plate print time in seconds.
        weight:
          type: number
          example: 23.4
          description: Plate filament weight in grams.
        filaments:
          type: array
          items:
            $ref: '#/components/schemas/PlateFilamentEntry'

    PrintProfile:
      type: object
      description: Summary of a single print profile (instance) for a model.
      properties:
        profileId:
          type: integer
          example: 4382910
        profileName:
          type: string
          example: "0.20mm Standard @BambuLab X1C"
        printTimeSeconds:
          type: integer
          example: 26340
        printTimeHours:
          type: number
          format: float
          example: 7.317
        weightGrams:
          type: number
          example: 47.5
        needAms:
          type: boolean
          example: false
        plateCount:
          type: integer
          example: 2
        plates:
          type: array
          items:
            $ref: '#/components/schemas/PlateEntry'
        filaments:
          type: array
          items:
            $ref: '#/components/schemas/FilamentEntry'

    ModelResponse:
      type: object
      description: |
        Full model data returned by /api/scrape and embedded in
        /api/models and /api/leaderboard.
      properties:
        profileId:
          type: integer
          example: 4382910
        modelId:
          type: integer
          example: 1717122
        modelName:
          type: string
          example: "Statue of Liberty"
        modelImage:
          type: string
          format: uri
          example: "https://makerworld.bblmakeworld.com/md5/cover.jpg"
        cleanUrl:
          type: string
          format: uri
          example: "https://makerworld.com/en/models/1717122-statue-of-liberty"
        profileName:
          type: string
          example: "0.20mm Standard @BambuLab X1C"
        printTimeSeconds:
          type: integer
          example: 26340
        printTimeHours:
          type: number
          example: 7.317
        weightGrams:
          type: number
          example: 47.5
        isDefault:
          type: boolean
          example: true
          description: Whether this profile is the model's default print profile.
        plateCount:
          type: integer
          example: 2
        filaments:
          type: array
          items:
            $ref: '#/components/schemas/FilamentEntry'
        plates:
          type: array
          items:
            $ref: '#/components/schemas/PlateEntry'
        profiles:
          type: array
          description: All print profiles available for this model.
          items:
            $ref: '#/components/schemas/PrintProfile'
        creatorName:
          type: string
          nullable: true
          example: "BambuLab_Official"
        creatorAvatar:
          type: string
          format: uri
          nullable: true
        likeCount:
          type: integer
          example: 4200
        downloadCount:
          type: integer
          example: 12800
        printCount:
          type: integer
          example: 3300
        ratingScore:
          type: number
          format: float
          example: 4.8
        license:
          type: string
          nullable: true
          example: "CC BY"
        calculationCount:
          type: integer
          example: 37
          description: Number of times this profile has been estimated.
        estimatedCost:
          type: number
          example: 250
          description: |
            Cached cost estimate at default rates
            (50/hr, 2/g, 20 plate fee, 30 multi-color fee,
            0 labour, 0% margin, rounding factor 10).
        lastCalculatedAt:
          type: string
          format: date-time
          example: "2025-11-21T09:14:00Z"
        cached:
          type: boolean
          description: Present in /api/scrape responses only. True when data came from the DB cache.

    PricingRates:
      type: object
      description: The effective pricing rates used for the estimate.
      properties:
        timeRate:
          type: number
          example: 50
        weightRate:
          type: number
          example: 2
        plateFee:
          type: number
          example: 20
        multicolorFee:
          type: number
          example: 30
        labourFee:
          type: number
          example: 0
        profitMarginPct:
          type: number
          example: 0
        roundingFactor:
          type: number
          example: 10

    SingleUnitBreakdown:
      type: object
      properties:
        timeCost:
          type: number
          example: 36.58
        materialCost:
          type: number
          example: 95.0
        plateCost:
          type: number
          example: 20.0
        hardwareCost:
          type: number
          example: 0
        cost:
          type: number
          example: 151.58

    HardwareItem:
      type: object
      description: An automatically detected hardware add-on matched by keyword.
      properties:
        keyword:
          type: string
          example: "magnet"
        name:
          type: string
          example: "Neodymium Magnet"
        cost:
          type: number
          example: 25

    PricingBreakdown:
      type: object
      description: Full itemised cost breakdown returned by /api/estimate/upload.
      properties:
        qty:
          type: integer
          example: 1
        rates:
          $ref: '#/components/schemas/PricingRates'
        singleUnit:
          $ref: '#/components/schemas/SingleUnitBreakdown'
        totalPrintTimeHours:
          type: number
          example: 7.317
        totalPlatesRun:
          type: integer
          example: 2
        plateCost:
          type: number
          example: 20
        totalMulticolorCost:
          type: number
          example: 0
        totalMaterialCost:
          type: number
          example: 95
        totalTimeCost:
          type: number
          example: 36.58
        totalHardwareCost:
          type: number
          example: 0
        subtotal:
          type: number
          example: 151.58
        labourFee:
          type: number
          example: 0
        profitAmount:
          type: number
          example: 0
        rawCost:
          type: number
          example: 151.58
        estimatedCost:
          type: number
          example: 160
          description: Final cost after rounding.
        currencySymbol:
          type: string
          example: "Rs."
        formula:
          type: string
          description: Human-readable formula string summarising the calculation. Contains HTML entities for frontend rendering.
          example: "1 unit(s) · 7.32h total print time · Max 4 per plate"
        activeHardware:
          type: array
          items:
            $ref: '#/components/schemas/HardwareItem'
        savings:
          type: number
          example: 0
          description: Cost saved vs. linear (non-batched) pricing. Non-zero only when qty > 1.

    PresetColor:
      type: object
      properties:
        name:
          type: string
          example: "PLA Basic Red"
        hex:
          type: string
          example: "#DC2626"
        multiplier:
          type: number
          example: 1.0
          description: Cost multiplier applied to weight-based material cost.

    HardwarePreset:
      type: object
      properties:
        keyword:
          type: string
          example: "magnet"
          description: Case-insensitive keyword matched against model name and profile name.
        name:
          type: string
          example: "Neodymium Magnet"
        cost:
          type: number
          example: 25
          description: Flat cost added per item (in default currency units).

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          example: "URL query parameter is required"

paths:
  /:
    get:
      operationId: getRoot
      tags: [Service]
      summary: Health check and API discovery
      description: |
        Returns a 200 OK with service metadata and a map of all available
        endpoints. Useful for load-balancer health checks and quick discovery.
      responses:
        '200':
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: OK
                  name:
                    type: string
                    example: MakerWorld Estimator API
                  description:
                    type: string
                  version:
                    type: string
                    example: "1.0.0"
                  endpoints:
                    type: object
                    additionalProperties:
                      type: string
                  links:
                    type: object
                    properties:
                      homepage:
                        type: string
                        format: uri
                      documentation:
                        type: string
                        format: uri

  /api/presets:
    get:
      operationId: getPresets
      tags: [Configuration]
      summary: Get filament color presets and hardware add-on presets
      description: |
        Returns the two shared preset tables used by both the frontend and the
        backend pricing engine:

        - **presetColors** — named filament colour swatches with cost multipliers.
          Colours are matched via nearest-neighbour RGB Euclidean distance.
        - **hardwarePresets** — keyword-triggered hardware add-ons automatically
          detected from model names and profile names.
      responses:
        '200':
          description: Preset data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  presetColors:
                    type: array
                    items:
                      $ref: '#/components/schemas/PresetColor'
                  hardwarePresets:
                    type: array
                    items:
                      $ref: '#/components/schemas/HardwarePreset'

  /api/detect-currency:
    get:
      operationId: detectCurrency
      tags: [Configuration]
      summary: Detect visitor currency and suggested locale from Cloudflare headers
      description: |
        Reads the `cf-ipcountry` header injected by Cloudflare (plus the
        optional `Accept-Language` header) to suggest a currency and a UI
        locale for the visitor. Both are **suggestions** — the URL prefix, a
        stored preference, and `hreflang` remain authoritative (no forced
        redirect).

        Country-to-currency mapping:

        | Country | Currency |
        |---------|----------|
        | IN | INR |
        | GB | GBP |
        | JP | JPY |
        | CN | CNY |
        | SG | SGD |
        | CA | CAD |
        | AU | AUD |
        | BR | BRL |
        | Euro-zone | EUR |
        | All others | USD |

        When running locally without Cloudflare the header is absent and the
        response falls back to `"INR"`.

        **Locale suggestion:** the country is the primary signal (Appendix A of
        `docs/I18N_PLAN.md`); `Accept-Language` only overrides the documented
        edge cases — India (`hi`), Canada (`fr`), Switzerland (`fr`/`it`) — and
        is otherwise used for unmapped countries. The default is `en`.
      responses:
        '200':
          description: Detected currency and suggested locale.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  country:
                    type: string
                    nullable: true
                    example: "IN"
                    description: ISO 3166-1 alpha-2 country code, or null if unavailable.
                  currency:
                    type: string
                    example: "INR"
                    description: ISO 4217 currency code.
                  suggestedLocale:
                    type: string
                    example: "zh-Hans"
                    description: Full BCP-47 locale tag suggested for the visitor (suggestion only).
                  htmlLang:
                    type: string
                    example: "zh-Hans"
                    description: Value for the `<html lang>` attribute (same as `suggestedLocale`).
                  urlPrefix:
                    type: string
                    example: "zh"
                    description: URL path prefix for the locale (`de`, `zh`, …); empty string for English.

  /api/scrape:
    get:
      operationId: scrapeModel
      tags: [Models]
      summary: Fetch and cache a MakerWorld model's print profile data
      security:
        - UserSessionAuth: []
      description: |
        Accepts a MakerWorld model URL, extracts the model ID and optional
        profile ID, and returns full print specification data.

        **Authentication & credits (server-enforced):**
        - Requires a verified user session: `Authorization: Bearer <pb-token>`
          plus the matching `userId` (query, body, or `X-User-Id` header).
          Returns `401` when the token is missing, invalid, or belongs to a
          different user.
        - Credits are checked BEFORE any upstream work (`402` when the balance
          is below 1) and deducted AFTER a successful calculation — failed
          scrapes are never charged.

        **Cache behaviour:**
        1. If `nocache=true` is absent, the database is checked first.
        2. On a cache hit the response includes `"cached": true` and
           increments the `calculation_count` counter.
        3. On a cache miss the MakerWorld JSON API is called at
           `makerworld.com/api/v1/design-service/design/{modelId}?handle=en`,
           the result is persisted, and `"cached": false` is returned.

        **Profile selection priority:**
        1. `#profileId-{id}` URL hash
        2. `profileId={id}` query param
        3. MakerWorld default instance
        4. First available instance
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - name: userId
          in: query
          required: true
          description: PocketBase user ID (must match the token owner). The `X-User-Id` header is also accepted.
          schema:
            type: string
            example: "usr_abc123"
        - name: url
          in: query
          required: true
          description: |
            Full MakerWorld model page URL. May include an optional profile
            selector as a hash (`#profileId-{id}`) or query param (`?profileId={id}`).
          schema:
            type: string
            format: uri
          example: "https://makerworld.com/en/models/1717122-statue-of-liberty#profileId-4382910"
        - name: nocache
          in: query
          required: false
          description: Set to `true` to bypass the DB cache and force a fresh fetch.
          schema:
            type: string
            enum: ["true", "false"]
            default: "false"
      responses:
        '200':
          description: Model data successfully fetched (from cache or live).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ModelResponse'
                  - type: object
                    required: [success]
                    properties:
                      success:
                        type: boolean
                        example: true
                      creditBalance:
                        type: integer
                        nullable: true
                        description: Remaining credits after the server-side deduction (null for Enterprise users).
                        example: 9
                      creditsDeducted:
                        type: integer
                        description: Credits deducted for this request (0 for Enterprise users).
                        example: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '400':
          description: Missing or invalid URL parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingUrl:
                  value:
                    success: false
                    error: "URL query parameter is required"
                invalidUrl:
                  value:
                    success: false
                    error: "Invalid URL. Must be a MakerWorld link."
                noModelId:
                  value:
                    success: false
                    error: "Could not extract model ID from URL."
        '404':
          description: Model not found or no print profiles available.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: MakerWorld upstream API returned an error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/scrape/collection:
    get:
      operationId: scrapeCollection
      tags: [Collections]
      summary: Fetch and cache all models in a MakerWorld collection
      security:
        - UserSessionAuth: []
      description: |
        Accepts a MakerWorld collection URL, extracts the collection ID, and
        calls the MakerWorld favorites JSON API to fetch collection metadata.
        For each model in the collection, it scrapes/checks the DB cache in
        parallel and aggregates the results.

        **Authentication & credits (server-enforced):**
        - Requires a verified user session (same contract as `/api/scrape`):
          `401` when the token is missing, invalid, or not owned by `userId`.
        - Costs 1 credit per model in the collection. The balance is checked
          before per-model scraping starts (`402` when below the model count),
          and only models that actually succeed are charged. A backend memory
          cache hit returns the cached result for 0 credits.
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - name: userId
          in: query
          required: true
          description: PocketBase user ID (must match the token owner). The `X-User-Id` header is also accepted.
          schema:
            type: string
            example: "usr_abc123"
        - name: url
          in: query
          required: true
          description: Full MakerWorld collection page URL.
          schema:
            type: string
            format: uri
          example: "https://makerworld.com/en/collections/27910720-large-prints"
        - name: nocache
          in: query
          required: false
          description: Set to `true` to bypass the DB cache for individual models.
          schema:
            type: string
            enum: ["true", "false"]
            default: "false"
      responses:
        '200':
          description: Collection details and models successfully fetched.
          content:
            application/json:
              schema:
                type: object
                required: [success, isCollection, collectionId, collectionTitle, models]
                properties:
                  success:
                    type: boolean
                    example: true
                  isCollection:
                    type: boolean
                    example: true
                  collectionId:
                    type: integer
                    example: 27910720
                  collectionTitle:
                    type: string
                    example: "Large Prints"
                  creatorName:
                    type: string
                    nullable: true
                    example: "Hirawat"
                  creatorAvatar:
                    type: string
                    nullable: true
                    format: uri
                    example: "https://public-cdn.bblmw.com/avatar/..."
                  designCnt:
                    type: integer
                    example: 14
                  models:
                    type: array
                    items:
                      oneOf:
                        - allOf:
                            - $ref: '#/components/schemas/ModelResponse'
                            - type: object
                              required: [success]
                              properties:
                                success:
                                  type: boolean
                                  example: true
                        - type: object
                          required: [success, modelId, title, cover, error]
                          properties:
                            success:
                              type: boolean
                              example: false
                            modelId:
                              type: integer
                              example: 1426447
                            title:
                              type: string
                              example: "Borsa Summer"
                            cover:
                              type: string
                              example: "https://..."
                            error:
                              type: string
                              example: "MakerWorld API returned status 404"
                  creditBalance:
                    type: integer
                    nullable: true
                    description: Remaining credits after the server-side deduction (null for Enterprise users or cached results).
                    example: 7
                  creditsDeducted:
                    type: integer
                    description: Number of models actually charged for this request.
                    example: 2
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '400':
          description: Missing or invalid URL parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Collection not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/models:
    get:
      operationId: listModels
      tags: [Models]
      summary: Browse and search the cached model directory
      description: |
        Returns all models stored in the database with optional free-text
        search and sort order.

        The `estimatedCost` in each model is pre-calculated at default rates
        and cached in the database for fast rendering without recalculation.
      parameters:
        - name: q
          in: query
          required: false
          description: Free-text search filter applied to model name and creator name.
          schema:
            type: string
          example: "statue"
        - name: sort
          in: query
          required: false
          description: |
            Sort order:
            - `popular` (default) — by calculation_count DESC, then last_calculated_at DESC
            - `recent` — by last_calculated_at DESC
            - `weight` — by weight_grams ASC
            - `time` — by print_time_seconds ASC
            - `price_asc` — by estimated_cost ASC
            - `price_desc` — by estimated_cost DESC
          schema:
            type: string
            enum: [popular, recent, weight, time, price_asc, price_desc]
            default: popular
        - name: min_price
          in: query
          required: false
          description: Filter models with estimated cost greater than or equal to this amount.
          schema:
            type: number
            example: 100
        - name: max_price
          in: query
          required: false
          description: Filter models with estimated cost less than or equal to this amount.
          schema:
            type: number
            example: 500
        - name: limit
          in: query
          required: false
          description: |
            Page size. Defaults to 60, hard-capped at 500. The endpoint is
            paginated — clients must page instead of expecting the full set.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 60
            example: 24
        - name: page
          in: query
          required: false
          description: 1-based page number.
          schema:
            type: integer
            minimum: 1
            default: 1
            example: 2
      responses:
        '200':
          description: |
            Paginated list of cached models. Responses are cacheable
            (`Cache-Control: public, max-age=60, stale-while-revalidate=300`);
            `nocache=true` responses are `no-store`.
          headers:
            Cache-Control:
              schema:
                type: string
              example: public, max-age=60, stale-while-revalidate=300
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  models:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelResponse'
                  page:
                    type: integer
                    example: 2
                  perPage:
                    type: integer
                    example: 24
                  totalItems:
                    type: integer
                    description: Total number of models matching the active filters.
                    example: 500
                  totalPages:
                    type: integer
                    example: 21
        '500':
          description: Database error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/leaderboard:
    get:
      operationId: getLeaderboard
      tags: [Leaderboard]
      summary: Top all-time models and weekly trending models
      description: |
        Returns two ranked lists of up to 10 models each:

        - **topAllTime** — ranked by `calculation_count` (all-time estimates),
          then by `last_calculated_at`.
        - **trending** — ranked by calculations in the last **7 days**
          (from `calculation_logs`), then by `last_calculated_at`.
          Each entry includes a `recentCalculations` field.
      responses:
        '200':
          description: Leaderboard data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  topAllTime:
                    type: array
                    maxItems: 10
                    items:
                      $ref: '#/components/schemas/ModelResponse'
                  trending:
                    type: array
                    maxItems: 10
                    items:
                      allOf:
                        - $ref: '#/components/schemas/ModelResponse'
                        - type: object
                          properties:
                            recentCalculations:
                              type: integer
                              example: 14
                              description: Number of estimate events in the last 7 days.
        '500':
          description: Database error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/estimate/upload:
    post:
      operationId: estimateUpload
      tags: [Estimation]
      summary: Estimate print cost for an uploaded STL or 3MF file
      description: |
        Accepts a binary STL or 3MF file via `multipart/form-data` and returns
        a parsed geometry/metadata summary together with a full pricing
        breakdown.

        **File parsing strategy:**
        - **STL** — Binary/ASCII geometry analysis. Print time and weight are
          estimated via slicing heuristics (volume to layer count, time, weight).
        - **3MF** — Zip archive inspection in priority order:
          1. BambuStudio slicer metadata (`Metadata/slice_info.config`)
          2. Plate G-code files (`Metadata/plate_*.gcode`) for embedded metadata
          3. Geometry fallback via fast string/regex scan of `.model` files

        **Rate limit:** 5 requests per IP per 60 seconds.

        **Privacy-first app:** the first-party web/mobile/desktop app never
        calls this endpoint — it parses STL/3MF files locally in the browser so
        they never leave the user's device. This endpoint is intended for
        API-key integrations that explicitly opt into server-side parsing.

        **Authentication:** Required only when `API_KEY` is set on the server.
        Supply via `Authorization: Bearer <key>` or `x-api-key: <key>`.

        **Credits (server-enforced):** A user API key (non-master) costs 1 credit
        per *successful* estimate — the deduction happens after parsing and
        pricing succeed, so failed uploads are never charged. The master key is
        free. A `402` is returned when the account is out of credits.

        All pricing parameters are optional and default to the web app's
        factory settings.
      security:
        - BearerAuth: []
        - ApiKeyHeader: []
        - {}
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: STL or 3MF file to parse. Maximum 15 MB.
                qty:
                  type: integer
                  default: 1
                  minimum: 1
                  description: Number of copies to estimate (enables bulk batching optimisation).
                timeRate:
                  type: number
                  default: 50
                  description: Printer hourly rate in currency units per hour.
                weightRate:
                  type: number
                  default: 2
                  description: Filament cost rate in currency units per gram.
                plateFee:
                  type: number
                  default: 20
                  description: Setup fee charged per additional plate run (first plate is included in time cost).
                multicolorFee:
                  type: number
                  default: 30
                  description: Fee per extra colour per plate run (AMS filament change surcharge).
                labourFee:
                  type: number
                  default: 0
                  description: Flat labour charge applied once per order.
                profitMargin:
                  type: number
                  default: 0
                  description: Profit margin percentage applied to (subtotal + labour).
                roundingFactor:
                  type: number
                  default: 10
                  description: Round up the final cost to the next multiple of this value. Set to 0 to disable rounding.
                currencySymbol:
                  type: string
                  default: "Rs."
                  description: Currency symbol used in the human-readable formula string.
                currency:
                  type: string
                  default: ""
                  description: Optional 3-letter currency code (e.g., USD, EUR, JPY) to resolve the exchange rate. If empty, the rate is resolved using currencySymbol.
                notify:
                  type: string
                  enum: ["true", "false"]
                  default: "false"
                  description: Set to "true" to trigger a Gotify push notification. Requires GOTIFY_URL and GOTIFY_TOKEN to be configured.
      responses:
        '200':
          description: File parsed and estimate calculated successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  filename:
                    type: string
                    example: "headphone_stand.3mf"
                  type:
                    type: string
                    enum: [STL, 3MF]
                    example: "3MF"
                  parsedData:
                    type: object
                    description: Raw values extracted from the file before pricing.
                    properties:
                      weightGrams:
                        type: number
                        example: 47.5
                      printTimeHours:
                        type: number
                        example: 7.317
                      printTimeSeconds:
                        type: integer
                        example: 26340
                      widthMm:
                        type: number
                        example: 120.4
                        description: Bounding-box width in millimetres (0 if unavailable).
                      lengthMm:
                        type: number
                        example: 85.2
                        description: Bounding-box length in millimetres (0 if unavailable).
                      heightMm:
                        type: number
                        example: 55.0
                        description: Bounding-box height in millimetres (0 if unavailable).
                      filaments:
                        type: array
                        items:
                          $ref: '#/components/schemas/FilamentEntry'
                      plates:
                        type: array
                        items:
                          $ref: '#/components/schemas/PlateEntry'
                      isFromMetadata:
                        type: boolean
                        example: true
                        description: |
                          true if exact slicer metadata was found inside the file;
                          false if values were estimated from geometry heuristics.
                  pricing:
                    $ref: '#/components/schemas/PricingBreakdown'
                  creditBalance:
                    type: integer
                    nullable: true
                    description: Remaining credits after the server-side deduction (null when the master key is used).
                    example: 9
                  creditsDeducted:
                    type: integer
                    description: Credits deducted for this request (0 when the master key is used).
                    example: 1
        '402':
          description: Insufficient credits on the account (user API key, out of balance).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Insufficient calculation credits on account."
        '403':
          description: |
            The user API key belongs to an account whose email address is not verified yet
            (`code: email_not_verified`). Failed parses are never charged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Please verify your email address to calculate. Check your inbox for the verification link."
                code: "email_not_verified"
        '400':
          description: Bad request — missing file, unsupported format, or invalid parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noFile:
                  value:
                    success: false
                    error: "No file uploaded or invalid file field"
                unsupportedType:
                  value:
                    success: false
                    error: "Invalid file type. Only STL and 3MF files are supported."
        '401':
          description: Unauthorized — missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Unauthorized. Valid API key required."
        '413':
          description: Payload too large — file exceeds the 15 MB limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Payload Too Large. Maximum upload size is 15MB."
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Too many requests. Limit is 5 uploads per minute."
        '500':
          description: Internal parsing or server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/notify:
    post:
      operationId: sendNotification
      tags: [Notifications]
      summary: Relay a print estimate notification to Gotify
      description: |
        Forwards an estimate summary to a self-hosted Gotify server.
        This endpoint is a no-op (returns success with a warning) if
        `GOTIFY_URL` / `GOTIFY_TOKEN` are not configured on the server.

        **Requires a verified user session** (`Authorization: Bearer <pb_token>`
        plus `X-User-Id`) to prevent anonymous Gotify spam.

        The web app calls this endpoint after a user-triggered estimate to
        deliver a push notification to the operator's device.
      security:
        - UserSessionAuth: []
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [modelName, cost, cleanUrl]
              properties:
                modelName:
                  type: string
                  example: "Statue of Liberty"
                cost:
                  type: string
                  description: Formatted cost string including currency symbol.
                  example: "Rs. 250"
                colors:
                  type: string
                  description: Comma-separated list of filament colour names.
                  example: "PLA Basic Black, PLA Silk Gold"
                hardware:
                  type: string
                  description: Comma-separated list of detected hardware add-ons, or "N/A".
                  example: "N/A"
                cleanUrl:
                  type: string
                  format: uri
                  description: Clean MakerWorld model URL (no query params or fragments).
                  example: "https://makerworld.com/en/models/1717122-statue-of-liberty"
                qty:
                  type: integer
                  description: Number of copies being estimated.
                  example: 3
                profileId:
                  type: integer
                  description: Print profile ID of the estimate.
                  example: 4382910
                profileName:
                  type: string
                  description: Human-readable profile name.
                  example: "0.20mm Standard @BambuLab X1C"
      responses:
        '200':
          description: Notification sent (or skipped because Gotify is not configured).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  warning:
                    type: string
                    description: Present only when Gotify is not configured.
                    example: "Gotify not configured"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: Failed to reach the Gotify server.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/credits:
    get:
      tags:
        - Payments & Credits
      summary: Retrieve user credit balance and plan status
      security:
        - UserSessionAuth: []
      description: |
        Returns the user's available calculation credits, free monthly API credits, total available credits, active plan, and next monthly reset timestamp.
        Requires the user's session token; the token must belong to `userId` (returns 401 otherwise).
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - in: query
          name: userId
          required: true
          schema:
            type: string
          description: PocketBase user ID. Must match the token owner.
      responses:
        '200':
          description: Credit balance and plan details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  credits:
                    type: integer
                    example: 10
                  totalCredits:
                    type: integer
                    example: 10
                  plan:
                    type: string
                    example: "free"
                  nextMonthlyReset:
                    type: string
                    format: date-time
                    example: "2026-08-01T00:00:00.000Z"
        '400':
          description: Missing userId parameter.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/credits/history:
    get:
      tags:
        - Payments & Credits
      summary: Retrieve user credit transaction history
      security:
        - UserSessionAuth: []
      description: |
        Returns an array of historical credit debits, grants, plan upgrades, and monthly resets for the specified user.
        Requires the user's session token; the token must belong to `userId` (returns 401 otherwise).
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - in: query
          name: userId
          required: true
          schema:
            type: string
          description: PocketBase user ID. Must match the token owner.
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            default: 50
          description: Maximum number of transaction records to return.
      responses:
        '200':
          description: List of credit transactions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transactions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        user:
                          type: string
                        type:
                          type: string
                          enum: [debit, credit, reset, plan_upgrade]
                        amount:
                          type: integer
                        balance_after:
                          type: integer
                        source:
                          type: string
                        model_name:
                          type: string
                        clean_url:
                          type: string
                        created:
                          type: string
                          format: date-time
        '400':
          description: Missing userId parameter.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/credits/deduct:
    post:
      tags:
        - Payments & Credits
      summary: Deduct calculation credits
      security:
        - UserSessionAuth: []
      description: |
        Deducts credits (default 1, up to `amount`) from the specified user. Returns HTTP 402 when the balance is insufficient.
        The deduction is atomic (optimistic-locked read-modify-write with retry), so concurrent deductions cannot overspend a balance.
        Requires the user's session token; the token must belong to `userId` (returns 401 otherwise).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
                  example: "usr_abc123"
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: Credit deducted successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  remainingCredits:
                    type: integer
                    example: 14
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient credits remaining.
        '403':
          $ref: '#/components/responses/EmailNotVerified'

  /api/auth/send-verification:
    post:
      tags:
        - Authentication
      summary: Send (or resend) the account verification email
      description: |
        Sends a verification email through the configured Listmonk instance.

        **Authenticated calls** (Bearer session token + userId) act on the signed-in account and
        return `429` while the 60-second resend cooldown is active, or `503` when the email
        service is not configured.

        **Anonymous calls** accept an `email` and always return a generic `200`
        (`{ success: true }`) whether or not the account exists, to prevent account enumeration.

        The verification link is `/verify-email?token=...` and expires after 24 hours.
      security:
        - UserSessionAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
                  example: "usr_abc123"
                email:
                  type: string
                  description: Required for anonymous requests.
                  example: "user@example.com"
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: Generic success (anonymous) or email queued (authenticated).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  sent:
                    type: boolean
                    example: true
                  alreadyVerified:
                    type: boolean
                    example: false
        '400':
          description: Missing email parameter.
        '429':
          description: Resend cooldown active. Includes `retryAfter` seconds.
        '503':
          description: Listmonk email service is not configured.

  /api/auth/verify-email:
    post:
      tags:
        - Authentication
      summary: Redeem an email verification token
      description: |
        Called by the `/verify-email` landing page with the `token` from the email link.
        On success the PocketBase `users.verified` flag is set, the single-use token is cleared,
        and (in the background) opted-in users are subscribed to the onboarding Listmonk list
        and sent the welcome email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                  example: "Xr9k...43chars"
      responses:
        '200':
          description: Email verified (or already verified).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  email:
                    type: string
                    example: "user@example.com"
                  alreadyVerified:
                    type: boolean
                    example: false
        '400':
          description: "Missing or invalid token (`code: invalid_token`)."
        '410':
          description: "Token expired (`code: expired_token`)."

  /api/payments/checkout:
    post:
      tags:
        - Payments & Credits
      summary: Create Dodo Payments checkout session
      description: |
        Initializes a Dodo Payments checkout session for subscription plans (Pro, Enterprise) or credit bulk packs (pack_50, pack_150, pack_500).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packId]
              properties:
                packId:
                  type: string
                  example: "pro_monthly"
                userId:
                  type: string
                  example: "usr_abc123"
                customerEmail:
                  type: string
                  example: "user@example.com"
      responses:
        '200':
          description: Checkout session created. Returns checkout_url.
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkout_url:
                    type: string
                    example: "https://test.dodopayments.com/buy/pro_monthly?session=123"

  /api/payments/verify:
    get:
      tags:
        - Payments & Credits
      summary: Verify Dodo Payment & Fulfill Credits/Plan
      description: |
        Verifies completed Dodo Payments by `payment_id` or `checkout_id`, grants credits/plan idempotently in PocketBase, and returns updated user balance.

        **Dual-key idempotency:** each payment log row stores both the Dodo
        payment id (`pay_...`) and the checkout session id (`chk_...`). The
        idempotency check matches on either identifier, so this endpoint and
        the `payment.succeeded` webhook can never grant the same purchase
        twice even when they see different Dodo identifiers for it.
      parameters:
        - in: query
          name: payment_id
          required: false
          schema:
            type: string
        - in: query
          name: checkout_id
          required: false
          schema:
            type: string
        - in: query
          name: email
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Payment verified and credits granted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  verified:
                    type: boolean
                  creditsGranted:
                    type: integer
                  credits:
                    type: integer
                  plan:
                    type: string

  /api/payments/webhook:
    post:
      tags:
        - Payments & Credits
      summary: Dodo Payments Webhook Receiver
      description: |
        Receives payment and subscription events from Dodo Payments. Verifies HMAC signature and updates user credits or plan status in PocketBase.

        Duplicate events are detected via dual-key idempotency (both the
        `pay_...` payment id and the `chk_...` checkout session id stored in
        `payment_logs`) and answered with `{ received: true, idempotent: true }`
        without granting credits again.
      responses:
        '200':
          description: Webhook processed successfully.
        '401':
          description: Invalid HMAC signature.

  /api/user/presets:
    get:
      tags:
        - User Settings
      summary: Fetch saved calculation presets
      security:
        - UserSessionAuth: []
      description: Returns saved custom calculation defaults (rates, fees, margin, currency) for a logged in user. Requires the user's session token matching `userId` (401 otherwise).
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - in: query
          name: userId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Presets object returned.
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - User Settings
      summary: Save calculation presets
      security:
        - UserSessionAuth: []
      description: Saves custom pricing defaults for a logged in user in PocketBase. Requires the user's session token matching `userId` (401 otherwise).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, presets]
              properties:
                userId:
                  type: string
                presets:
                  type: object
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: Presets saved.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/user/api-key:
    get:
      tags:
        - User Settings
      summary: Retrieve or generate user API Key
      security:
        - UserSessionAuth: []
      description: |
        Returns the active user-scoped API key for authenticating programmatic upload and scrape requests.
        Requires the user's session token matching `userId` (401 otherwise) — third parties cannot fetch or
        regenerate another user's key.

        Keys are stored as SHA-256 hashes, so the raw `usr_...` value is only returned when it is
        generated: existing keys respond with `apiKey: null` and `keyHidden: true`. Use `POST` to rotate.
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - in: query
          name: userId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key returned (or `keyHidden` when only the hash is stored).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  apiKey:
                    type: string
                    nullable: true
                    description: Raw key — only present at generation time.
                  keyHidden:
                    type: boolean
                    description: True when a hashed key exists and cannot be re-displayed.
                  plan:
                    type: string
                  lastUsed:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API Key access requires a Pro or Enterprise plan.
    post:
      tags:
        - User Settings
      summary: Generate new user API Key
      security:
        - UserSessionAuth: []
      description: |
        Creates a new unique user API key for the account (rotates any existing key). Requires the
        user's session token matching `userId` (401 otherwise). The raw `usr_...` value is returned
        once — only its SHA-256 hash is stored.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: New API key created (raw value returned once).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  apiKey:
                    type: string
                  keyHidden:
                    type: boolean
                  plan:
                    type: string
                  lastUsed:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API Key access requires a Pro or Enterprise plan.

  /api/user/email-preferences:
    post:
      tags:
        - User Settings
      summary: Opt in/out of product-update emails
      security:
        - UserSessionAuth: []
      description: |
        Persists the account's marketing-consent flag (`users.marketing_opt_in`) and syncs the
        Listmonk onboarding-list subscription server-side: opted in → confirmed list membership,
        opted out → unsubscribed. The Listmonk sync is best-effort — the PocketBase consent record
        is authoritative, so the user's choice is never lost when the mail service is down.
        Requires the user's session token matching `userId` (401 otherwise).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, marketingOptIn]
              properties:
                userId:
                  type: string
                marketingOptIn:
                  type: boolean
                  description: True to receive product updates, false to opt out.
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: Preference saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  marketingOptIn:
                    type: boolean
                    example: false
                  listSynced:
                    type: boolean
                    description: True when the Listmonk list subscription was updated.
        '400':
          description: Missing userId or non-boolean marketingOptIn.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/user/history:
    get:
      tags:
        - User Settings
      summary: Fetch user calculation history
      security:
        - UserSessionAuth: []
      description: Returns the user's saved calculation/collection history (most recent first, max 50). Requires the user's session token matching `userId` (401 otherwise).
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
        - in: query
          name: userId
          required: true
          schema:
            type: string
      responses:
        '200':
          description: History items.
        '400':
          description: Missing userId parameter.
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - User Settings
      summary: Save a calculation history entry
      security:
        - UserSessionAuth: []
      description: Appends a calculation history entry for the user. Requires the user's session token matching `userId` (401 otherwise).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, historyItem]
              properties:
                userId:
                  type: string
                historyItem:
                  type: object
      parameters:
        - $ref: '#/components/parameters/UserSessionToken'
      responses:
        '200':
          description: History saved.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/mcp:
    get:
      tags:
        - MCP
      summary: MCP Server Discovery & Capabilities
      description: Returns Model Context Protocol server info, protocol version, and the full catalog of available tools.
      responses:
        '200':
          description: MCP capabilities and tools schema.
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  version:
                    type: string
                  protocolVersion:
                    type: string
                  capabilities:
                    type: object
                  tools:
                    type: array
                    items:
                      type: object
    post:
      tags:
        - MCP
      summary: MCP JSON-RPC 2.0 Request Handler
      description: Handles Model Context Protocol requests over Streamable HTTP (`initialize`, `tools/list`, `tools/call`, `ping`). The credit-gated tools `scrape_model` and `estimate_file` require a valid user API key (or master key) and deduct 1 credit per successful call (401 without a key, 402 when out of credits).
      security:
        - BearerAuth: []
        - ApiKeyHeader: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jsonrpc, method]
              properties:
                jsonrpc:
                  type: string
                  example: "2.0"
                id:
                  oneOf:
                    - type: string
                    - type: integer
                  example: 1
                method:
                  type: string
                  enum: [initialize, tools/list, tools/call, ping, notifications/initialized]
                  example: tools/call
                params:
                  type: object
                  properties:
                    name:
                      type: string
                      example: scrape_model
                    arguments:
                      type: object
      responses:
        '200':
          description: JSON-RPC 2.0 response containing result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jsonrpc:
                    type: string
                    example: "2.0"
                  id:
                    oneOf:
                      - type: string
                      - type: integer
                  result:
                    type: object
        '400':
          description: Invalid JSON-RPC request or missing parameters.
        '401':
          description: Unauthorized. Missing or invalid API key for restricted tools.
        '402':
          description: Payment Required. Insufficient calculation credits.

