openapi: 3.0.3
info:
  title: ProofFetch Lite
  version: "1.0.0"
  description: |
    ProofFetch Lite — fetch a public URL and return content proof for agents.

    **Tag:** ProofFetch Lite (v1 — no screenshots; avoids paid browser/proxy).

    **Billing note for Agent Wonderland:** Only HTTP 200 with `"ok": true` is a successful
    billable result. Failures return HTTP 4xx/5xx with `"ok": false` and `"chargeable": false`
    so Wonderland should **not** charge the consumer (refund / no capture).

    SSRF guards: blocks private/reserved IPs, localhost, link-local, cloud metadata,
    http(s) only, 15s timeout, 2MB max body.
  contact:
    name: Kyle Stepp / ProofFetch
servers:
  - url: https://proofetch.hardenbranch.workers.dev
    description: Production (set after deploy)
  - url: http://127.0.0.1:8787
    description: Local Deno
paths:
  /health:
    get:
      operationId: health
      summary: Health check
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
  /v1/proof:
    post:
      operationId: createProof
      summary: Fetch URL and return content proof
      description: |
        Fetches the given public http(s) URL with SSRF protections and returns
        ISO timestamp, SHA-256 of the raw body, extracted title, cleaned markdown,
        HTTP status, and final URL after redirects.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProofRequest"
            example:
              url: https://example.com
      responses:
        "200":
          description: Successful proof (billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofSuccess"
        "400":
          description: Invalid input or SSRF blocked (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "413":
          description: Body too large (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "502":
          description: Upstream fetch/DNS failure (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "504":
          description: Timeout (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
  /v1/sitecrawl:
    post:
      operationId: siteCrawl
      summary: SiteCrawl — polite same-origin crawl with per-page proof receipts
      description: |
        Reads robots.txt (RFC 9309; honors Disallow/Allow and Crawl-delay for "ProofFetch" or "*"),
        uses sitemap.xml when present (else a shallow same-origin link BFS, depth <= 3), and fetches
        politely (concurrency 3, 300ms delay, User-Agent "ProofFetch/1.0 (+contact ktstepp85@gmail.com)").
        Every request goes through the same SSRF guards as /v1/proof. Per-page markdown is capped at 50k chars;
        the crawl stops scheduling new pages after ~70s.

        **Billing:** $0.002 per page that returned HTTP 2xx (`billing.pages_billable`), minimum $0.01.
        If no page returns 2xx, the call fails (HTTP 502, `chargeable: false`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiteCrawlRequest"
            example:
              url: https://docs.deno.com/runtime/
              max_pages: 5
      responses:
        "200":
          description: Crawl completed with at least one 2xx page (billable per page)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiteCrawlSuccess"
        "400":
          description: Invalid input or SSRF blocked (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "403":
          description: Start URL disallowed by robots.txt (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "502":
          description: No page returned 2xx / upstream failure (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "503":
          description: robots.txt unreachable (5xx) — crawling not permitted (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
  /v1/secsection:
    post:
      operationId: secSection
      summary: SecSection — one Item section from an SEC filing as markdown
      description: |
        Resolves ticker → CIK (SEC company_tickers.json, cached 24h), reads data.sec.gov submissions,
        picks the latest (or `filing_index`-th) filing of `form`, fetches its primary document from
        www.sec.gov/Archives, and extracts the requested Item (e.g. 1A Risk Factors, 7 MD&A, 1 Business,
        8-K 2.02) using heading heuristics (longest span between "Item X." headings, so table-of-contents
        entries and running page headers are skipped). S-1 sections are located by name
        (Risk Factors, Business, MD&A, Use of Proceeds, ...). SEC requests are throttled to <= 5 req/s
        with a declared User-Agent.

        **Billing:** $0.02 per successful section. Errors are NOT billable.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SecSectionRequest"
            example:
              ticker: AAPL
              form: 10-K
              section: 1A
      responses:
        "200":
          description: Section extracted (billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecSectionSuccess"
        "400":
          description: Invalid input (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "404":
          description: Unknown ticker/CIK or no such filing (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "422":
          description: Section not found / empty in the filing (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "502":
          description: SEC upstream error (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
        "503":
          description: SEC rate-limited / blocked; retry later (NOT billable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProofError"
components:
  schemas:
    ProofRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: Public http(s) URL to fetch
    ProofSuccess:
      type: object
      required:
        - ok
        - fetched_at
        - content_sha256
        - text_markdown
        - status_code
        - final_url
      properties:
        ok:
          type: boolean
          enum: [true]
        product:
          type: string
          example: ProofFetch Lite
        fetched_at:
          type: string
          format: date-time
          description: ISO-8601 UTC timestamp when the fetch started
        content_sha256:
          type: string
          description: Hex SHA-256 of the raw response body bytes
        title:
          type: string
          nullable: true
          description: Document title when HTML
        text_markdown:
          type: string
          description: Cleaned markdown (or text/binary note)
        status_code:
          type: integer
          description: Final HTTP status from the target
        final_url:
          type: string
          format: uri
          description: URL after following redirects
    SiteCrawlRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: Public http(s) start URL
        max_pages:
          type: integer
          minimum: 1
          maximum: 50
          default: 20
          description: Pages to fetch (values above 50 are capped to 50)
        same_origin:
          type: boolean
          default: true
          description: Only true is supported (false returns 400)
    SiteCrawlPage:
      type: object
      properties:
        url: { type: string, format: uri }
        final_url: { type: string, format: uri }
        status: { type: integer, nullable: true, description: "Upstream HTTP status (null on fetch error)" }
        title: { type: string, nullable: true }
        text_markdown: { type: string }
        content_sha256: { type: string, description: "Hex SHA-256 of the raw response body" }
        fetched_at: { type: string, format: date-time }
        text_truncated: { type: boolean }
        note: { type: string }
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
    SiteCrawlSuccess:
      type: object
      required: [ok, pages, totals, crawl_sha256]
      properties:
        ok: { type: boolean, enum: [true] }
        product: { type: string, example: SiteCrawl }
        start_url: { type: string }
        origin: { type: string }
        same_origin: { type: boolean }
        discovery: { type: string, enum: [sitemap, bfs, sitemap+bfs] }
        robots: { type: object }
        politeness: { type: object }
        totals:
          type: object
          properties:
            max_pages: { type: integer }
            pages_returned: { type: integer }
            pages_ok: { type: integer }
            pages_non_2xx: { type: integer }
            pages_error: { type: integer }
            skipped_robots: { type: integer }
            skipped_offsite: { type: integer }
            markdown_chars: { type: integer }
            http_requests: { type: integer }
            elapsed_ms: { type: integer }
            stopped_by_deadline: { type: boolean }
        crawl_sha256:
          type: string
          description: SHA-256 over sorted lines "<url> <content_sha256>" joined by newline
        started_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time }
        billing:
          type: object
          properties:
            unit_price_usd: { type: number }
            min_usd: { type: number }
            pages_billable: { type: integer }
            price_usd: { type: number }
        pages:
          type: array
          items:
            $ref: "#/components/schemas/SiteCrawlPage"
    SecSectionRequest:
      type: object
      required: [section]
      properties:
        ticker: { type: string, description: "Ticker (e.g. AAPL). Provide ticker or cik." }
        cik: { type: string, description: "SEC CIK (digits)" }
        form: { type: string, enum: ["10-K", "10-Q", "8-K", "S-1"], default: "10-K" }
        section: { type: string, description: 'Item code ("1A", "7", "1", "2.02") or name ("Risk Factors", "MD&A")' }
        filing_index: { type: integer, minimum: 0, maximum: 50, default: 0, description: "0 = latest" }
        part: { type: string, enum: [I, II, III, IV], description: "Optional, mainly for 10-Q" }
    SecSectionSuccess:
      type: object
      required: [ok, company_name, cik, form, filing_date, accession, filing_url, section, text_markdown, content_sha256, fetched_at]
      properties:
        ok: { type: boolean, enum: [true] }
        product: { type: string, example: SecSection }
        company_name: { type: string }
        cik: { type: string, description: "10-digit zero-padded CIK" }
        ticker: { type: string, nullable: true }
        form: { type: string }
        filing_date: { type: string, format: date }
        report_date: { type: string, nullable: true }
        accession: { type: string }
        filing_url: { type: string, format: uri }
        filing_index_url: { type: string, format: uri }
        section: { type: string }
        section_title: { type: string, nullable: true }
        part: { type: string, nullable: true }
        text_markdown: { type: string }
        chars: { type: integer }
        truncated: { type: boolean }
        content_sha256: { type: string, description: "SHA-256 of text_markdown (UTF-8)" }
        document_sha256: { type: string, description: "SHA-256 of the raw primary document bytes" }
        fetched_at: { type: string, format: date-time }
        billing:
          type: object
          properties:
            price_usd: { type: number }
    ProofError:
      type: object
      required: [ok, error, chargeable]
      properties:
        ok:
          type: boolean
          enum: [false]
        product:
          type: string
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            detail:
              type: string
        chargeable:
          type: boolean
          enum: [false]
          description: Always false — Wonderland must not charge on errors
