openapi: 3.1.0
info:
  title: ASXAN Evidence Hub · public read-only API
  version: '0.3.0'
  description: |
    Static-content read-only API surface for AI agent consumption.
    asxan.ai is a static-site educational hub; most "endpoints" below are static-file URLs AI agents can fetch directly.
    Public tier: no authentication, no mutating operations, CORS unrestricted (Cloudflare Pages default).

    v1 authenticated tier (OS-A · 2026-07): /api/v1/nc/{slug} serves the SAME single-source NC data,
    but gated by API key (Bearer or x-api-key) and metered (one usage row per successful call).
    Discover slugs via the free public /api/nc/ index, then fetch with your key. Two faces, one cards.ts.

    v0.2.0 changelog (2026-06-06 · audit synthesis P0-5 fix):
      - NcCardPublic schema expanded: evidence_anchors[] + authorized_claims[] + regulatory.tga (4-market)
      - New shared schemas: EvidenceAnchor + AuthorizedClaim + RegulatoryStatus
      - Schema versions: nc-public-v0.1 / nc-public-v0.2 / nc-public-v0.3 all valid
      - schema_version v0.3 introduces clinical_anchor_count + safety_anchor_count derived fields
      - Frozen single public API version v0.2 across openapi.yaml + ai-plugin.json
      - Deferred to v0.3: 4xx/5xx response schemas + RateLimit-* response headers + MCP discovery
  contact:
    name: ASXAN Editorial
    url: https://asxan.ai/contact/
    email: info@asxan.ai
  license:
    name: Educational reference · attribution-required
    url: https://asxan.ai/about/
  x-built-at: '2026-06-06T16:05:00-07:00'
  x-build-stats-source: /_built_stats.json

servers:
  - url: https://asxan.ai
    description: Production

