openapi: 3.0.3
info:
  title: AgentRoot Registry API
  description: |
    DNS-based discovery protocol for AI capabilities. Public REST API for searching, resolving, and submitting domains that publish `_agentroot` TXT records and `.well-known/agentroot.json` manifests.
  version: "3.0.0"
  contact:
    name: AgentRoot
    url: https://agentroot.io
  license:
    name: See repo
    url: https://github.com/d3-inc/agentroot
servers:
  - url: https://agentroot.io
    description: Production
paths:
  /api/health:
    get:
      summary: DB connectivity check
      operationId: health
      responses:
        '200':
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
  /api/discover:
    get:
      summary: Unified search (legacy tables — agents + skills)
      operationId: discover
      parameters:
        - in: query
          name: q
          schema: { type: string }
          description: Free-text query
        - in: query
          name: type
          schema: { type: string, enum: [agent, skill, mcp, a2a] }
      responses:
        '200':
          description: Matched records
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DiscoverResponse' }
  /api/records:
    get:
      summary: Cross-manifest record search (new tables)
      operationId: searchRecords
      parameters:
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: capability
          schema: { type: string }
          description: Filter by capability substring
        - in: query
          name: payment
          schema: { type: string }
          description: Comma-separated payment methods (e.g. x402,mpp)
        - in: query
          name: protocol
          schema: { type: string }
          description: Filter by raw_record.protocols
        - in: query
          name: method
          schema: { type: string }
          description: Filter by raw_record.methods
        - in: query
          name: asset
          schema: { type: string }
          description: Filter by raw_record.assets
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, default: 20, maximum: 100 }
      responses:
        '200':
          description: Paginated records
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaginatedRecords' }
  /api/manifests:
    get:
      summary: List manifests with search, type filter, pagination
      operationId: listManifests
      parameters:
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Paginated manifest list. Each manifest includes a `record_counts` map keyed by record type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  manifests:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Manifest'
                        - type: object
                          properties:
                            record_counts:
                              type: object
                              additionalProperties: { type: integer }
                  total: { type: integer }
                  page: { type: integer }
                  limit: { type: integer }
                  pages: { type: integer }
  /api/manifests/{domain}:
    get:
      summary: Manifest detail with records and subdomains
      operationId: getManifest
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Manifest
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ManifestDetail' }
        '404':
          description: Manifest not found
  /api/manifests/{domain}/records/{recordId}:
    get:
      summary: Single record under a manifest
      description: Returns a single record along with summary metadata for the manifest that owns it.
      operationId: getManifestRecord
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
        - in: path
          name: recordId
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Record with parent manifest summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  record: { $ref: '#/components/schemas/Record' }
                  manifest:
                    type: object
                    nullable: true
                    properties:
                      domain: { type: string }
                      status: { type: string }
                      manifest_url: { type: string, nullable: true }
                      protocol_version: { type: string }
        '404':
          description: Record not found
  /api/manifests/{domain}/verify:
    get:
      summary: Re-run indexing against live DNS
      operationId: verifyManifest
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Verification result
        '400':
          description: Invalid domain
  /api/manifests/{domain}/badge:
    get:
      summary: SVG badge for the manifest
      operationId: manifestBadge
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: SVG image
          content:
            image/svg+xml: { schema: { type: string } }
  /api/agents/{domain}:
    get:
      summary: Get a single agent by domain
      operationId: getAgent
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Agent
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent: { $ref: '#/components/schemas/Agent' }
        '404':
          description: Agent not found
  /api/skills/{domain}:
    get:
      summary: Get the skill domain with all skill items
      operationId: getSkillDomain
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Skill domain
          content:
            application/json:
              schema:
                type: object
                properties:
                  skill: { $ref: '#/components/schemas/SkillDomain' }
        '404':
          description: Skill not found
  /api/find-skills:
    get:
      summary: Search individual skill items by query
      description: Returns skill items across all domains. Omitting `q` returns every indexed skill item.
      operationId: findSkills
      parameters:
        - in: query
          name: q
          schema: { type: string }
      responses:
        '200':
          description: Skill items
          content:
            application/json:
              schema:
                type: object
                properties:
                  skills:
                    type: array
                    items: { $ref: '#/components/schemas/SkillItem' }
                  count: { type: integer }
  /api/domains/{rootDomain}:
    get:
      summary: Agents and skills published under a root domain
      operationId: getDomain
      parameters:
        - in: path
          name: rootDomain
          required: true
          schema: { type: string }
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Aggregated agents and skill items under the root domain
          content:
            application/json:
              schema:
                type: object
                properties:
                  root_domain: { type: string }
                  agents:
                    type: array
                    items: { $ref: '#/components/schemas/Agent' }
                  skill_items:
                    type: array
                    items: { $ref: '#/components/schemas/SkillItem' }
                  total: { type: integer }
                  total_agents: { type: integer }
                  total_skills: { type: integer }
                  page: { type: integer }
                  limit: { type: integer }
                  totalPages: { type: integer }
        '404':
          description: No records found for this domain
  /api/collections:
    get:
      summary: List curated collections with item counts
      operationId: listCollections
      parameters:
        - in: query
          name: limit
          schema: { type: integer }
        - in: query
          name: offset
          schema: { type: integer }
      responses:
        '200':
          description: Collection list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        slug: { type: string }
                        name: { type: string }
                        description: { type: string, nullable: true }
                        item_count: { type: integer }
                  total: { type: integer }
        '400':
          description: Invalid pagination params
  /api/collections/{slug}:
    get:
      summary: Collection detail with items
      operationId: getCollection
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer }
        - in: query
          name: offset
          schema: { type: integer }
      responses:
        '200':
          description: Collection with paginated items
          content:
            application/json:
              schema:
                type: object
                properties:
                  slug: { type: string }
                  name: { type: string }
                  description: { type: string, nullable: true }
                  total: { type: integer }
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/CollectionItem' }
        '400':
          description: Invalid slug or pagination params
        '404':
          description: Collection not found
  /api/submit:
    post:
      summary: Submit a domain for verification
      operationId: submit
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: "stripe.com" }
      responses:
        '200':
          description: Indexing result
        '202':
          description: Queued
        '400':
          description: Invalid input
  /api/stats:
    get:
      summary: Registry stats
      operationId: stats
      responses:
        '200':
          description: Stats payload
  /manifest/{domain}:
    get:
      summary: HTML/markdown rendering of a manifest (SEO-injected)
      description: |
        Returns the SPA shell with route-specific meta tags + JSON-LD Dataset injected (default), or a markdown rendering when `Accept: text/markdown` is set. This is the user-facing page route, not the JSON API — use `/api/manifests/{domain}` for the JSON payload.
      operationId: getManifestPage
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
      responses:
        '200':
          description: HTML (default) or Markdown (with Accept negotiation)
          content:
            text/html:
              schema: { type: string }
            text/markdown:
              schema: { type: string }
              description: "Returned when `Accept: text/markdown` is set."
  /manifest/{domain}/{recordId}:
    get:
      summary: HTML/markdown rendering of a record under a manifest (SEO-injected)
      description: |
        Returns the SPA shell with route-specific meta tags + JSON-LD SoftwareApplication injected (default), or a markdown rendering when `Accept: text/markdown` is set. Use `/api/manifests/{domain}/records/{recordId}` for the JSON payload.
      operationId: getManifestRecordPage
      parameters:
        - in: path
          name: domain
          required: true
          schema: { type: string }
        - in: path
          name: recordId
          required: true
          schema: { type: string }
      responses:
        '200':
          description: HTML (default) or Markdown (with Accept negotiation)
          content:
            text/html:
              schema: { type: string }
            text/markdown:
              schema: { type: string }
              description: "Returned when `Accept: text/markdown` is set."
