openapi: 3.1.0
info:
  title: AI Product Index — read API
  version: 1.0.0
  description: |
    Machine-readable directory where AI products register themselves so AI
    agents can discover them. This is a static site: the read API is a set of
    pre-built JSON documents regenerated on every accepted registration.

    WRITE PATHS (autonomous, via GitHub issues on https://github.com/110kc3/seo):
    "[register] ..." = new listing, "[update] ..." = full replacement by the
    original submitter, "[upgrade] ..." = paid tier change (live up to payment
    verification, which rejects with code payments_not_enabled until rails are
    configured). Bodies are JSON conforming to https://index.kc-it.pl/api/schema.json.
    Full protocol: https://index.kc-it.pl/llms.txt · MCP server: mcp/server.mjs in the repo.

    HEALTH: listing URLs are re-checked weekly; 3 consecutive failures delist
    (strike state at https://index.kc-it.pl/health.json).

    AUDIT: GET /api/score?url=... is free and returns an A-F grade plus per-check
    pass/fail. POST /api/audit is the paid half — the same checks, plus why each
    one failed, fixes ranked by weight, and a paste-ready code snippet per
    failure. Priced per call over x402, and both protocol versions are accepted:
    v2 (PAYMENT-REQUIRED / PAYMENT-SIGNATURE / PAYMENT-RESPONSE) and v1
    (X-PAYMENT / X-PAYMENT-RESPONSE), so clients on either spec can pay.

    TIERS: verified and featured exist in the schema and index ranking; the tier
    field is server-set, so the read API does not change shape when prices
    change. While no receiving address is configured, paid paths reject with code
    payments_not_enabled.
  contact:
    url: https://github.com/110kc3/seo
servers:
  - url: https://index.kc-it.pl
paths:
  /api/index.json:
    get:
      operationId: getRegistry
      summary: Full registry — every listing in one document
      responses:
        "200":
          description: The complete registry.
          content:
            application/json:
              schema:
                type: object
                required: [count, updated, listings]
                properties:
                  count:
                    type: integer
                  updated:
                    type: [string, "null"]
                    description: Date (YYYY-MM-DD) of the most recent listing.
                  listings:
                    type: array
                    items:
                      $ref: "https://index.kc-it.pl/api/schema.json"
  /api/schema.json:
    get:
      operationId: getListingSchema
      summary: JSON Schema for a listing (also the submission schema)
      responses:
        "200":
          description: JSON Schema (draft-07).
          content:
            application/json: {}
  /listings/{slug}.json:
    get:
      operationId: getListing
      summary: One listing
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$"
      responses:
        "200":
          description: The listing.
          content:
            application/json:
              schema:
                $ref: "https://index.kc-it.pl/api/schema.json"
        "404":
          description: Unknown slug.
  /llms-full.txt:
    get:
      operationId: getFullText
      summary: The whole index as plain text (for LLM context windows)
      responses:
        "200":
          description: Plain-text dump of every listing.
          content:
            text/plain: {}
  /api/stats.json:
    get:
      operationId: getStats
      summary: Traffic stats for this index over a 30-day window
      description: |
        Aggregated request counts by inferred client type (ai_agent, ai_crawler,
        browser, script, ...) and by path bucket, plus agent_share. Client type is
        inferred from the self-reported user-agent; no IP addresses or other
        personal data are collected.
      responses:
        "200":
          description: Aggregated counters.
          content:
            application/json: {}
        "503":
          description: Stats not published yet (code stats_not_enabled).
  /api/x402/info:
    get:
      operationId: getPaymentTerms
      summary: Payment terms for the paid endpoint, without provoking a 402
      description: |
        Returns the active rail's network, asset, price in atomic units, payTo
        address and the x402 protocol versions accepted. Everything here also
        appears in the 402 challenge — this just saves a wasted request.
      responses:
        "200":
          description: Active payment terms.
          content:
            application/json: {}
        "503":
          description: Rail not configured (code payments_not_enabled).
  /api/score:
    get:
      operationId: getFreeScore
      summary: Agent-readability score (free, no payment)
      description: |
        Returns an A-F letter grade, the numeric score out of 100, and all 13
        checks with a label and pass/fail — the verdict, free and without signup.

        It deliberately omits what POST /api/audit sells: the per-check reason for
        failure, fixes ranked by weight, and a paste-ready code snippet for each
        failing check.

        Results are cached per URL for an hour and a cached answer is unmetered;
        uncached audits are limited to 20/hour/IP because each fetches the target.
        Private, loopback and link-local targets are refused, same as the paid path.
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
            format: uri
            pattern: "^https?://"
          description: Public http(s) URL to score.
      responses:
        "200":
          description: The score.
          content:
            application/json: {}
        "400":
          description: Missing or rejected url.
        "429":
          description: Rate limited (code rate_limited).
        "502":
          description: The target site could not be read.
  /api/audit:
    post:
      operationId: auditUrl
      summary: Agent-readability audit of one URL (paid, x402)
      description: |
        Scores how readable a site is to AI agents across 13 weighted checks
        (llms.txt presence and shape, llms-full.txt, robots.txt, AI-crawler
        posture, sitemap, schema.org JSON-LD, title/description, Open Graph,
        canonical, machine-readable alternates, agent card, HTTPS) and returns
        ranked next_steps.

        Payment is x402 over HTTP, in either protocol version — one 402 answers
        both. v2 clients read the base64 PAYMENT-REQUIRED header, retry with
        PAYMENT-SIGNATURE, and get the receipt in PAYMENT-RESPONSE. v1 clients
        read the same 402's JSON body (v1 field names: maxAmountRequired, a
        network name, a flat resource URL), retry with X-PAYMENT, and get the
        receipt in X-PAYMENT-RESPONSE. The target URL is validated before any
        charge, so a request that would be rejected is never billed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  pattern: "^https?://"
                  description: Public http(s) URL to audit. Private and local hosts are rejected.
      responses:
        "200":
          description: The audit. PAYMENT-RESPONSE header carries the settlement receipt.
          content:
            application/json:
              schema:
                type: object
                required: [ok, url, score, max_score, grade, checks]
                properties:
                  ok: { type: boolean }
                  url: { type: string }
                  audited_at: { type: string, format: date-time }
                  score: { type: integer }
                  max_score: { type: integer }
                  grade: { type: string }
                  checks:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        weight: { type: integer }
                        pass: { type: boolean }
                        detail: { type: string }
                        fix: { type: string }
                  next_steps:
                    type: array
                    items: { type: string }
        "400":
          description: Invalid JSON, invalid target URL, or malformed payment payload.
        "402":
          description: Payment required, terms mismatched, authorization replayed, or settlement failed.
        "413":
          description: Body too large.
        "502":
          description: Target unreachable, or the payment facilitator did not respond.
        "503":
          description: Payment rails not configured (code payments_not_enabled).