paths:
  /sitemap-index.xml:
    get:
      summary: XML sitemap index (entry point for full crawl)
      operationId: getSitemapIndex
      responses:
        '200':
          description: sitemapindex XML
          content:
            application/xml:
              schema:
                type: string
  /sitemap-0.xml:
    get:
      summary: URLs of all pages with priority + lastmod + changefreq
      operationId: getSitemap0
      responses:
        '200':
          description: urlset XML
  /llms.txt:
    get:
      summary: AI consumption index · per-section page enumeration
      operationId: getLlmsTxt
      responses:
        '200':
          description: plain-text llms.txt
          content:
            text/plain:
              schema:
                type: string
  /llms-full.txt:
    get:
      summary: AI consumption full · per-page metadata + cluster topology graph
      operationId: getLlmsFullTxt
      responses:
        '200':
          description: plain-text llms-full.txt
  /robots.txt:
    get:
      summary: AI crawler explicit allow + search + disallow paths
      operationId: getRobotsTxt
      responses:
        '200':
          description: robots.txt
  /_built_stats.json:
    get:
      summary: Build-time computed stats (single-source 4-surface anchor)
      operationId: getBuiltStats
      responses:
        '200':
          description: Stats JSON (ingredient_pages · nc_cards · evidence_articles · goal_stacks · lifestyle_guides · verified_pmid_count · built_at)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BuildStats' }
  /blog/rss.xml:
    get:
      summary: RSS 2.0 feed · evidence long-form articles · auto-discover for incremental crawl
      operationId: getBlogRss
      responses:
        '200':
          description: RSS 2.0 XML
          content:
            application/rss+xml:
              schema:
                type: string
  /:
    get:
      summary: Home page · 4-hub graph (Ingredients · Blog · Goals · Lifestyles) + ASXAN ecosystem 3-card
      operationId: getHome
      responses:
        '200':
          description: HTML with Schema.org JSON-LD (WebSite + Organization + mainEntity CollectionPage)
          content:
            text/html:
              schema:
                type: string
  /ingredients/:
    get:
      summary: Ingredient collection (count from build-stats.json · grouped by body-system)
      operationId: getIngredients
      responses:
        '200':
          description: HTML with CollectionPage + ItemList JSON-LD
  /blog/:
    get:
      summary: Evidence Long-Form Articles index (count from build-stats.json · cluster topology graph)
      operationId: getBlog
      responses:
        '200':
          description: HTML with Blog + BlogPosting + isRelatedTo peer cross-link
  /goals/:
    get:
      summary: Health Goal Stacks (count from build-stats.json)
      operationId: getGoals
      responses:
        '200':
          description: HTML with CollectionPage + ItemList
  /lifestyles/:
    get:
      summary: Lifestyle Nutrition Guides (count from build-stats.json)
      operationId: getLifestyles
      responses:
        '200':
          description: HTML with CollectionPage + ItemList
  /faq/:
    get:
      summary: FAQ aggregate · individual Question @id + about (source page link)
      operationId: getFaq
      responses:
        '200':
          description: HTML with FAQPage JSON-LD (mainEntity array · per-Q @id)
  /contact/:
    get:
      summary: Contact page · primary email + 4 inquiry paths (Partnership/BD · Data licensing · Evidence consulting · Press)
      operationId: getContact
      responses:
        '200':
          description: HTML with ContactPage JSON-LD (mainEntity Organization with contactPoint email info@asxan.ai)

  /api/nc/:
    get:
      summary: NutriCodex (NC) public API index · 121 entries
      operationId: getNcIndex
      responses:
        '200':
          description: JSON index with available_cards array + base_url + schema_version
          content:
            application/json:
              schema:
                type: object
                properties:
                  schema_version: { type: string, example: 'nc-public-v0.3' }
                  available_cards: { type: array, items: { type: object, properties: { slug: { type: string }, url: { type: string } } } }
                  total: { type: integer }
  /api/nc/{slug}.json:
    get:
      summary: NC card by slug · 4-axis public knowledge (mechanism + target + use_case + body_system) + evidence_anchors[] + authorized_claims[] + regulatory{fda,efsa,anvisa,tga?}
      operationId: getNcCard
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
          description: NC entry slug · 121 entries available · see /api/nc/ for full list
      responses:
        '200':
          description: NC card JSON
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NcCardPublic' }
        '404':
          description: NC card not found

  # /api/fc/ endpoints removed 2026-07-06 · FormuCube internal-only (public API leaked composition+dosage IP)

  /api/v1/nc/{slug}:
    get:
      summary: 'NC card by slug · authenticated + metered (paid tier · OS-A)'
      description: |
        Same single-source NC data as public /api/nc/{slug}.json, but gated by API key and
        metered (one usage_records row per successful call). Discover slugs via the free
        public /api/nc/ index, then fetch here with your key (Bearer or x-api-key).
      operationId: getNcCardV1
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
          description: 'NC entry slug · see /api/nc/ for full list'
      responses:
        '200':
          description: 'NC card (wrapped · metered)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NcCardV1Response' }
        '401':
          description: 'no-identity (no credential) · invalid-api-key (unknown/revoked key)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
        '403':
          description: 'capability-denied:nc:read (key lacks nc:read capability)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
        '404':
          description: 'not-found (unknown slug)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'API key as Bearer token · Authorization: Bearer axk_live_xxx'
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: 'API key in x-api-key header (alternative to Bearer)'
  schemas:
    Page:
      type: object
      description: Generic static HTML page
      properties:
        url:
          type: string
          format: uri
        html:
          type: string
        schema_org_jsonld_blocks:
          type: array
          description: Embedded JSON-LD scripts (text/json-ld)
          items:
            type: string

    BuildStats:
      type: object
      description: Build-time computed stats · single-source 4-surface anchor (homepage + llms.txt + ai-plugin.json + this OpenAPI)
      required: [schema_version, built_at, source]
      properties:
        schema_version: { type: string, example: 'build-stats-v0.1' }
        built_at: { type: string, format: date-time, description: ISO timestamp from scripts/compute-build-stats.mjs }
        source: { type: string, description: Script path that generated this stats }
        ingredient_pages: { type: integer, description: ls src/pages/ingredients (minus index.astro) }
        evidence_articles: { type: integer, description: ls src/pages/blog (minus feed/rss/atom/jsonfeed/index) }
        goal_stacks: { type: integer, description: ls src/pages/goals (minus index.astro) }
        lifestyle_guides: { type: integer, description: ls src/pages/lifestyles (minus index.astro) }
        nc_cards: { type: integer, description: NC_CARDS entries in src/data/nc/cards.ts }
        verified_pmid_count: { type: integer, description: "Unique PMIDs across all cards.ts evidence_anchors[]" }
        audit_checks: { type: integer, description: Audit framework constant (100) }
        json_ld_blocks_advertised: { type: integer, description: Static advertised JSON-LD blocks count (pending audit-time real count) }

    EvidenceAnchor:
      type: object
      description: PubMed-anchored evidence reference · core 6 fields align with NutriCodex internal merged.json schema (nc_pmid_search.py output) · 2 optional extensions for audit + UX layer
      required: [pmid, title, authors, year, journal]
      properties:
        pmid: { type: string, description: PubMed ID }
        title: { type: string }
        authors: { type: array, items: { type: string } }
        year: { type: string, description: Publication year as string (preserves leading zeros) }
        journal: { type: string }
        pub_type:
          type: string
          description: e.g. RCT · Cochrane SR · Meta-analysis · Review · Observational · Mechanistic
        outcome_tag:
          type: string
          enum: [positive, negative, neutral, mechanistic, review, safety-signal]
          description: Trial outcome direction · safety-signal flags safety-context anchors (not efficacy indicator)
        role_note:
          type: string
          description: Why this anchor was chosen / caveats / safety boundary context

    AuthorizedClaim:
      type: object
      description: Regulatory-authorized verbatim health claim
      required: [market, framework_reference, claim_verbatim]
      properties:
        market:
          type: string
          enum: [FDA, EFSA, ANVISA, TGA]
        framework_reference:
          type: string
          description: Regulatory citation · e.g. 'Reg 432/2012' · 'IN 28/2018' · '21 CFR 101.93'
        claim_verbatim:
          type: string
          description: Verbatim claim text · character-level second-pass verified against official source
        authorization_date:
          type: string
          format: date
        conditions:
          type: string
          description: Conditions of use · e.g. SOURCE OF [VITAMIN/MINERAL] threshold per Reg 1924/2006

    RegulatoryStatus:
      type: object
      description: Per-market regulatory narrative · structured equivalent of the freeform regulatory{} block
      properties:
        fda:
          type: string
          description: United States · DSHEA structure/function claims + 21 CFR Part 101 Subpart E SSA + Qualified Health Claims (QHC) status
        efsa:
          type: string
          description: European Union · Reg 432/2012 Article 13(1) function claims + Article 14 disease-risk-reduction status + NDA Panel opinion citation
        anvisa:
          type: string
          description: Brasil · IN 28/2018 alegações autorizadas + RDC 243/2018 dietary supplements main law + RDC 24/2010 propaganda
        tga:
          type: string
          description: Australia · TGA Therapeutic Goods (Permissible Indications) Determination · 4-market scope completion (deferred for cards predating v0.3)

    NcCardPublic:
      type: object
      description: NutriCodex (NC) public card · 4-axis (mechanism + target + use_case + body_system) + evidence anchors + authorized claims + 4-market regulatory · IP hidden
      required: [slug, name, mechanism, target, use_case, body_system, evidence_tier, evidence_anchor_pmid_count, schema_version]
      properties:
        slug: { type: string }
        name: { type: string }
        alternate_names: { type: array, items: { type: string } }
        mechanism: { type: array, items: { type: string } }
        target: { type: array, items: { type: string } }
        use_case: { type: array, items: { type: string } }
        body_system: { type: array, items: { type: string } }
        evidence_tier: { type: string, enum: [A, B, C] }
        evidence_anchor_pmid_count: { type: integer, description: Total anchor PMID count (clinical + safety) }
        clinical_anchor_count:
          type: integer
          description: v0.3+ · anchors with outcome_tag ∈ {positive, negative, neutral, review, mechanistic}
        safety_anchor_count:
          type: integer
          description: v0.3+ · anchors with outcome_tag === safety-signal · these are context boundaries · NOT efficacy reduction signals
        evidence_anchors:
          type: array
          description: PubMed-anchored evidence array · core 6 fields + 2 optional audit/UX extensions
          items: { $ref: '#/components/schemas/EvidenceAnchor' }
        evidence_long_form_url: { type: string, format: uri }
        dosage_range: { type: string }
        safety_notes: { type: string }
        regulatory: { $ref: '#/components/schemas/RegulatoryStatus' }
        authorized_claims:
          type: array
          description: Verbatim authorized claims across markets (FDA / EFSA / ANVISA / TGA)
          items: { $ref: '#/components/schemas/AuthorizedClaim' }
        ingredient_hub_url: { type: string, format: uri }
        canonical_ingredient_slug: { type: string, description: "Present when the ingredient page lives under a different slug than the card (alias) · e.g. card 'casein' → page 'casein-protein'" }
        related_goal_slugs: { type: array, items: { type: string } }
        related_lifestyle_slugs: { type: array, items: { type: string } }
        last_evidence_review: { type: string, format: date }
        schema_version:
          type: string
          enum: ['nc-public-v0.1', 'nc-public-v0.2', 'nc-public-v0.3']
          description: NC public schema version · v0.3 adds clinical_anchor_count + safety_anchor_count derived fields

    NcCardV1Response:
      type: object
      description: v1 authenticated tier response envelope · wraps the same NcCardPublic data + request/tenant/plan metadata
      required: [ok, requestId, tenant, data]
      properties:
        ok: { type: boolean, example: true }
        requestId: { type: string, format: uuid, description: 'per-request id · joins usage_records for billing/audit' }
        tenant: { type: string, description: 'resolved tenant_slug from API key' }
        plan: { type: string, nullable: true, enum: [free, starter, growth, pro, enterprise] }
        data: { $ref: '#/components/schemas/NcCardPublic' }

    ApiError:
      type: object
      description: Error envelope (v1 tier) · ok:false + machine-readable reason string
      required: [ok, error]
      properties:
        ok: { type: boolean, example: false }
        error:
          type: string
          description: 'no-identity(401) · invalid-api-key(401) · capability-denied:nc:read(403) · not-found(404) · internal(500)'

    # FcFormulaPublic schema removed 2026-07-06 · FormuCube internal-only
