openapi: 3.0.3
info:
  title: Xcatcher Agent API
  version: 3.0.0
  description: |
    Fetch recent public X (Twitter) posts from named handles. Agents can use
    accountless x402 v2 pay-per-crawl on Base or an API-key points account.
    Live responses and payment headers are authoritative.
  contact: {url: "https://xcatcher.top/docs/"}
  x-agent-skill: "https://xcatcher.top/skills/xcatcher/SKILL.md"
externalDocs:
  description: Agent integration guide
  url: "https://xcatcher.top/docs/"
servers:
  - url: "https://xcatcher.top"
tags:
  - {name: Discovery}
  - {name: Direct x402}
  - {name: Authentication}
  - {name: Keys}
  - {name: Tasks}
  - {name: Compatibility payments}
  - {name: MCP}

components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: xc_live_...
    TaskToken:
      type: http
      scheme: bearer
      bearerFormat: xtask_...
    PaymentSignature:
      type: apiKey
      in: header
      name: PAYMENT-SIGNATURE
      description: Base64 JSON x402 payment payload generated by a wallet/client.
  parameters:
    TaskId:
      name: task_id
      in: path
      required: true
      schema: {type: integer, minimum: 1}
    ResultLimit:
      name: limit
      in: query
      schema: {type: integer, minimum: 1, maximum: 200, default: 50}
    ResultOffset:
      name: offset
      in: query
      schema: {type: integer, minimum: 0, default: 0}
  headers:
    PaymentRequired:
      description: Base64 JSON PaymentRequired challenge.
      schema: {type: string}
    PaymentResponse:
      description: Base64 JSON settlement response.
      schema: {type: string}
    RequestId:
      description: Stable request correlation ID.
      schema: {type: string}
  schemas:
    Error:
      type: object
      required: [success, error_code, msg, error]
      properties:
        success: {type: boolean, example: false}
        error_code: {type: string, example: VALIDATION_ERROR}
        msg: {type: string}
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: {type: string}
            message: {type: string}
            request_id: {type: string}
            details: {type: object, additionalProperties: true}
    AuthRequest:
      type: object
      additionalProperties: false
      required: [username, password]
      properties:
        username: {type: string, minLength: 3, maxLength: 32, pattern: "^[A-Za-z0-9_.-]+$"}
        password: {type: string, minLength: 10, maxLength: 128, format: password}
    AuthResponse:
      type: object
      required: [success, username, points, api_key, key_id, scopes]
      properties:
        success: {type: boolean, example: true}
        username: {type: string}
        points: {type: integer}
        api_key: {type: string, writeOnly: true, example: xc_live_xxx}
        key_id: {type: integer}
        scopes: {type: array, items: {type: string}}
    CrawlRequest:
      type: object
      additionalProperties: false
      required: [users]
      properties:
        users:
          type: array
          minItems: 1
          maxItems: 500
          items:
            type: string
            description: Handle, @handle, or x.com/twitter.com profile URL.
        mode: {type: string, enum: [normal, deep], default: normal}
        idempotency_key:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9._:-]+$"
          description: API-key path only. Reuse for retries of identical intent.
    PaymentRequirement:
      type: object
      required: [scheme, network, amount, asset, payTo, maxTimeoutSeconds]
      additionalProperties: true
      properties:
        scheme: {type: string, example: exact}
        network: {type: string, example: "eip155:8453"}
        amount: {type: string, example: "20000"}
        asset: {type: string, example: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"}
        payTo: {type: string}
        maxTimeoutSeconds: {type: integer, example: 120}
        extra: {type: object, additionalProperties: true}
    PaymentRequired:
      type: object
      required: [x402Version, resource, accepts, extensions]
      properties:
        x402Version: {type: integer, enum: [2]}
        error: {type: string}
        resource:
          type: object
          required: [url, description, mimeType]
          properties:
            url: {type: string, format: uri}
            description: {type: string}
            mimeType: {type: string}
            serviceName: {type: string}
        accepts:
          type: array
          minItems: 1
          items: {$ref: "#/components/schemas/PaymentRequirement"}
        extensions: {type: object, additionalProperties: true}
    Task:
      type: object
      required: [task_id, status, status_code, lines, cost_points, has_result]
      properties:
        task_id: {type: integer}
        status: {type: string, enum: [queued, processing, done, failed, cancelled]}
        status_code: {type: integer}
        task_mode: {type: string, enum: [normal, deep], nullable: true}
        lines: {type: integer}
        cost_points: {type: integer}
        submit_time: {type: string, format: date-time, nullable: true}
        updated_time: {type: string, format: date-time, nullable: true}
        has_result: {type: boolean}
        result_meta: {type: object, additionalProperties: true}
        results_url: {type: string, nullable: true}
        download_url: {type: string, nullable: true}
        error: {type: object, nullable: true, additionalProperties: true}
    ResultPage:
      type: object
      required: [success, task_id, rows, total, offset, limit]
      properties:
        success: {type: boolean, example: true}
        task_id: {type: integer}
        rows:
          type: array
          items:
            type: object
            properties:
              username: {type: string}
              tweet_time: {type: string, nullable: true}
              content: {type: string}
              tweet_link: {type: string, nullable: true}
        total: {type: integer}
        offset: {type: integer}
        limit: {type: integer}
        next_offset: {type: integer, nullable: true}
        result_meta: {type: object, additionalProperties: true}
    KeyCreate:
      type: object
      additionalProperties: false
      properties:
        name: {type: string, minLength: 1, maxLength: 64, default: agent-key}
        scopes:
          type: array
          uniqueItems: true
          items:
            type: string
            enum: ["account:read", "tasks:read", "tasks:write", "results:read", "payments:write", "keys:manage"]
        expires_in_days: {type: integer, minimum: 1, maximum: 365, default: 90}
    AnyJson: {type: object, additionalProperties: true}

paths:
  /mcp/health:
    get:
      tags: [Discovery]
      operationId: getMcpHealth
      summary: Public MCP health check
      responses:
        "200": {description: Healthy, content: {text/plain: {schema: {type: string, example: ok}}}}
  /mcp/:
    post:
      tags: [MCP]
      operationId: mcpJsonRpc
      summary: Remote MCP Streamable HTTP endpoint
      description: Discovery and direct x402 tools are public. Account tools inspect the optional Bearer API key.
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}
      responses:
        "200": {description: JSON-RPC response, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}

  /api/v1/x402/crawl:
    post:
      tags: [Direct x402]
      operationId: directX402Crawl
      summary: Get a payment challenge or submit payment and create an accountless crawl
      description: Without PAYMENT-SIGNATURE returns 402. With a valid x402 v2 exact EIP-3009 signature, settles once and returns task-scoped access.
      security:
        - {}
        - PaymentSignature: []
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/CrawlRequest"}}}
      responses:
        "201":
          description: Payment settled and task created or recovered
          headers:
            PAYMENT-RESPONSE: {$ref: "#/components/headers/PaymentResponse"}
            X-Request-ID: {$ref: "#/components/headers/RequestId"}
          content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}
        "402":
          description: x402 v2 payment challenge or unsuccessful settlement
          headers:
            PAYMENT-REQUIRED: {$ref: "#/components/headers/PaymentRequired"}
            PAYMENT-RESPONSE: {$ref: "#/components/headers/PaymentResponse"}
            X-Request-ID: {$ref: "#/components/headers/RequestId"}
          content: {application/json: {schema: {$ref: "#/components/schemas/PaymentRequired"}}}
        "409": {description: Quote/signature/request mismatch, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "429": {description: Rate limited, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}

  /api/v1/x402/tasks/{task_id}:
    get:
      tags: [Direct x402]
      operationId: getDirectTask
      summary: Get a paid accountless task
      security: [{TaskToken: []}]
      parameters: [{$ref: "#/components/parameters/TaskId"}]
      responses:
        "200": {description: Task state, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "401": {description: Invalid or expired task token, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/x402/tasks/{task_id}/results:
    get:
      tags: [Direct x402]
      operationId: getDirectResults
      summary: Get structured paid-task results
      security: [{TaskToken: []}]
      parameters:
        - {$ref: "#/components/parameters/TaskId"}
        - {$ref: "#/components/parameters/ResultLimit"}
        - {$ref: "#/components/parameters/ResultOffset"}
      responses:
        "200": {description: Result page, content: {application/json: {schema: {$ref: "#/components/schemas/ResultPage"}}}}
        "401": {description: Invalid or expired task token, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "409": {description: Result not ready, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/x402/tasks/{task_id}/download:
    get:
      tags: [Direct x402]
      operationId: downloadDirectResult
      summary: Download paid-task XLSX
      security: [{TaskToken: []}]
      parameters: [{$ref: "#/components/parameters/TaskId"}]
      responses:
        "200": {description: XLSX stream, content: {application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: {schema: {type: string, format: binary}}}}
        "401": {description: Invalid or expired task token, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "409": {description: Result not ready, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}

  /api/v1/auth/register:
    post:
      tags: [Authentication]
      operationId: register
      summary: Create a trial account and API key
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/AuthRequest"}}}}
      responses:
        "201": {description: Account created, content: {application/json: {schema: {$ref: "#/components/schemas/AuthResponse"}}}}
        "400": {description: Invalid input, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "409": {description: Username already exists, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "429": {description: Registration rate limited, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/auth/login:
    post:
      tags: [Authentication]
      operationId: login
      summary: Issue an independently revocable API key
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/AuthRequest"}}}}
      responses:
        "200": {description: Key issued, content: {application/json: {schema: {$ref: "#/components/schemas/AuthResponse"}}}}
        "401": {description: Invalid credentials, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "409": {description: Active-key limit reached, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}

  /api/v1/me:
    get:
      tags: [Authentication]
      operationId: getAccount
      summary: Get account, points, and current key scopes
      security: [{ApiKey: []}]
      responses:
        "200": {description: Account data, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "401": {description: Invalid key or missing scope, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/keys:
    get:
      tags: [Keys]
      operationId: listApiKeys
      summary: List key metadata (never secrets)
      security: [{ApiKey: []}]
      responses:
        "200": {description: Key metadata, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
    post:
      tags: [Keys]
      operationId: createApiKey
      summary: Create a scoped expiring key
      security: [{ApiKey: []}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/KeyCreate"}}}}
      responses:
        "201": {description: New key secret shown once, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "409": {description: Active-key limit reached, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/keys/{key_id}:
    delete:
      tags: [Keys]
      operationId: revokeApiKey
      summary: Revoke one API key
      security: [{ApiKey: []}]
      parameters:
        - {name: key_id, in: path, required: true, schema: {type: integer, minimum: 1}}
      responses:
        "200": {description: Revoked, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "404": {description: Key not found, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}

  /api/v1/tasks:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: List recent owned tasks
      security: [{ApiKey: []}]
      parameters:
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
        - {name: before_id, in: query, schema: {type: integer, minimum: 1}}
      responses:
        "200": {description: Cursor page, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Create an idempotent points-account crawl
      security: [{ApiKey: []}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/CrawlRequest"}}}}
      responses:
        "200": {description: Created or idempotently replayed, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "400": {description: Invalid input, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
        "402":
          description: Insufficient points
          headers: {PAYMENT-REQUIRED: {$ref: "#/components/headers/PaymentRequired"}}
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "409": {description: Idempotency conflict, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/tasks/{task_id}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Get safe task state
      security: [{ApiKey: []}]
      parameters: [{$ref: "#/components/parameters/TaskId"}]
      responses:
        "200": {description: Task, content: {application/json: {schema: {$ref: "#/components/schemas/Task"}}}}
        "404": {description: Not found for this account, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/tasks/{task_id}/results:
    get:
      tags: [Tasks]
      operationId: getTaskResults
      summary: Get native structured result rows
      security: [{ApiKey: []}]
      parameters:
        - {$ref: "#/components/parameters/TaskId"}
        - {$ref: "#/components/parameters/ResultLimit"}
        - {$ref: "#/components/parameters/ResultOffset"}
      responses:
        "200": {description: Result page, content: {application/json: {schema: {$ref: "#/components/schemas/ResultPage"}}}}
        "409": {description: Result not ready, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/tasks/{task_id}/download:
    get:
      tags: [Tasks]
      operationId: downloadTaskResult
      summary: Download complete XLSX
      security: [{ApiKey: []}]
      parameters: [{$ref: "#/components/parameters/TaskId"}]
      responses:
        "200": {description: XLSX stream, content: {application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: {schema: {type: string, format: binary}}}}
        "409": {description: Result not ready, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
  /api/v1/tasks/{task_id}/cancel:
    post:
      tags: [Tasks]
      operationId: cancelTask
      summary: Cancel a queued task and refund points
      security: [{ApiKey: []}]
      parameters: [{$ref: "#/components/parameters/TaskId"}]
      responses:
        "200": {description: Cancelled, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "400": {description: Task is no longer queued, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}

  /api/v1/x402/quote:
    get:
      tags: [Compatibility payments]
      operationId: createPointsQuote
      summary: Create an existing-account points quote
      parameters:
        - {name: points, in: query, required: true, schema: {type: integer, minimum: 1, maximum: 200000}}
      responses:
        "200": {description: Live compatibility requirements, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
  /api/v1/x402/topup:
    post:
      tags: [Compatibility payments]
      operationId: topUpAccount
      summary: Verify a compatibility payment and credit points
      security: [{ApiKey: [], PaymentSignature: []}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, required: [quote_id], properties: {quote_id: {type: string}}}}}}
      responses:
        "200": {description: Points credited, content: {application/json: {schema: {$ref: "#/components/schemas/AnyJson"}}}}
        "402": {description: Payment required or invalid, headers: {PAYMENT-REQUIRED: {$ref: "#/components/headers/PaymentRequired"}}, content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}}