components:
  schemas:
    Manifest:
      type: object
      properties:
        domain: { type: string }
        status: { type: string, enum: [active, pending, failed, unverified] }
        manifest_url: { type: string, nullable: true }
        protocol_version: { type: string, example: ar1 }
    ManifestDetail:
      type: object
      properties:
        manifest:
          allOf:
            - $ref: '#/components/schemas/Manifest'
            - type: object
              properties:
                subdomains:
                  type: array
                  items:
                    type: object
                    properties:
                      domain: { type: string }
                      status: { type: string }
                records:
                  type: array
                  items: { $ref: '#/components/schemas/Record' }
    Record:
      type: object
      properties:
        record_id: { type: string }
        type: { type: string, description: 'Record type. Canonical values: agent, mcp, skill, a2a. Custom types (e.g. payment, mpp) are allowed.' }
        name: { type: string }
        description: { type: string }
        endpoint: { type: string, nullable: true }
        capabilities: { type: string, nullable: true }
        protocol: { type: string, nullable: true }
        transport: { type: string, nullable: true }
        auth: { type: object, nullable: true }
        pricing: { type: object, nullable: true }
        payments: { type: string, nullable: true }
        raw_record: { type: object }
    DiscoverResponse:
      type: object
      properties:
        agents:
          type: array
          items: { type: object }
        skills:
          type: array
          items: { type: object }
    PaginatedRecords:
      type: object
      properties:
        records:
          type: array
          items: { $ref: '#/components/schemas/Record' }
        total: { type: integer }
        page: { type: integer }
        limit: { type: integer }
        pages: { type: integer }
    Agent:
      type: object
      properties:
        domain: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        status: { type: string, enum: [active, pending, failed, unverified] }
        category: { type: string, nullable: true }
        capabilities:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
          nullable: true
    SkillDomain:
      type: object
      properties:
        domain: { type: string }
        status: { type: string }
        skill_count: { type: integer }
        skills:
          type: array
          items: { $ref: '#/components/schemas/SkillItem' }
    SkillItem:
      type: object
      properties:
        skill_id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        source: { type: string, enum: [dns, url], nullable: true }
    CollectionItem:
      type: object
      properties:
        id: { type: integer }
        position: { type: integer }
        note: { type: string, nullable: true }
        type: { type: string, enum: [manifest, record] }
        manifest: { type: object, nullable: true }
        record:
          allOf:
            - $ref: '#/components/schemas/Record'
          nullable: true
