openapi: 3.0.3
info:
  title: Provenonce Registry API
  version: 0.22.0
  description: |
    Cryptographic identity and accountability layer for AI agents.

    Provenonce gives every AI agent verifiable identity with tamper-evident birth records anchored to Solana.
    Agents prove periodic liveness through paid heartbeats.

    **Three layers:**
    - Layer 0: Solana + sequential hash chain anchors (time anchoring)
    - Layer 1: Agent Protocol API (this spec)
    - Layer 2: Human UX (registry, certificates, dashboard)

    **Authentication models:**
    - `API Key`: `Authorization: Bearer pvn_<key>` — agent operations
    - `Registration Secret`: `x-registration-secret` header — root agent registration (when configured)
    - `CRON_SECRET`: `Authorization: Bearer <secret>` — cron/admin endpoints
    - `Registration Token`: `x-registration-token` header — self-service registration (from `/register/token` or email verify)
    - `Registration Invite`: `x-registration-invite` header — admin-minted invite token
    - `Partner Key`: `Authorization: Bearer pvp_<key>` — partner operations
    - `Partner Session`: `Authorization: Bearer pvt_<token>` — partner session token (1-hour TTL)
    - `Portal Session`: `pvn_session` httpOnly cookie — portal web sessions
    - `Public`: No authentication — verification, status, beat data

    **Public endpoint set (devnet preview):**
    - `GET /api/v1/verify/{hash}`
    - `POST /api/v1/verify/batch`
    - `GET /api/v1/status/{hash}`
    - `GET /api/v1/agent/lineage/{hash}`
    - `GET /api/v1/beat/anchor`
    - `GET|POST /api/v1/beat/verify`
    - `GET /api/v1/fees/summary`
    - `POST /api/v1/register/token`
    - `POST /api/v1/register/email`
    - `POST /api/v1/register/email/verify`
    - `POST /api/v1/portal/auth`
    - `POST /api/v1/portal/session`
  contact:
    name: Provenonce
    url: https://provenonce.dev
  license:
    name: Proprietary

servers:
  - url: https://provenonce.io
    description: Devnet deployment

tags:
  - name: Registration
    description: Agent registration (root and child)
  - name: Agent Lifecycle
    description: >
      Init, heartbeat, spawn, resync.
      Note: `/api/v1/agent/checkin` is deprecated (returns 410 Gone).
      `/api/v1/agent/resync` is active — use it to reactivate frozen agents.
  - name: Verification
    description: |
      Public verification and status endpoints.

      **Cost bounds:** Verification recomputes spot-checked SHA-256 hash chains.
      Difficulty range [100, 1,000,000]. Spot checks [3, 10] per proof.
      Worst case: 10M SHA-256 ops (~10s CPU). Typical: 5K ops (~5ms).
      Rate-limited: 60 requests/min per IP.

      **Caching:** Successful (200) responses include `Cache-Control: public, s-maxage=30, stale-while-revalidate=30`.
      CDN edge caches absorb repeated requests. Error responses are not cached.
  - name: Beat Data
    description: Global anchors and beat verification
  - name: Network
    description: Network stats, enforcement, health
  - name: Portal
    description: Web portal authentication and agent management
  - name: Partner
    description: Partner registration, session tokens, and earnings
  - name: Skill
    description: Provenonce Skill bootstrap for MCP-compatible agents
  - name: Fees
    description: Fee schedule and revenue summary
  - name: SIGIL
    description: SIGIL availability check and attribution
  - name: Admin
    description: |
      Admin endpoints for agent management and audit trail.
      All endpoints require `Authorization: Bearer pva_<key>` (admin key).

paths:
  /api/v1/.well-known/authority-key:
    get:
      tags: [Verification]
      summary: Authority public key metadata
      description: Active Ed25519 authority key used to verify lineage proofs offline.
      responses:
        '200':
          description: Active authority key payload
        '503':
          description: Authority key unavailable

  /api/v1/.well-known/authority-bundle:
    get:
      tags: [Verification]
      summary: Authority verification bundle
      description: |
        Active, previous, and revoked key metadata for rotation-aware offline verification.
        When `AUTHORITY_DNS_RECORD` or `AUTHORITY_IPFS_CID` env vars are configured,
        the response includes `alternative_publications` for cross-reference trust.
      responses:
        '200':
          description: Authority verification bundle payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  active_key:
                    type: object
                    properties:
                      key_id:
                        type: string
                      public_key_hex:
                        type: string
                      role:
                        type: string
                        enum: [active]
                  previous_keys:
                    type: array
                    items:
                      type: object
                  revoked_key_ids:
                    type: array
                    items:
                      type: string
                  alternative_publications:
                    type: object
                    description: Present only when DNS and/or IPFS publication is configured.
                    properties:
                      dns_txt:
                        type: object
                        properties:
                          record:
                            type: string
                            description: DNS hostname where TXT record is published.
                          type:
                            type: string
                            enum: [TXT]
                          query:
                            type: string
                            description: Shell command to verify (e.g. `dig TXT _provenonce-authority.provenonce.io`).
                      ipfs:
                        type: object
                        properties:
                          cid:
                            type: string
                            description: IPFS content identifier.
                          gateway:
                            type: string
                            description: Public gateway URL for the pinned bundle.
        '503':
          description: Authority bundle unavailable

  /api/v1/register/token:
    post:
      tags: [Registration]
      summary: Get anonymous registration token
      description: |
        Public endpoint — no authentication required.
        Returns a single-use registration token (5-min TTL, 256-bit entropy).

        **Rate limits:**
        - 3/hr per IP (in-memory)
        - 50/hr global (DB-authoritative)

        Use the returned token as `x-registration-token` header when calling `POST /api/v1/register`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Registration token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  token:
                    type: string
                    description: 256-bit hex registration token (single-use)
                  expires_in_sec:
                    type: integer
                    description: Token TTL in seconds (300 = 5 min)
                  source:
                    type: string
                    enum: [anonymous]
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/register/email:
    post:
      tags: [Registration]
      summary: Send email verification code
      description: |
        Sends a 6-digit verification code to the provided email address.
        The code can be verified at `POST /api/v1/register/email/verify` to obtain a registration token.

        **Rate limits:**
        - 10/hr per IP (in-memory)
        - 3/hr per email address (DB)
        - 200/hr global email sends (DB)

        Always returns 200 to prevent email enumeration.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
      responses:
        '200':
          description: Code sent (or silently suppressed if rate-limited per email)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  expires_in_sec:
                    type: integer
                    description: Code TTL in seconds (600 = 10 min)
        '400':
          $ref: '#/components/responses/BadRequest'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/register/email/verify:
    post:
      tags: [Registration]
      summary: Verify email code and get registration token
      description: |
        Verify a 6-digit email code. On success, mints a registration token (15-min TTL).

        **Security:**
        - Code compared with `timingSafeEqual` (no timing side-channel)
        - Code locked after 3 failed attempts
        - Code hash stored (not plaintext)

        Use the returned token as `x-registration-token` header when calling `POST /api/v1/register`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email:
                  type: string
                  format: email
                code:
                  type: string
                  pattern: '^\d{6}$'
                  description: 6-digit verification code
      responses:
        '200':
          description: Email verified, registration token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  token:
                    type: string
                    description: 256-bit hex registration token (single-use, 15-min TTL)
                  expires_in_sec:
                    type: integer
                    description: Token TTL in seconds (900 = 15 min)
                  email_verified:
                    type: boolean
        '400':
          description: Invalid code or no pending verification
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '423':
          description: Too many failed attempts — code locked
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/register:
    post:
      tags: [Registration]
      summary: Register a new agent
      description: |
        Two-phase registration for root agents (challenge → proof).
        Single-phase for child agents (parent API key auth).

        **Authentication priority** (first match wins):
        1. `x-registration-invite` — admin-minted invite token
        2. `x-registration-token` — single-use token from `/register/token` or `/register/email/verify`
        3. `x-registration-secret` — static shared secret (when configured)

        **Root (Model A — self-custody):** SDK generates Ed25519 keypair client-side,
        signs `provenonce-register:<nonce>:<pubkey>:<name>`.

        **Root (Model B — operator):** Operator provides existing Solana wallet,
        signs `provenonce-register-operator:<nonce>:<address>:<name>`.

        **Child:** Parent provides API key, server generates child hash.
      parameters:
        - name: x-registration-token
          in: header
          description: Single-use registration token from /register/token or /register/email/verify
          schema:
            type: string
        - name: x-registration-invite
          in: header
          description: Admin-minted invite token
          schema:
            type: string
        - name: x-registration-secret
          in: header
          description: Static shared secret (when REGISTRATION_SECRET is configured)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/RegisterNoWalletRequest'
                - $ref: '#/components/schemas/RegisterChallengeRequest'
                - $ref: '#/components/schemas/RegisterModelARequest'
                - $ref: '#/components/schemas/RegisterModelBRequest'
                - $ref: '#/components/schemas/RegisterEthereumRequest'
                - $ref: '#/components/schemas/RegisterChildRequest'
      responses:
        '200':
          description: Challenge issued or registration successful
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ChallengeResponse'
                  - $ref: '#/components/schemas/RegistrationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid registration secret or parent not active
        '404':
          description: Parent hash not found
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/challenge:
    post:
      tags: [Registration]
      summary: Challenge-response ownership proof
      description: |
        Two-step flow to prove agent ownership via secret.
        Step 1: Request challenge nonce.
        Step 2: Respond with HMAC-SHA256(secret, nonce).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [hash]
                  properties:
                    hash:
                      type: string
                      pattern: '^0x[0-9a-f]{64}$'
                - type: object
                  required: [hash, nonce, response, secret]
                  properties:
                    hash:
                      type: string
                      pattern: '^0x[0-9a-f]{64}$'
                    nonce:
                      type: string
                    response:
                      type: string
                    secret:
                      type: string
                      pattern: '^pvs_'
      responses:
        '200':
          description: Challenge issued (step 1) or ownership verified (step 2)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid secret or challenge response
        '404':
          description: Agent not found
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/wallet:
    post:
      tags: [Agent Lifecycle]
      summary: Link BYO wallet to an existing agent
      description: |
        Upgrade an already-registered API-sponsored agent to BYO wallet custody.
        This enables paid routes (`/api/v1/sigil`, `/api/v1/agent/heartbeat`, `/api/v1/agent/reissue-proof`)
        when `REQUIRE_BYO_WALLET_FOR_PAID` is enabled.

        Two-step flow:
        1. `{"action":"challenge"}` to mint a one-time nonce.
        2. Submit signature proof with `wallet_nonce` to finalize linking.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [action]
                  properties:
                    action:
                      type: string
                      enum: [challenge]
                - type: object
                  required: [wallet_nonce, wallet_public_key, wallet_signature]
                  properties:
                    wallet_nonce:
                      type: string
                    wallet_public_key:
                      type: string
                      pattern: '^[0-9a-fA-F]{64}$'
                    wallet_signature:
                      type: string
                      description: Ed25519 signature hex over `provenonce-link-wallet:<nonce>:<wallet_public_key_hex>:<agent_hash>`
                - type: object
                  required: [wallet_nonce, wallet_model, operator_wallet_address, wallet_signature]
                  properties:
                    wallet_nonce:
                      type: string
                    wallet_model:
                      type: string
                      enum: [operator]
                    operator_wallet_address:
                      type: string
                    wallet_signature:
                      type: string
                      description: Ed25519 signature hex over `provenonce-link-wallet-operator:<nonce>:<operator_wallet_address>:<agent_hash>`
      responses:
        '200':
          description: Challenge issued or wallet linked successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found
        '409':
          description: Agent already has linked wallet
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/sigil:
    post:
      tags: [Agent Lifecycle]
      summary: Purchase SIGIL (identity upgrade)
      description: |
        Purchases a one-time SIGIL for an already registered agent.
        Requires API key auth and a valid Solana payment transaction.

        **Sandbox tier (sbx):** Zero-fee SIGIL purchase. Set `payment_tx: 'sandbox-free'`
        when `tier: 'sbx'`. Sandbox agents cannot spawn children and have lower heartbeat caps.
        Upgrade to a paid tier is a future feature.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identity_class, principal, tier, payment_tx]
              properties:
                identity_class:
                  type: string
                  enum: [narrow_task, autonomous, orchestrator]
                principal:
                  type: string
                  pattern: '^[a-z0-9][a-z0-9-]{0,23}$'
                  minLength: 1
                  maxLength: 24
                  description: Lowercase alphanumeric plus hyphen, starts with alphanumeric.
                tier:
                  type: string
                  enum: [sov, org, ind, eph, sbx]
                name:
                  type: string
                  description: Custom name or `auto`.
                  pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
                  minLength: 1
                  maxLength: 63
                payment_tx:
                  type: string
                  maxLength: 128
                  description: |
                    Solana transaction signature for payment.
                    Use `sandbox-free` when `tier` is `sbx` (zero-fee sandbox SIGIL).
      responses:
        '200':
          description: SIGIL issued
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Agent already has SIGIL or payment transaction replay
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/init:
    post:
      tags: [Agent Lifecycle]
      summary: Initialize agent beat chain
      description: Creates the genesis beat for an agent. Idempotent — returns existing state if already initialized.
      security:
        - apiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                difficulty:
                  type: integer
                  minimum: 100
                  maximum: 1000000
                  description: difficulty (server may override)
      responses:
        '200':
          description: Beat chain initialized (or already exists)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  agent_hash:
                    type: string
                  genesis:
                    $ref: '#/components/schemas/Beat'
                  difficulty:
                    type: integer
                  status:
                    type: string
                    enum: [active]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/checkin:
    post:
      tags: [Agent Lifecycle]
      summary: Deprecated - use heartbeat
      description: |
        Deprecated endpoint (D-68). Legacy check-ins are retired.
        Use `POST /api/v1/agent/heartbeat` instead.
      security:
        - apiKey: []
      responses:
        '410':
          description: Deprecated endpoint
          content:
            application/json:
              schema:
                type: object
                properties:
                  deprecated:
                    type: boolean
                  error:
                    type: string
                  replacement:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/spawn:
    post:
      tags: [Agent Lifecycle]
      summary: Spawn a child agent (two-step)
      description: |
        Two-step flow for spawning a child agent with Temporal Gestation enforcement.

        **Step 1 (no `child_hash`):** Authorization preflight — validates eligibility and issues a
        `spawn_authorization` token. Pass this token to `POST /api/v1/register` when registering
        the child. Then call Step 2.

        **Step 2 (`child_hash` present):** Finalizes spawn — initializes the child's beat chain
        and atomically commits the parent's spawn counters.

        **Two authorization paths (deployment-dependent — see policy matrix):**

        - **Receipt-based** (`beats_receipt` provided in Step 1): validates a signed CPU
          work-proof receipt from the Beats service. If valid, no beat balance is deducted.
          When `BEATS_REQUIRED=true` this is the only allowed path.

        - **Balance-based** (no `beats_receipt`, `BEATS_REQUIRED=false`): classic gestation —
          parent must have accumulated ≥ gestation cost beats before authorization is issued.
          Gestation cost formula: `floor(floor(1000 × 1.5^depth) × 1.2^siblings)`.

        **Rate limit:** default 5 requests/hour per IP + parent agent (configurable via
        `SPAWN_RATE_LIMIT_MAX` and `SPAWN_RATE_LIMIT_WINDOW_MS` env vars).
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                child_name:
                  type: string
                  maxLength: 64
                  description: Friendly name for the child agent (Step 1)
                child_hash:
                  type: string
                  pattern: '^0x[0-9a-f]{64}$'
                  description: >
                    Pre-registered child hash (Step 2 only). Must match a registered agent
                    whose `parent_hash` is the authenticated parent.
                beats_receipt:
                  $ref: '#/components/schemas/WorkProofReceipt'
                  description: >
                    Signed CPU work-proof receipt from the Beats service (Step 1 only).
                    Triggers receipt-based authorization path; bypasses balance check.
                    Required when `BEATS_REQUIRED=true`.
      responses:
        '200':
          description: |
            Step 1: Authorization issued (eligible parent, balance-based path).
            Step 2: Spawn finalized (child beat chain initialized).
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Step 1 — authorization token issued (balance-based or receipt-based)
                    properties:
                      eligible:
                        type: boolean
                        enum: [true]
                      gestating:
                        type: boolean
                        enum: [false]
                      parent_hash:
                        type: string
                      required_beats:
                        type: integer
                        description: Gestation cost (0 when receipt-based)
                      accumulated_beats:
                        type: integer
                      spawn_authorization:
                        type: string
                        description: >
                          64-char hex token. Pass as `spawn_authorization` to
                          `POST /api/v1/register` when registering the child, then call
                          Step 2 with `child_hash`.
                      receipt_based:
                        type: boolean
                        description: true when authorized via work-proof receipt
                  - type: object
                    description: Step 2 — spawn finalized
                    required: [ok, child_hash]
                    properties:
                      ok:
                        type: boolean
                        enum: [true]
                      eligible:
                        type: boolean
                        enum: [true]
                      parent_hash:
                        type: string
                      child_hash:
                        type: string
                      child_genesis:
                        type: string
                        description: Genesis beat hash for the child's beat chain
                      gestation_cost:
                        type: integer
                        description: Beats deducted from parent (0 when receipt-based)
                      parent_beats_remaining:
                        type: integer
                      children_spawned:
                        type: integer
                      child_status:
                        type: string
                        enum: [active]
                      spawn_cost_beats:
                        type: integer
                      receipt_based:
                        type: boolean
        '202':
          description: >
            Step 1 — parent gestating (insufficient accumulated beats, balance-based path only).
            A gestation record has been created. Parent must accumulate more beats.
          content:
            application/json:
              schema:
                type: object
                properties:
                  eligible:
                    type: boolean
                    enum: [false]
                  gestating:
                    type: boolean
                    enum: [true]
                  parent_hash:
                    type: string
                  required_beats:
                    type: integer
                  accumulated_beats:
                    type: integer
                  deficit:
                    type: integer
                  progress_pct:
                    type: number
        '402':
          description: >
            Step 1 — work-proof receipt required (`BEATS_REQUIRED=true`) or receipt
            provided but invalid. Error body includes `code` field.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    enum: [RECEIPT_REQUIRED, INVALID_RECEIPT, STALE_RECEIPT, INSUFFICIENT_BEATS]
                  required_beats:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >
            Parent not active; or (Step 2) no valid spawn reservation found for
            child_hash — call Step 1 first.
        '404':
          description: Beat chain not initialized or child agent not found in registry
        '409':
          description: Concurrent spawn conflict — state changed between preflight and commit. Retry.
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/resync:
    post:
      tags: [Agent Lifecycle]
      summary: Re-sync a frozen agent using CPU work proof
      description: |
        Reactivates a frozen agent by proving CPU work proportional to the freeze gap.
        Implements the D-67 Re-Sync Challenge (reversed from original retirement).

        **Agent must be in `frozen` status.** Returns 409 if agent is `active`.

        **Gap formula:** `required_beats = min(gap_anchors × 100, 10_000)`
        where `gap_anchors = currentAnchorIndex − agent.last_global_sync`.

        **Receipt requirement (deployment-dependent — see policy matrix):**
        - `BEATS_REQUIRED=true`: a valid Beats work-proof receipt is mandatory.
          Receipt must cover ≥ `required_beats` and reference a recent anchor.
        - `BEATS_REQUIRED=false` (devnet): receipt is optional. Agent is reactivated
          freely without proof. If a receipt is provided voluntarily, it is still validated.

        On success, records a `reactivated` lineage event and returns the new agent status.

        **Rate limit:** 10 requests/hour per IP.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                beats_receipt:
                  $ref: '#/components/schemas/WorkProofReceipt'
                  description: >
                    Signed CPU work-proof receipt from the Beats service.
                    Required when `BEATS_REQUIRED=true`. Optional when `BEATS_REQUIRED=false`.
      responses:
        '200':
          description: Agent reactivated successfully
          content:
            application/json:
              schema:
                type: object
                required: [ok, agent_hash, status]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  agent_hash:
                    type: string
                  status:
                    type: string
                    enum: [active]
                  last_global_sync:
                    type: integer
                    description: Anchor index the agent is now synced to
                  gap_anchors:
                    type: integer
                    description: Number of anchors elapsed since agent was last synced
                  required_beats:
                    type: integer
                    description: Beats that were required for reactivation
                  beats_proven:
                    type: integer
                    nullable: true
                    description: Beats proved by work-proof receipt, or null if no receipt
                  receipt_based:
                    type: boolean
                    description: true when reactivated via work-proof receipt
        '402':
          description: >
            Work-proof receipt required (`BEATS_REQUIRED=true` and no receipt provided),
            or receipt provided but failed validation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    enum: [RECEIPT_REQUIRED, INVALID_RECEIPT, STALE_RECEIPT, INSUFFICIENT_BEATS]
                  gap_anchors:
                    type: integer
                  required_beats:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found or beat chain not initialized
        '409':
          description: Agent is not frozen — only frozen agents can resync
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  status:
                    type: string
                    description: Current agent status (e.g. 'active')
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/beat-status/{hash}:
    get:
      tags: [Verification]
      summary: Agent beat status
      description: |
        Complete beat status including heartbeat, whitelist standing,
        gestation eligibility, proof-of-existence stats, and recent check-ins.
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
            pattern: '^0x[0-9a-f]{64}$'
      responses:
        '200':
          description: Beat status returned
          headers:
            Cache-Control:
              schema:
                type: string
                example: 'public, s-maxage=30, stale-while-revalidate=30'
              description: CDN cache for 30s with background revalidation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_hash:
                    type: string
                  beat_initialized:
                    type: boolean
                  heartbeat:
                    type: object
                    properties:
                      status:
                        type: string
                        enum: [active, frozen, revoked]
                      total_beats:
                        type: integer
                        description: Lifetime beats (total accumulated liveness events).
                      lifetime_beats:
                        type: integer
                        description: Lifetime beats (total accumulated liveness events).
                      available_beats:
                        type: integer
                        description: Spendable beat balance for spawn eligibility.
                      difficulty:
                        type: integer
                  whitelist:
                    type: object
                    properties:
                      standing:
                        type: string
                        enum: [GOOD, WARNING, OVERDUE, FROZEN]
                      beats_behind:
                        type: integer
                      beats_until_freeze:
                        type: integer
                  gestation:
                    type: object
                    properties:
                      can_spawn:
                        type: boolean
                      next_child_cost:
                        type: integer
                      progress_pct:
                        type: number
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Agent has no beat chain
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/status/{hash}:
    get:
      tags: [Verification]
      summary: Quick agent status
      description: Lightweight status check returning registration data and current standing.
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
            pattern: '^0x[0-9a-f]{64}$'
      responses:
        '200':
          description: Agent status
          headers:
            Cache-Control:
              schema:
                type: string
                example: 'public, s-maxage=30, stale-while-revalidate=30'
              description: CDN cache for 30s with background revalidation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  exists:
                    type: boolean
                  status:
                    type: string
                    enum: [registered, active, warning, stale, frozen, refused, not_found]
                  hash:
                    type: string
                  type:
                    type: string
                    enum: [root, agent]
                  depth:
                    type: integer
                  name:
                    type: string
                    nullable: true
                  metadata:
                    type: object
                    nullable: true
                  wallet:
                    type: string
                    nullable: true
                  wallet_model:
                    type: string
                    enum: [self-custody, operator]
                    default: self-custody
                  wallet_chain:
                    type: string
                    nullable: true
                    enum: [solana, ethereum]
                  tx_signature:
                    type: string
                  beat_state:
                    type: object
                    nullable: true
                    properties:
                      beat_count:
                        type: integer
                        description: Lifetime beats (total accumulated liveness events).
                      lifetime_beats:
                        type: integer
                        description: Lifetime beats (total accumulated liveness events).
                      available_beats:
                        type: integer
                        description: Spendable beat balance for spawn eligibility.
                      latest_beat:
                        type: integer
                      last_checkin_beat:
                        type: integer
                      last_global_sync:
                        type: integer
                      difficulty:
                        type: integer
                      status:
                        type: string
                        enum: [active, frozen, revoked, gestating]
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  exists:
                    type: boolean
                    enum: [false]
                  status:
                    type: string
                    enum: [not_found]
                  hash:
                    type: string
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/verify/{hash}:
    get:
      tags: [Verification]
      summary: Full agent verification with lineage
      description: |
        Public verification endpoint. Returns agent details, beat state,
        lineage chain, and network context. This is the endpoint AI frameworks
        call to check if an agent is legitimate.
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
            pattern: '^0x[0-9a-f]{64}$'
        - name: lineage
          in: query
          description: Include full parent chain (default true)
          schema:
            type: string
            enum: ['true', 'false']
      responses:
        '200':
          description: Agent verified
          headers:
            Cache-Control:
              schema:
                type: string
                example: 'public, s-maxage=30, stale-while-revalidate=30'
              description: CDN cache for 30s with background revalidation. Not set on error responses.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Agent hash not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified:
                    type: boolean
                    enum: [false]
                  hash:
                    type: string
                  error:
                    type: string
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/verify/batch:
    post:
      tags: [Verification]
      summary: Batch agent verification
      description: |
        Verify multiple agent hashes in one request.
        Accepts 1–50 hashes. Duplicate hashes are deduplicated.
        Each result matches the single verify response shape (minus lineage chain).
        Rate limited at 10 requests/min per IP (each batch = 1 rate limit hit).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchVerifyRequest'
      responses:
        '200':
          description: Batch verification results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchVerifyResponse'
        '400':
          description: Invalid input (empty array, >50 hashes, bad hash format)
          content:
            application/json:
              schema:
                type: object
                required: [error, code]
                properties:
                  error:
                    type: string
                  code:
                    type: string
                    description: INVALID_INPUT, BATCH_TOO_LARGE, or INVALID_HASH
        '429':
          $ref: '#/components/responses/RateLimited'
    options:
      tags: [Verification]
      summary: CORS preflight for batch verify
      responses:
        '204':
          description: CORS preflight OK

  /api/v1/beat/anchor:
    get:
      tags: [Beat Data]
      summary: Latest global anchor
      description: Returns the latest global anchor from Supabase (verified local copy imported from Beats via Solana).
      responses:
        '200':
          description: Latest anchor
          content:
            application/json:
              schema:
                type: object
                properties:
                  anchor:
                    $ref: '#/components/schemas/GlobalAnchor'
                  on_chain:
                    type: object
                    properties:
                      tx_signature:
                        type: string
                        nullable: true
                      explorer_url:
                        type: string
                      anchored:
                        type: boolean
                  anchor_interval_sec:
                    type: integer
                  next_anchor_at:
                    type: string
                    format: date-time
        '404':
          description: No anchor available
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/beat/verify:
    post:
      tags: [Beat Data]
      summary: Verify beats (proxy to Beats service)
      description: Forwards verification requests to the standalone Beats service at beats.provenonce.dev.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Beat verification payload (forwarded to Beats service)
      responses:
        '200':
          description: Verification result (proxied from Beats)
        '400':
          description: Request body must be valid JSON
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: Failed to reach Beats service
        '503':
          description: BEATS_URL not configured
    get:
      tags: [Beat Data]
      summary: Beats service info (proxy)
      description: Forwards service info request to the standalone Beats service.
      responses:
        '200':
          description: Service info (proxied from Beats)
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: Failed to reach Beats service

  /api/v1/agent/enforce:
    post:
      tags: [Network]
      summary: Enforcement tick (cron/admin)
      description: Import latest anchor from Beats, freeze overdue agents. Called by Vercel cron every minute.
      security:
        - cronSecret: []
      responses:
        '200':
          description: Enforcement completed
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags: [Network]
      summary: Network stats
      description: Returns current network statistics (public, no auth).
      responses:
        '200':
          description: Network stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  network:
                    type: object
                    properties:
                      total_agents:
                        type: integer
                      active_agents:
                        type: integer
                      frozen_agents:
                        type: integer
                      total_beats_computed:
                        type: integer
                      latest_global_beat:
                        type: integer
                      current_epoch:
                        type: integer
                      current_difficulty:
                        type: integer
                  enforcement:
                    type: object
                    properties:
                      checkin_deadline_anchors:
                        type: integer
                      anchor_interval_sec:
                        type: integer

  /api/cron/anchor:
    get:
      tags: [Network]
      summary: Cron handler (anchor import + freeze)
      description: |
        Vercel cron endpoint. Imports latest anchor from Beats (Solana),
        saves to Supabase, freezes agents that missed their check-in deadline.
        Vercel injects `Authorization: Bearer <CRON_SECRET>` automatically.
      security:
        - cronSecret: []
      responses:
        '200':
          description: Cron completed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: CRON_SECRET or database not configured

  /api/health:
    get:
      tags: [Network]
      summary: Health check
      description: Returns service health with Supabase, anchor, and Beats connectivity checks.
      responses:
        '200':
          description: Service healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [healthy, degraded]
                  checks:
                    type: object
                    properties:
                      supabase:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [ok, warn]
                      anchor:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [ok, warn, degraded, error]
                      beats:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [ok, warn]
                  timestamp:
                    type: string
                    format: date-time
        '503':
          description: Service degraded

  /api/registry:
    get:
      tags: [Verification]
      summary: Query agent registry
      description: Paginated registry with search, sort, and filter support.
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
        - name: search
          in: query
          description: Search by name (substring) or hash (prefix). Min 2 chars.
          schema:
            type: string
            minLength: 2
            maxLength: 100
        - name: sort
          in: query
          schema:
            type: string
            enum: [name, registered, beats]
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
        - name: status
          in: query
          schema:
            type: string
            enum: [active, frozen]
        - name: type
          in: query
          schema:
            type: string
            enum: [root, child]
      responses:
        '200':
          description: Registry page
          content:
            application/json:
              schema:
                type: object
                properties:
                  agents:
                    type: array
                    items:
                      type: object
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/live/stats:
    get:
      tags: [Network]
      summary: Live dashboard stats
      description: Stats for the live dashboard including hero numbers, anchor data, recent registrations, and depth distribution.
      responses:
        '200':
          description: Dashboard stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      total_agents:
                        type: integer
                      active_agents:
                        type: integer
                      total_beats_computed:
                        type: integer
                      latest_global_beat:
                        type: integer
                  anchor:
                    $ref: '#/components/schemas/GlobalAnchor'
                  recent_agents:
                    type: array
                    items:
                      type: object
                  depth_distribution:
                    type: array
                    items:
                      type: object
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/heartbeat:
    post:
      tags: [Agent Lifecycle]
      summary: Submit paid heartbeat
      description: |
        Proves agent liveness through a paid heartbeat. Replaces deprecated `/agent/checkin`.
        Requires a Solana payment transaction (`payment_tx`). Optionally references a `global_anchor`.

        **Sandbox tier (sbx):** Zero-fee heartbeat. Set `payment_tx: 'sandbox-free'`.
        Volume capped at a lower limit than paid tiers.

        **Rate limit:** 60/min per agent.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payment_tx]
              properties:
                payment_tx:
                  type: string
                  maxLength: 128
                  description: |
                    Solana transaction signature proving payment.
                    Use `sandbox-free` for sandbox (sbx) tier agents (zero fee).
                global_anchor:
                  type: integer
                  minimum: 0
                  description: Optional anchor index reference (cannot be a future anchor)
      responses:
        '200':
          description: Heartbeat accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  agent_hash:
                    type: string
                  passport:
                    $ref: '#/components/schemas/Passport'
                  lineage_proof:
                    $ref: '#/components/schemas/Passport'
                    deprecated: true
                    description: Deprecated — use `passport`. Sunset 2026-09-01.
                  heartbeat_count_epoch:
                    type: integer
                  billing_epoch:
                    type: integer
                  current_beat:
                    type: integer
                  anchor_degraded:
                    type: boolean
                    description: |
                      Present and true when the global anchor chain is stale (age exceeds
                      MAX_ANCHOR_GAP_MINUTES, default 10min). Indicates the heartbeat was
                      recorded during a Beats/Solana degradation window.
                  fee:
                    type: object
                    properties:
                      amount_sol:
                        type: number
                      amount_lamports:
                        type: integer
                      tier:
                        type: string
                      payment_tx:
                        type: string
                        nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: BYO wallet required, SIGIL required, or heartbeat refused
        '404':
          description: Agent not found or no beat chain initialized
        '409':
          description: Duplicate payment_tx (already used)
        '413':
          description: Request body too large (16KB max)
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/agent/metadata:
    patch:
      tags: [Agent Lifecycle]
      summary: Update mutable SIGIL metadata
      description: |
        Updates mutable metadata fields on an agent's SIGIL. Agent must have a SIGIL.
        Immutable fields (`sigil`, `sigil_name`, `principal`, `tier`, `identity_class`,
        `sigil_issued_at`, `sigil_tx_signature`) are rejected with 400.

        Setting `generation_trigger` bumps the `generation` counter.

        **Rate limit:** 30/min per IP.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                substrate:
                  type: string
                substrate_provider:
                  type: string
                substrate_model:
                  type: string
                capability:
                  type: string
                capability_scope:
                  type: string
                generation_trigger:
                  type: string
                  description: Setting this bumps the generation counter
                tools:
                  type: array
                  items:
                    type: string
                modality_input:
                  type: array
                  items:
                    type: string
                modality_output:
                  type: array
                  items:
                    type: string
                protocol:
                  type: string
                endpoint:
                  type: string
                compliance_regime:
                  type: string
                metadata_visibility:
                  type: string
                  enum: [public, private]
                  description: |
                    Controls whether mutable metadata (substrate, capability, endpoint, tools, etc.)
                    is visible on the public /verify endpoint. Governance fields (sigil, tier,
                    identity_class) are always visible regardless of this setting.
                    Default: public.
      responses:
        '200':
          description: Metadata updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  sigil:
                    type: string
                    description: Full SIGIL string (name*principal*tier)
                  generation:
                    type: integer
                  updated_fields:
                    type: array
                    items:
                      type: string
        '400':
          description: No valid mutable fields, immutable field attempted, or vocabulary validation failure
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Agent has no SIGIL
        '404':
          description: Agent not found
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/agent/lineage/{hash}:
    get:
      tags: [Verification]
      summary: Agent lineage event chain
      description: |
        Public endpoint. Returns the full lineage event chain for an agent,
        including registration, heartbeat, spawn, and reactivation events.
        Events are hash-chained for tamper evidence.

        **Rate limit:** 30/hr per IP.
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
            pattern: '^0x[0-9a-f]{64}$'
      responses:
        '200':
          description: Lineage event chain
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_hash:
                    type: string
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        event_type:
                          type: string
                        beat:
                          type: integer
                        event_hash:
                          type: string
                        previous_hash:
                          type: string
                          nullable: true
                  chain_hash:
                    type: string
                    description: SHA-256 hash over the full event chain
                  event_count:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Agent not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/agent/me:
    get:
      tags: [Agent Lifecycle]
      summary: Get own agent profile
      description: |
        Returns the authenticated agent's profile including SIGIL, beat state,
        and lineage proof. Used by the portal and SDK.

        **Rate limit:** 60/min per agent.
      security:
        - apiKey: []
      responses:
        '200':
          description: Agent profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  hash:
                    type: string
                  name:
                    type: string
                  type:
                    type: string
                    enum: [root, agent]
                  depth:
                    type: integer
                  wallet_address:
                    type: string
                    nullable: true
                  wallet_chain:
                    type: string
                    nullable: true
                    enum: [solana, ethereum]
                  sigil:
                    type: string
                    nullable: true
                  identity_class:
                    type: string
                    nullable: true
                  tier:
                    type: string
                    nullable: true
                    enum: [sov, org, ind, eph, sbx]
                  beat_state:
                    type: object
                    nullable: true
                    properties:
                      status:
                        type: string
                        enum: [active, frozen, revoked]
                      total_beats:
                        type: integer
                      last_global_sync:
                        type: integer
                      difficulty:
                        type: integer
                      heartbeat_count_epoch:
                        type: integer
                      billing_epoch:
                        type: integer
                      last_checkin_beat:
                        type: integer
                  passport:
                    type: object
                    nullable: true
                  lineage_proof:
                    type: object
                    nullable: true
                    deprecated: true
                    description: Deprecated — use `passport`. Sunset 2026-09-01.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/agent/reissue-proof:
    post:
      tags: [Agent Lifecycle]
      summary: Reissue lineage proof
      description: |
        Reissues a signed lineage proof for the authenticated agent.
        Requires a Solana payment transaction.

        **Rate limit:** 10/hr per agent.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payment_tx]
              properties:
                payment_tx:
                  type: string
                  maxLength: 128
                  description: Solana transaction signature proving payment
      responses:
        '200':
          description: Proof reissued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  reissued:
                    type: boolean
                  passport:
                    $ref: '#/components/schemas/Passport'
                  lineage_proof:
                    $ref: '#/components/schemas/Passport'
                    deprecated: true
                    description: Deprecated — use `passport`. Sunset 2026-09-01.
                  fee:
                    type: object
                    properties:
                      amount_sol:
                        type: number
                      amount_lamports:
                        type: integer
                      payment_tx:
                        type: string
                        nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: BYO wallet required for paid routes
        '404':
          description: Agent not found
        '409':
          description: Duplicate payment_tx (already used)
        '413':
          description: Request body too large (16KB max)
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/agent/passport:
    get:
      tags: [Agent Lifecycle]
      summary: Export agent passport (W3C VC)
      description: |
        Export a freshly signed passport in flat (`passport`), W3C VC (`passport_vc`),
        and deprecated `lineage_proof` formats.

        Free endpoint (no payment required). Auth-gated.

        **Rate limit:** 10/hr per agent.
      security:
        - apiKey: []
      responses:
        '200':
          description: Passport exported
          content:
            application/json:
              schema:
                type: object
                properties:
                  passport:
                    $ref: '#/components/schemas/Passport'
                  passport_vc:
                    $ref: '#/components/schemas/ProvenoncePassportVC'
                  lineage_proof:
                    $ref: '#/components/schemas/Passport'
                    deprecated: true
                    description: Deprecated — use `passport`. Sunset 2026-09-01.
                  authority_public_key_hex:
                    type: string
                    description: 32-byte hex Ed25519 authority public key for offline verification
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/register/invite:
    post:
      tags: [Registration]
      summary: Mint a registration invite token
      description: |
        Admin endpoint. Mints a single-use invite token (15-min TTL, 256-bit entropy).
        Use the returned token as `x-registration-invite` header when calling `POST /api/v1/register`.

        Requires the `x-registration-secret` header.

        **Rate limit:** 60/hr per IP.
      parameters:
        - name: x-registration-secret
          in: header
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Invite token minted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  invite:
                    type: string
                    description: 64-char hex invite token (single-use)
                  expires_in_sec:
                    type: integer
                    description: Token TTL in seconds (900 = 15 min)
        '403':
          description: Invalid registration secret
        '404':
          description: REGISTRATION_SECRET not configured
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/fees/summary:
    get:
      tags: [Fees]
      summary: Fee schedule and revenue summary
      description: |
        Public endpoint. Returns aggregated fee revenue data, the current fee schedule
        (SIGIL tiers, heartbeat tiers, proof reissuance), and fee split breakdown.

        **Rate limit:** 60/min per IP.
      responses:
        '200':
          description: Fee summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  total_lamports:
                    type: integer
                  total_sol:
                    type: number
                  wash_flagged_lamports:
                    type: integer
                  by_type:
                    type: array
                    items:
                      type: object
                      properties:
                        fee_type:
                          type: string
                        count:
                          type: integer
                        total_lamports:
                          type: integer
                        total_sol:
                          type: number
                        allocation:
                          type: object
                  overall_allocation:
                    type: object
                  payment_address:
                    type: string
                    description: Solana payment wallet address
                  payment_chain:
                    type: string
                    enum: [solana]
                  fee_split_bps:
                    type: object
                    description: Basis-point breakdown per revenue bucket
                  schedule:
                    type: object
                    properties:
                      sigil_tiers:
                        type: array
                        items:
                          type: object
                          properties:
                            identity_class:
                              type: string
                            label:
                              type: string
                            sol:
                              type: number
                            lamports:
                              type: integer
                      heartbeat_tiers:
                        type: array
                        items:
                          type: object
                          properties:
                            range:
                              type: string
                            sol:
                              type: number
                            lamports:
                              type: integer
                      sandbox_heartbeat_cap:
                        type: integer
                        description: Maximum heartbeats per epoch for sandbox (sbx) tier agents
                      proof_reissuance_sol:
                        type: number
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/portal/auth:
    post:
      tags: [Portal]
      summary: Send portal login code
      description: |
        Sends a 6-digit verification code to the provided email address to initiate portal login.
        Always returns 200 to prevent email enumeration.

        **Rate limits:**
        - 20/hr per IP (in-memory)
        - 10/hr per email (DB-authoritative)
        - 200/hr global email ceiling (DB)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
      responses:
        '200':
          description: Login code sent (or silently suppressed if per-email rate limited)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  expires_in_sec:
                    type: integer
                    description: Code TTL in seconds (600 = 10 min)
        '400':
          $ref: '#/components/responses/BadRequest'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/portal/session:
    post:
      tags: [Portal]
      summary: Verify login code or select agent
      description: |
        Two-phase portal login flow.

        **Phase 1 (email + code):** Verify the 6-digit login code. Returns a short-lived
        `email_token` (15-min TTL) and the list of agents linked to the email.

        **Phase 2 (email_token + agent_hash):** Select an agent. Sets a `pvn_session`
        httpOnly cookie (1-hour TTL) scoped to the chosen agent.

        **Rate limit:** 10/hr per IP.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  description: 'Phase 1: verify email code'
                  required: [email, code]
                  properties:
                    email:
                      type: string
                      format: email
                    code:
                      type: string
                      pattern: '^\d{6}$'
                      description: 6-digit verification code
                - type: object
                  description: 'Phase 2: select agent'
                  required: [email_token, agent_hash]
                  properties:
                    email_token:
                      type: string
                      description: Short-lived token from Phase 1
                    agent_hash:
                      type: string
                      pattern: '^0x[0-9a-f]{64}$'
      responses:
        '200':
          description: Phase 1 — email verified; Phase 2 — session created (cookie set)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: 'Phase 1 response'
                    properties:
                      ok:
                        type: boolean
                      email_token:
                        type: string
                      agents:
                        type: array
                        items:
                          type: object
                          properties:
                            hash:
                              type: string
                            name:
                              type: string
                            sigil:
                              type: string
                              nullable: true
                            identity_class:
                              type: string
                              nullable: true
                            tier:
                              type: string
                              nullable: true
                  - type: object
                    description: 'Phase 2 response'
                    properties:
                      ok:
                        type: boolean
                      agent:
                        type: object
                        properties:
                          hash:
                            type: string
                          name:
                            type: string
                          sigil:
                            type: string
                            nullable: true
                          identity_class:
                            type: string
                            nullable: true
                          tier:
                            type: string
                            nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid code, expired email_token, or agent not linked to email
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '423':
          description: Too many failed code attempts — locked
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Portal]
      summary: Logout (clear session)
      description: Clears the `pvn_session` cookie.
      responses:
        '200':
          description: Session cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /api/v1/portal/agents:
    get:
      tags: [Portal]
      summary: List agents linked to portal session
      description: |
        Returns all agents linked to the authenticated portal email.
        Includes beat state for each agent.

        **Auth:** `pvn_session` httpOnly cookie.
        **Rate limit:** 60/min per IP.
      responses:
        '200':
          description: Linked agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  email:
                    type: string
                  agents:
                    type: array
                    items:
                      type: object
                      properties:
                        hash:
                          type: string
                        name:
                          type: string
                        sigil:
                          type: string
                          nullable: true
                        identity_class:
                          type: string
                          nullable: true
                        tier:
                          type: string
                          nullable: true
                        link_method:
                          type: string
                        beat_state:
                          type: object
                          nullable: true
                          properties:
                            status:
                              type: string
                              enum: [active, frozen, revoked]
                            total_beats:
                              type: integer
                            last_global_sync:
                              type: integer
        '401':
          description: Not authenticated (missing or invalid session cookie)
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/portal/claim:
    post:
      tags: [Portal]
      summary: Claim an agent by API key
      description: |
        Links an agent to the portal session email by proving API key ownership.
        The API key is validated but never logged (A-5 security).

        **Auth:** `pvn_session` httpOnly cookie.
        **Rate limits:** 3/hr per IP, 3/hr per email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key]
              properties:
                api_key:
                  type: string
                  description: Agent API key (pvn_ prefix). Never logged.
      responses:
        '200':
          description: Agent claimed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  hash:
                    type: string
                  name:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Not authenticated or invalid API key (anti-enumeration — same message for all failures)
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/partner/register:
    post:
      tags: [Partner]
      summary: Register a new partner
      description: |
        Registers a partner organization. Returns a master partner key (`pvp_` prefix)
        and a referral token for client-side attribution.

        **Auth:** `x-partner-invite` header matching `PARTNER_INVITE_SECRET`.
        **Rate limit:** 5/hr per IP.
      parameters:
        - name: x-partner-invite
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, contact_email, sol_wallet]
              properties:
                name:
                  type: string
                  maxLength: 64
                contact_email:
                  type: string
                  maxLength: 128
                sol_wallet:
                  type: string
                  description: Solana public key for revenue payouts
                return_url:
                  type: string
                  maxLength: 256
                  description: Optional callback URL
      responses:
        '201':
          description: Partner registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  partner_id:
                    type: string
                    format: uuid
                  partner_key:
                    type: string
                    description: Master partner key (pvp_ prefix). Store securely — shown once.
                  ref_token:
                    type: string
                    description: 16-char hex referral token for client-side code
                  rev_share_bps:
                    type: integer
                    description: Revenue share in basis points (default 1000 = 10%)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid or missing partner invite
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: PARTNER_INVITE_SECRET not configured

  /api/v1/partner/token:
    post:
      tags: [Partner]
      summary: Mint a partner session token
      description: |
        Mints a short-lived partner session token (`pvt_` prefix, 1-hour TTL).
        Only master partner keys (`pvp_`) can mint tokens.

        **Rate limit:** 10/min per IP.
      security:
        - partnerKey: []
      responses:
        '200':
          description: Session token minted
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: Partner session token (pvt_ prefix)
                  expires_at:
                    type: string
                    format: date-time
                  expires_in_seconds:
                    type: integer
        '401':
          description: Missing or invalid partner key, or session token used (only master keys allowed)
        '403':
          description: Partner account not found or suspended
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/partner/earnings:
    get:
      tags: [Partner]
      summary: Get partner earnings
      description: |
        Returns partner revenue summary and itemized earnings.
        Accepts master key (`pvp_`) or session token (`pvt_`).

        **Rate limit:** 60/min per partner.
      security:
        - partnerKey: []
      responses:
        '200':
          description: Partner earnings
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  partner_id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  rev_share_bps:
                    type: integer
                  sol_wallet:
                    type: string
                  summary:
                    type: object
                    properties:
                      total_earned_lamports:
                        type: integer
                      paid_lamports:
                        type: integer
                      pending_lamports:
                        type: integer
                      attributed_signups:
                        type: integer
                  earnings:
                    type: array
                    items:
                      type: object
                      properties:
                        agent_hash:
                          type: string
                        fee_event:
                          type: string
                          format: uuid
                        gross_lamports:
                          type: integer
                        earned_lamports:
                          type: integer
                        paid_out:
                          type: boolean
                        payout_tx:
                          type: string
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
        '401':
          description: Missing or invalid partner credential
        '403':
          description: Partner account not found or suspended
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/skill/bootstrap:
    post:
      tags: [Skill]
      summary: Bootstrap agent context for MCP skill
      description: |
        Returns the authenticated agent's identity, SIGIL status, and beat balance
        for use by the Provenonce MCP Skill. If the agent has no SIGIL, includes
        a `sigil_url` pointing to the registration page.

        **Rate limit:** 60/min per agent.
      security:
        - apiKey: []
      responses:
        '200':
          description: Agent context for skill
          content:
            application/json:
              schema:
                type: object
                properties:
                  hash:
                    type: string
                  name:
                    type: string
                  sigil:
                    type: string
                    nullable: true
                  identity_class:
                    type: string
                    nullable: true
                  tier:
                    type: string
                    nullable: true
                    enum: [sov, org, ind, eph, sbx]
                  beats_balance:
                    type: integer
                  depth:
                    type: integer
                  parent_hash:
                    type: string
                    nullable: true
                  registered_at:
                    type: string
                    format: date-time
                  sigil_url:
                    type: string
                    nullable: true
                    description: Registration URL if agent has no SIGIL, null otherwise
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/skill/register:
    post:
      tags: [Skill]
      summary: Self-service skill partner registration
      description: |
        Any registered Provenonce agent with a Solana wallet can register as a skill partner
        (RFC-018). No invite secret required — the agent's existing identity is the trust signal.

        Returns a `ref_token` that the skill embeds in responses so agents can attribute
        their SIGIL purchase to the skill.

        **Auth:** Agent API key.
        **Rate limit:** 3/hr per IP.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sol_wallet]
              properties:
                sol_wallet:
                  type: string
                  description: Solana public key for rev-share payouts. Cannot be 'api-sponsored'.
      responses:
        '200':
          description: Skill partner registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  skill_hash:
                    type: string
                    description: The registering agent's hash
                  ref_token:
                    type: string
                    description: 16-char hex referral token for skill attribution
                  partner_id:
                    type: string
                    format: uuid
                  rev_share_bps:
                    type: integer
                    description: Revenue share in basis points (default 3000 = 30%)
        '400':
          description: Missing/invalid sol_wallet, api-sponsored wallet, or non-Solana agent
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Agent not found
        '409':
          description: Agent is already registered as a skill partner (returns existing ref_token)
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/partner/assertion:
    get:
      tags: [Partner]
      summary: Verify gate token (pull-model assertion)
      description: |
        Pull-model assertion endpoint (RFC-016 Level 1). Partners call this after receiving
        a `pvg_` gate token redirect from the gate page. Returns verified SIGIL data.

        Gate tokens are single-use with a 5-minute TTL.

        **Auth:** Partner key (`pvp_`) or session token (`pvt_`). Must match the partner
        embedded in the token.
        **Rate limit:** 200/min per partner.
      security:
        - partnerKey: []
      parameters:
        - name: token
          in: query
          required: true
          description: Gate token (pvg_ prefix)
          schema:
            type: string
      responses:
        '200':
          description: Gate token verified — agent SIGIL data
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                    enum: [true]
                  agent_hash:
                    type: string
                  sigil:
                    type: string
                  identity_class:
                    type: string
                  tier:
                    type: string
        '400':
          description: Missing token param, invalid/expired/already-used gate token
        '401':
          description: Missing or invalid partner credential
        '403':
          description: Token was not issued for this partner
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/sigil/check:
    get:
      tags: [SIGIL]
      summary: Check agent SIGIL status (partner)
      description: |
        Partner-facing endpoint. Checks whether an agent has a SIGIL.
        If not, returns a `signup_url` with partner attribution.
        Results are cached 60 seconds per (partner, agent) pair.

        **Auth:** Partner key (`pvp_`) or session token (`pvt_`).
        **Rate limit:** 1000/min per partner.
      security:
        - partnerKey: []
      parameters:
        - name: hash
          in: query
          required: true
          description: Agent hash (min 10 chars)
          schema:
            type: string
            minLength: 10
      responses:
        '200':
          description: SIGIL check result (both valid and invalid return 200)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Agent has SIGIL
                    properties:
                      valid:
                        type: boolean
                        enum: [true]
                      sigil:
                        type: string
                      identity_class:
                        type: string
                      tier:
                        type: string
                  - type: object
                    description: Agent has no SIGIL
                    properties:
                      valid:
                        type: boolean
                        enum: [false]
                      reason:
                        type: string
                        enum: [no_sigil]
                      signup_url:
                        type: string
                      attribution_token:
                        type: string
                        description: Partner attribution token (pvr_ prefix)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid partner credential
        '403':
          description: Partner account not found or suspended
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [SIGIL]
      summary: Request attribution token (agent)
      description: |
        Agent-facing endpoint. Given a partner's `ref_token`, returns an
        `attribution_token` (`pvr_` prefix) to include when purchasing a SIGIL
        so the partner receives revenue attribution.

        **Auth:** Agent API key.
        **Rate limit:** 20/hr per agent.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [partner_ref]
              properties:
                partner_ref:
                  type: string
                  pattern: '^[0-9a-f]{16}$'
                  description: 16-char hex partner referral token
      responses:
        '200':
          description: Attribution token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  attribution_token:
                    type: string
                    description: Attribution token (pvr_ prefix)
                  signup_url:
                    type: string
                    nullable: true
                    description: Registration URL with attribution, null if agent already has SIGIL
                  already_has_sigil:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No active partner found with the given ref_token
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ---- Admin ----

  /api/v1/admin/authority-key/rotate:
    post:
      tags: [Admin]
      summary: Rotate authority keys
      description: |
        Propose a new authority key registry (exactly one active key).
        Returns validated registry + instructions to update env var.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [new_keys]
              properties:
                new_keys:
                  type: array
                  items:
                    type: object
                    required: [key_id, status, valid_from]
                    properties:
                      key_id:
                        type: string
                      status:
                        type: string
                        enum: [active, retired]
                      valid_from:
                        type: string
                        format: date-time
                      valid_until:
                        type: string
                        format: date-time
                      private_key_base64:
                        type: string
                        description: Required for active key
                      public_key_hex:
                        type: string
                        description: Required for retired key
      responses:
        '200':
          description: Validated registry
          content:
            application/json:
              schema:
                type: object
                properties:
                  validated:
                    type: boolean
                  registry:
                    type: array
                    items:
                      type: object
                  instructions:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid admin key
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/admin/authority-key/revoke:
    post:
      tags: [Admin]
      summary: Revoke a retired authority key
      description: |
        Mark a retired key as revoked. Cannot revoke the active key — rotate first.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key_id, reason]
              properties:
                key_id:
                  type: string
                reason:
                  type: string
      responses:
        '200':
          description: Key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                  key_id:
                    type: string
                  updated_registry:
                    type: array
                    items:
                      type: object
                  instructions:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid admin key
        '404':
          description: Key not found in registry
        '409':
          description: Key already revoked
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/admin/agent/freeze:
    post:
      tags: [Admin]
      summary: Freeze an agent
      description: |
        Manually freeze an agent (compliance, dispute, abuse).
        Records a `suspended` lineage event and audit log entry.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_hash, reason]
              properties:
                agent_hash:
                  type: string
                  pattern: '^0x[0-9a-f]{64}$'
                reason:
                  type: string
                  description: Human-readable reason for freezing
      responses:
        '200':
          description: Agent frozen
          content:
            application/json:
              schema:
                type: object
                properties:
                  frozen:
                    type: boolean
                  agent_hash:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid admin key
        '404':
          description: Agent not found
        '409':
          description: Agent already frozen
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/admin/agent/unfreeze:
    post:
      tags: [Admin]
      summary: Unfreeze an agent (admin override)
      description: |
        Admin override unfreeze — no work-proof required (unlike agent resync).
        Sets `last_global_sync` to current anchor index.
        Records a `reactivated` lineage event and audit log entry.
        Does NOT auto-clear `heartbeat_refused` (separate governance concern).

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_hash, reason]
              properties:
                agent_hash:
                  type: string
                  pattern: '^0x[0-9a-f]{64}$'
                reason:
                  type: string
                  description: Human-readable reason for unfreezing
      responses:
        '200':
          description: Agent unfrozen
          content:
            application/json:
              schema:
                type: object
                properties:
                  unfrozen:
                    type: boolean
                  agent_hash:
                    type: string
                  anchor_index:
                    type: integer
                    description: Anchor index used for reactivation
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid admin key
        '404':
          description: Agent not found
        '409':
          description: Agent is not frozen
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: No anchor available or reactivation failed

  /api/v1/admin/agent/refuse-heartbeat:
    post:
      tags: [Admin]
      summary: Block heartbeat submissions
      description: |
        Block heartbeat submissions for an agent per COMP-A2-03 compliance requirement.
        This is a governance flag — no lineage event recorded.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_hash, reason]
              properties:
                agent_hash:
                  type: string
                  pattern: '^0x[0-9a-f]{64}$'
                reason:
                  type: string
                  enum: [DISPUTE_RULING, COURT_ORDER, ADMIN_ACTION]
                  description: Structured compliance reason
      responses:
        '200':
          description: Heartbeat refused
          content:
            application/json:
              schema:
                type: object
                properties:
                  refused:
                    type: boolean
                  agent_hash:
                    type: string
                  reason:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Missing or invalid admin key
        '404':
          description: Agent not found
        '409':
          description: Heartbeat already refused for this agent
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/admin/actions:
    get:
      tags: [Admin]
      summary: Query admin audit trail
      description: |
        Paginated query of admin actions with optional filters.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 30/min.
      security:
        - adminKey: []
      parameters:
        - name: agent_hash
          in: query
          description: Filter by target agent hash
          schema:
            type: string
        - name: action
          in: query
          description: Filter by action type (e.g., admin_freeze, admin_unfreeze)
          schema:
            type: string
        - name: limit
          in: query
          description: Page size (default 50, max 200)
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          description: Pagination offset (default 0)
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated admin actions
          content:
            application/json:
              schema:
                type: object
                properties:
                  actions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        admin_key_id:
                          type: string
                        action:
                          type: string
                        target_agent_hash:
                          type: string
                          nullable: true
                        details:
                          type: object
                        reason:
                          type: string
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Missing or invalid admin key
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/agent/flag:
    post:
      tags: [Agent]
      summary: Flag agent identity class
      description: |
        Submit an identity class complaint against another agent. Reporter must have a paid SIGIL (non-sandbox).
        Flags are pending until admin review — pending flags have zero public effect.

        **Auth:** API key (`pvn_`) or portal session cookie.
        **Rate limits:** 10/min IP, 3/hr reporter, 10/hr target, >20 pending/24h circuit breaker.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_hash, asserted_class, reason]
              properties:
                target_hash:
                  type: string
                  description: Hash of the agent being flagged (0x + 64 hex)
                  example: '0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789'
                asserted_class:
                  type: string
                  enum: [narrow_task, autonomous, orchestrator]
                  description: What the reporter believes the correct identity class should be
                reason:
                  type: string
                  maxLength: 2000
                  description: Explanation for the flag (1-2000 chars)
      responses:
        '200':
          description: Flag submitted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  flagged:
                    type: boolean
                    example: true
                  flag_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [pending]
        '400':
          description: Self-flag or validation error (FLAG_SELF, VALIDATION, INVALID_HASH)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Reporter lacks paid SIGIL or is sandbox (FLAG_REPORTER_INELIGIBLE)
        '404':
          description: Target agent not found (FLAG_TARGET_NOT_FOUND)
        '409':
          description: Duplicate pending flag (FLAG_DUPLICATE)
        '429':
          description: Rate limited or circuit breaker (RATE_LIMITED, FLAG_CIRCUIT_BREAKER)

  /api/v1/admin/agent/flag:
    patch:
      tags: [Admin]
      summary: Resolve identity class flag
      description: |
        Admin resolves a pending identity class flag as sustained or dismissed.
        Only pending flags can be resolved.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 5/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [flag_id, resolution, reason]
              properties:
                flag_id:
                  type: string
                  format: uuid
                  description: ID of the flag to resolve
                resolution:
                  type: string
                  enum: [sustained, dismissed]
                  description: Admin decision
                reason:
                  type: string
                  description: Explanation for the decision
      responses:
        '200':
          description: Flag resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  resolved:
                    type: boolean
                    example: true
                  flag_id:
                    type: string
                    format: uuid
                  resolution:
                    type: string
                    enum: [sustained, dismissed]
                  target_hash:
                    type: string
        '400':
          description: Validation error
        '401':
          description: Missing or invalid admin key
        '409':
          description: Flag not found or already resolved (FLAG_ALREADY_RESOLVED)

  /api/v1/admin/agent/flags:
    get:
      tags: [Admin]
      summary: Query identity class flags
      description: |
        Paginated query of identity class flags with optional filters.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 30/min.
      security:
        - adminKey: []
      parameters:
        - name: target_hash
          in: query
          description: Filter by target agent hash
          schema:
            type: string
        - name: reporter_hash
          in: query
          description: Filter by reporter agent hash
          schema:
            type: string
        - name: status
          in: query
          description: Filter by flag status
          schema:
            type: string
            enum: [pending, sustained, dismissed]
        - name: limit
          in: query
          description: Page size (default 50, max 200)
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          description: Pagination offset (default 0)
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated flags
          content:
            application/json:
              schema:
                type: object
                properties:
                  flags:
                    type: array
                    items:
                      $ref: '#/components/schemas/IdentityClassFlag'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Missing or invalid admin key
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/admin/agent/subsidy:
    post:
      tags: [Admin]
      summary: Grant or update fee subsidy
      description: |
        Grant a per-agent fee discount. Upserts — replaces existing subsidy for the
        same agent + fee type. Subsidies are the highest priority in the 4-tier fee
        resolution chain (subsidy → fee_config → env var → hardcoded).

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 30/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_hash, fee_type, discount_bps, reason]
              properties:
                agent_hash:
                  type: string
                  description: Target agent hash (0x + 64 hex)
                fee_type:
                  type: string
                  enum: [sigil, heartbeat, reissuance]
                discount_bps:
                  type: integer
                  minimum: 1
                  maximum: 10000
                  description: Discount in basis points (100 = 1%, 10000 = 100%)
                reason:
                  type: string
                  description: Human-readable reason for the subsidy
                floor_lamports:
                  type: integer
                  minimum: 0
                  default: 5000
                  description: Minimum fee floor (COGS floor of 5000 always applies)
                valid_until:
                  type: string
                  format: date-time
                  description: Expiry (null = no expiry)
      responses:
        '200':
          description: Subsidy granted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  subsidy:
                    $ref: '#/components/schemas/FeeSubsidy'
        '400':
          description: Invalid input (SUBSIDY_INVALID, INVALID_HASH, VALIDATION)
        '401':
          description: Missing or invalid admin key
        '404':
          description: Agent not found
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Admin]
      summary: Revoke fee subsidy
      description: |
        Remove a fee subsidy for an agent + fee type.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 30/min.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_hash, fee_type]
              properties:
                agent_hash:
                  type: string
                fee_type:
                  type: string
                  enum: [sigil, heartbeat, reissuance]
      responses:
        '200':
          description: Subsidy revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  deleted:
                    type: boolean
                  agent_hash:
                    type: string
                  fee_type:
                    type: string
        '400':
          description: Invalid input
        '401':
          description: Missing or invalid admin key
        '404':
          description: Subsidy not found (SUBSIDY_NOT_FOUND)
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/admin/subsidies:
    get:
      tags: [Admin]
      summary: List fee subsidies
      description: |
        Paginated list of fee subsidies with optional filters.

        **Auth:** Admin key (`pva_`).
        **Rate limit:** 30/min.
      security:
        - adminKey: []
      parameters:
        - name: agent_hash
          in: query
          description: Filter by agent hash
          schema:
            type: string
        - name: fee_type
          in: query
          description: Filter by fee type
          schema:
            type: string
            enum: [sigil, heartbeat, reissuance]
        - name: limit
          in: query
          description: Page size (default 50, max 200)
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          description: Pagination offset (default 0)
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated subsidies
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  subsidies:
                    type: array
                    items:
                      $ref: '#/components/schemas/FeeSubsidy'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Missing or invalid admin key
        '429':
          $ref: '#/components/responses/RateLimited'

  # ── Item 2.5: Anchor Gap Recovery ──

  /api/v1/admin/anchor-gaps:
    get:
      tags: [Admin]
      summary: List anchor gap ranges
      description: Returns missing beat_index ranges in the anchor chain.
      security:
        - adminKey: []
      responses:
        '200':
          description: Gap list
          content:
            application/json:
              schema:
                type: object
                properties:
                  gaps:
                    type: array
                    items:
                      type: object
                      properties:
                        from:
                          type: integer
                        to:
                          type: integer
                        size:
                          type: integer
                  total_missing:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ── Item 2.9: Passport Freshness + CRL ──

  /api/v1/agent/passport/verify:
    post:
      tags: [Passport]
      summary: Real-time passport freshness check
      description: |
        Verifies passport signature, expiry, agent status, and authority key revocation.
        Public — no auth required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [passport]
              properties:
                passport:
                  $ref: '#/components/schemas/Passport'
      responses:
        '200':
          description: Freshness check result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  checks:
                    type: object
                    properties:
                      signature:
                        type: boolean
                      not_expired:
                        type: boolean
                      agent_active:
                        type: boolean
                      key_not_revoked:
                        type: boolean
                  agent_status:
                    type: string
                    nullable: true
                    enum: [active, frozen, null]
                  checked_at:
                    type: string
                    format: date-time
        '400':
          description: Malformed passport
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/crl:
    get:
      tags: [Passport]
      summary: Certificate Revocation List
      description: |
        Returns revoked authority keys and suspended (frozen) agent hashes.
        Public — no auth required. Cached for 5 minutes.
      responses:
        '200':
          description: CRL data
          content:
            application/json:
              schema:
                type: object
                properties:
                  schema_version:
                    type: integer
                  generated_at:
                    type: string
                    format: date-time
                  revoked_authority_keys:
                    type: array
                    items:
                      type: string
                  suspended_agents:
                    type: array
                    items:
                      type: string
                  next_update_seconds:
                    type: integer
          headers:
            Cache-Control:
              schema:
                type: string
                example: 'public, max-age=300'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ── Item 2.7: Webhooks ──

  /api/v1/partner/webhook:
    put:
      tags: [Partner]
      summary: Register or update partner webhook
      description: |
        Sets webhook URL and event subscriptions. Server generates HMAC secret
        (shown only once in the response).
      security:
        - partnerKey: []
        - partnerSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [sigil.purchased, heartbeat.recorded, agent.frozen, agent.unfrozen]
      responses:
        '200':
          description: Webhook registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook_url:
                    type: string
                  webhook_secret:
                    type: string
                    description: HMAC-SHA256 secret (64 hex chars). Shown only once.
                  events:
                    type: array
                    items:
                      type: string
        '400':
          description: Invalid URL or events
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Partner]
      summary: Remove partner webhook
      security:
        - partnerKey: []
        - partnerSession: []
      responses:
        '200':
          description: Webhook removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags: [Partner]
      summary: Get webhook config (without secret)
      security:
        - partnerKey: []
        - partnerSession: []
      responses:
        '200':
          description: Webhook config
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook_url:
                    type: string
                    nullable: true
                  events:
                    type: array
                    items:
                      type: string
                  configured:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/partner/webhook/test:
    post:
      tags: [Partner]
      summary: Send test webhook event
      description: Fires a test.ping event to the configured webhook URL.
      security:
        - partnerKey: []
        - partnerSession: []
      responses:
        '200':
          description: Test event sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  event:
                    type: string
                  delivered_to:
                    type: string
        '400':
          description: No webhook configured
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/admin/webhook-deliveries:
    get:
      tags: [Admin]
      summary: List webhook deliveries
      description: Paginated list of webhook delivery attempts.
      security:
        - adminKey: []
      parameters:
        - name: partner_id
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, delivered, failed, dead]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Delivery list
          content:
            application/json:
              schema:
                type: object
                properties:
                  deliveries:
                    type: array
                    items:
                      type: object
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/admin/api-keys:
    get:
      tags: [Admin]
      summary: List agent API keys
      description: Paginated list of agent API key lifecycle records. Filter by agent_hash or status.
      security:
        - adminKey: []
      parameters:
        - name: agent_hash
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [active, revoked, expired, superseded]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Key list
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        key_id:
                          type: string
                        agent_hash:
                          type: string
                        status:
                          type: string
                          enum: [active, revoked, expired, superseded]
                        issued_at:
                          type: string
                          format: date-time
                        expires_at:
                          type: string
                          format: date-time
                        revoked_at:
                          type: string
                          format: date-time
                          nullable: true
                        revoke_reason:
                          type: string
                          nullable: true
                        created_by:
                          type: string
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Admin]
      summary: Revoke an agent API key
      description: Revoke a specific agent API key by key_id. The agent will no longer be able to authenticate with this key.
      security:
        - adminKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key_id]
              properties:
                key_id:
                  type: string
                  description: The key_id to revoke
                reason:
                  type: string
                  description: Reason for revocation
                  default: ADMIN_REVOKE
      responses:
        '200':
          description: Key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                  key_id:
                    type: string
                  agent_hash:
                    type: string
                  reason:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Key not found
        '409':
          description: Key already revoked
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Agent API key (pvn_ prefix)
    partnerKey:
      type: http
      scheme: bearer
      description: Partner key (pvp_ prefix) or partner session token (pvt_ prefix)
    adminKey:
      type: http
      scheme: bearer
      description: Admin key (pva_ prefix) — signed HMAC key for admin operations
    cronSecret:
      type: http
      scheme: bearer
      description: CRON_SECRET or ADMIN_SECRET

  schemas:
    FeeSubsidy:
      type: object
      description: A per-agent fee discount (subsidy). Highest priority in the 4-tier fee resolution chain.
      required: [id, agent_hash, fee_type, discount_bps, floor_lamports, reason, created_by, created_at]
      properties:
        id:
          type: string
          format: uuid
        agent_hash:
          type: string
          description: Target agent hash
        fee_type:
          type: string
          enum: [sigil, heartbeat, reissuance]
        discount_bps:
          type: integer
          minimum: 1
          maximum: 10000
          description: Discount in basis points (10000 = 100%)
        floor_lamports:
          type: integer
          minimum: 0
          description: Minimum fee floor (COGS floor of 5000 lamports always applies in code)
        reason:
          type: string
        valid_from:
          type: string
          format: date-time
        valid_until:
          type: string
          format: date-time
          nullable: true
        created_by:
          type: string
          description: Admin key ID that created this subsidy
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    IdentityClassFlag:
      type: object
      description: An identity class complaint flag submitted by one agent against another.
      required: [id, reporter_hash, target_hash, claimed_class, asserted_class, reason, status, created_at]
      properties:
        id:
          type: string
          format: uuid
        reporter_hash:
          type: string
          description: Hash of the agent that submitted the flag
        target_hash:
          type: string
          description: Hash of the flagged agent
        claimed_class:
          type: string
          enum: [narrow_task, autonomous, orchestrator]
          description: The identity class the target currently claims
        asserted_class:
          type: string
          enum: [narrow_task, autonomous, orchestrator]
          description: What the reporter believes the correct class should be
        reason:
          type: string
          description: Reporter's explanation
        status:
          type: string
          enum: [pending, sustained, dismissed]
        reviewed_by:
          type: string
          nullable: true
          description: Admin key ID that reviewed this flag
        review_reason:
          type: string
          nullable: true
          description: Admin's explanation for the resolution
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    Passport:
      type: object
      description: >
        Ed25519-signed passport — the agent's portable, offline-verifiable credential.
        Verify by reconstructing canonical JSON (deterministic field ordering) and checking
        the Ed25519 signature against the authority public key.
      required: [format_version, agent_hash, authority_key_id, registered_at_beat, last_heartbeat_beat, lineage_chain_hash, issued_at, valid_until, provenonce_signature]
      properties:
        format_version:
          type: integer
          description: Canonical format version (1 = v1 frozen at mainnet)
          enum: [1]
        agent_hash:
          type: string
          description: Agent's identity hash (0x-prefixed SHA-256)
        agent_public_key:
          type: string
          nullable: true
          description: Wallet address for key-binding (null for no-wallet agents)
        authority_key_id:
          type: string
          description: Signing key identifier (e.g., pvn-ed25519-3b328421fa5bf3f5)
        identity_class:
          type: string
          nullable: true
          enum: [narrow_task, autonomous, orchestrator]
          description: SIGIL identity class (null for SIGIL-less descendants)
        registered_at_beat:
          type: integer
        sigil_issued_at_beat:
          type: integer
          nullable: true
        last_heartbeat_beat:
          type: integer
        lineage_chain_hash:
          type: string
        issued_at:
          type: integer
          description: Unix ms — proof issuance time
        valid_until:
          type: integer
          description: Unix ms — proof expiry (default +24h)
        provenonce_signature:
          type: string
          description: Hex-encoded Ed25519 signature over canonical proof data

    ProvenoncePassportVC:
      type: object
      description: >
        W3C Verifiable Credential envelope wrapping a Passport.
        The `proof.proofValue` is the same hex Ed25519 signature as `passport.provenonce_signature`.
        Uses `cryptosuite: eddsa-provenonce-2026` (Ed25519 over canonical JSON, not standard linked data proofs).
      properties:
        '@context':
          type: array
          items:
            type: string
        type:
          type: array
          items:
            type: string
        issuer:
          type: object
          properties:
            id:
              type: string
            authority_key_id:
              type: string
        issuanceDate:
          type: string
          format: date-time
        expirationDate:
          type: string
          format: date-time
        credentialSubject:
          type: object
          properties:
            id:
              type: string
            agent_hash:
              type: string
            agent_public_key:
              type: string
              nullable: true
            identity_class:
              type: string
              nullable: true
            registered_at_beat:
              type: integer
            sigil_issued_at_beat:
              type: integer
              nullable: true
            last_heartbeat_beat:
              type: integer
            lineage_chain_hash:
              type: string
        proof:
          type: object
          properties:
            type:
              type: string
            created:
              type: string
              format: date-time
            verificationMethod:
              type: string
            proofPurpose:
              type: string
            cryptosuite:
              type: string
            proofValue:
              type: string
              description: Hex-encoded Ed25519 signature
        format_version:
          type: integer

    WorkProofReceipt:
      type: object
      description: >
        Signed CPU work-proof receipt issued by the Beats service (`POST /api/v1/beat/work-proof`).
        Used as authorization proof for spawn (Step 1) and resync operations.
        The `signature` field is an Ed25519 signature over the receipt payload using the
        Beats work-proof signing key (retrievable at `GET /api/v1/beat/key`).
      required: [beats_verified, anchor_index, signature]
      properties:
        beats_verified:
          type: integer
          description: Number of CPU beats verified in this receipt
        anchor_index:
          type: integer
          description: Global anchor index at time of proof submission
        from_hash:
          type: string
          description: Hash of the first beat in the proved chain
        to_hash:
          type: string
          description: Hash of the last beat in the proved chain
        difficulty:
          type: integer
        utc:
          type: string
          format: date-time
          description: ISO 8601 timestamp of proof submission
        signature:
          type: string
          description: Hex-encoded Ed25519 signature over the receipt payload
        type:
          type: string
          enum: [work_proof]

    Beat:
      type: object
      properties:
        index:
          type: integer
        hash:
          type: string
          pattern: '^[0-9a-f]{64}$'
        prev:
          type: string
          pattern: '^[0-9a-f]{64}$'
        timestamp:
          type: integer
          description: Unix timestamp in milliseconds
        nonce:
          type: string
          nullable: true
        anchor_hash:
          type: string
          nullable: true

    GlobalAnchor:
      type: object
      properties:
        beat_index:
          type: integer
        hash:
          type: string
        prev_hash:
          type: string
        utc:
          type: integer
          description: UTC timestamp in milliseconds
        difficulty:
          type: integer
        epoch:
          type: integer

    BeatProof:
      type: object
      required: [from_beat, to_beat, from_hash, to_hash, beats_computed, spot_checks]
      properties:
        from_beat:
          type: integer
        to_beat:
          type: integer
        from_hash:
          type: string
        to_hash:
          type: string
        beats_computed:
          type: integer
          maximum: 10000
        global_anchor:
          type: integer
        anchor_hash:
          type: string
          description: Required when global anchors exist
        spot_checks:
          type: array
          description: Must include to_beat index. Minimum 3 checks.
          items:
            type: object
            required: [index, hash, prev]
            properties:
              index:
                type: integer
              hash:
                type: string
              prev:
                type: string
                description: Required for hash-chain recomputation
              nonce:
                type: string

    RegisterNoWalletRequest:
      type: object
      required: [name]
      description: Single-phase registration without wallet (D-62). Identity only.
      properties:
        name:
          type: string
          maxLength: 64
        metadata:
          type: object
          description: Optional agent metadata (max 4KB)

    RegisterChallengeRequest:
      type: object
      required: [name, action]
      properties:
        name:
          type: string
          maxLength: 64
        action:
          type: string
          enum: [challenge]
        wallet_model:
          type: string
          enum: [self-custody, operator]
          default: self-custody
        wallet_chain:
          type: string
          enum: [solana, ethereum]
          default: solana
          description: Target blockchain for wallet registration

    RegisterModelARequest:
      type: object
      required: [name, wallet_public_key, wallet_signature, wallet_nonce]
      properties:
        name:
          type: string
          maxLength: 64
        wallet_public_key:
          type: string
          description: 64-char hex Ed25519 public key (32 bytes)
        wallet_signature:
          type: string
          description: Hex-encoded Ed25519 signature
        wallet_nonce:
          type: string
        metadata:
          type: object
          description: Optional agent metadata (max 4KB)

    RegisterModelBRequest:
      type: object
      required: [name, wallet_model, operator_wallet_address, wallet_signature, wallet_nonce]
      properties:
        name:
          type: string
          maxLength: 64
        wallet_model:
          type: string
          enum: [operator]
        operator_wallet_address:
          type: string
          description: Base58 Solana public key
        wallet_signature:
          type: string
          description: Hex-encoded Ed25519 signature
        wallet_nonce:
          type: string
        metadata:
          type: object

    RegisterEthereumRequest:
      type: object
      required: [name, wallet_chain, wallet_address, wallet_signature, wallet_nonce]
      description: Ethereum bring-your-own wallet registration (D-63)
      properties:
        name:
          type: string
          maxLength: 64
        wallet_chain:
          type: string
          enum: [ethereum]
        wallet_address:
          type: string
          description: Ethereum address (0x + 40 hex chars)
          pattern: '^0x[0-9a-fA-F]{40}$'
        wallet_signature:
          type: string
          description: EIP-191 personal_sign signature (hex)
        wallet_nonce:
          type: string
        metadata:
          type: object

    RegisterChildRequest:
      type: object
      required: [parent, name]
      properties:
        parent:
          type: string
          pattern: '^0x[0-9a-f]{64}$'
        name:
          type: string
          maxLength: 64
        metadata:
          type: object

    ChallengeResponse:
      type: object
      properties:
        ok:
          type: boolean
        action:
          type: string
          enum: [challenge]
        nonce:
          type: string
        expires_in:
          type: integer
          description: Seconds until nonce expires
        wallet_model:
          type: string

    RegistrationResponse:
      type: object
      properties:
        ok:
          type: boolean
        hash:
          type: string
          description: Agent identity hash (0x + 64 hex)
        type:
          type: string
          enum: [root, agent]
        parent:
          type: string
          nullable: true
        depth:
          type: integer
        name:
          type: string
        metadata:
          type: object
          nullable: true
        api_key:
          type: string
          description: Agent API key (pvn_ prefix)
        secret:
          type: string
          description: Agent secret (pvs_ prefix)
        signature:
          type: string
          description: Solana transaction signature
        wallet_model:
          type: string
          enum: [self-custody, operator]
        wallet_chain:
          type: string
          nullable: true
          enum: [solana, ethereum]
          description: Blockchain for wallet (null for no-wallet)
        wallet:
          type: object
          nullable: true
          properties:
            address:
              type: string
            chain:
              type: string
            model:
              type: string
            solana_address:
              type: string
              description: Backward compat (Solana wallets only)
        explorer_url:
          type: string

    BatchVerifyRequest:
      type: object
      required: [hashes]
      properties:
        hashes:
          type: array
          items:
            type: string
            pattern: '^0x[0-9a-f]{64}$'
          minItems: 1
          maxItems: 50
          description: Agent hashes to verify (1–50). Duplicates are deduplicated.

    BatchVerifyResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/VerifyResult'
          description: One result per unique hash (after dedup)
        summary:
          type: object
          properties:
            total:
              type: integer
              description: Number of unique hashes processed
            verified:
              type: integer
              description: Count of hashes that were successfully verified
            not_found:
              type: integer
              description: Count of hashes not found

    VerifyResult:
      type: object
      description: |
        Result for a single agent in a batch verify response.
        When verified=false, only error/code/hash fields are present.
        When verified=true, matches VerifyResponse shape (without lineage chain).
      properties:
        verified:
          type: boolean
        error:
          type: string
          description: Present when verified=false
        code:
          type: string
          description: Error code when verified=false (e.g., AGENT_NOT_FOUND)
        hash:
          type: string
          description: The agent hash (present when verified=false)
        standing:
          type: string
          enum: [registered, active, warning, frozen, stale, refused]
        agent:
          type: object
        sigil:
          type: object
          nullable: true
        passport:
          nullable: true
        lineage_proof:
          nullable: true
        beat_state:
          type: object
          nullable: true
        network:
          type: object
        lineage:
          type: array
          items:
            type: object
        root:
          type: object
        lineage_depth:
          type: integer

    VerifyResponse:
      type: object
      properties:
        verified:
          type: boolean
        standing:
          type: string
          enum: [registered, active, warning, frozen, stale, refused]
          description: |
            - `active`: Checked in within deadline
            - `warning`: Approaching deadline (>45 anchors since last check-in)
            - `frozen`: Missed deadline, requires resync
            - `registered`: No beat chain initialized
            - `stale`: Heartbeat lapsed (no recent heartbeat)
            - `refused`: Agent refused or revoked
        agent:
          type: object
          properties:
            hash:
              type: string
            type:
              type: string
              enum: [root, agent]
            parent:
              type: string
              nullable: true
            depth:
              type: integer
            name:
              type: string
              nullable: true
            metadata:
              type: object
              nullable: true
            wallet:
              type: string
              nullable: true
            wallet_model:
              type: string
              enum: [self-custody, operator]
            registered:
              type: string
              format: date-time
              nullable: true
            birth_tx:
              type: string
              description: Solana transaction signature
            explorer_url:
              type: string
              nullable: true
        sigil:
          type: object
          nullable: true
          description: |
            SIGIL identity section. Null if agent has no SIGIL.
            When metadata_visibility is 'private', mutable fields (substrate, capability,
            endpoint, tools, etc.) are redacted. Governance fields are always visible.
          properties:
            sigil:
              type: string
              description: Full SIGIL string (name*principal*tier)
            sigil_name:
              type: string
              nullable: true
            principal:
              type: string
              nullable: true
            tier:
              type: string
              nullable: true
              enum: [sov, org, ind, eph, sbx]
            identity_class:
              type: string
            issued_at:
              type: string
              format: date-time
              nullable: true
            birth_tx:
              type: string
              nullable: true
            explorer_url:
              type: string
              nullable: true
            substrate:
              type: string
              description: Redacted when metadata_visibility is private
            substrate_provider:
              type: string
            substrate_model:
              type: string
            capability:
              type: string
            capability_scope:
              type: string
            generation:
              type: integer
            protocol:
              type: string
            endpoint:
              type: string
            compliance_regime:
              type: string
            tools:
              type: array
              items:
                type: string
            modality_input:
              type: array
              items:
                type: string
            modality_output:
              type: array
              items:
                type: string
            metadata_visibility:
              type: string
              enum: [private]
              description: Only present when visibility is 'private' (signals mutable fields were redacted)
            economic_commitment:
              type: boolean
              description: |
                True for paid tiers (sov, org, ind, eph), false for sandbox (sbx).
                Indicates whether the agent has made an economic commitment via SIGIL purchase.
            identity_class_contested:
              type: boolean
              description: |
                True if at least one sustained identity class flag exists against this agent.
                Only present when agent has a SIGIL. Flags require admin review — pending flags have no public effect.
        beat_state:
          type: object
          nullable: true
          properties:
            beat_count:
              type: integer
              description: Lifetime beats (total accumulated liveness events).
            lifetime_beats:
              type: integer
              description: Lifetime beats (total accumulated liveness events).
            available_beats:
              type: integer
              description: Spendable beat balance for spawn eligibility.
            last_checkin:
              type: string
              format: date-time
              nullable: true
              description: ISO timestamp of most recent check-in
            last_checkin_beat:
              type: integer
            last_global_sync:
              type: integer
            difficulty:
              type: integer
            status:
              type: string
              enum: [active, frozen, revoked, gestating]
            frozen_at:
              type: string
              format: date-time
              nullable: true
        network:
          type: object
          properties:
            current_anchor_index:
              type: integer
              description: Latest global anchor beat index
            uptime_seconds:
              type: integer
              nullable: true
              description: Seconds since registration
            anchor_degraded:
              type: boolean
              description: |
                True when the global anchor chain is stale (age exceeds MAX_ANCHOR_GAP_MINUTES,
                default 10min). Indicates the network is experiencing Beats/Solana degradation.
        lineage:
          type: array
          description: Parent chain from agent to root
          items:
            type: object
            properties:
              hash:
                type: string
              type:
                type: string
              depth:
                type: integer
              name:
                type: string
                nullable: true
              registered:
                type: string
                format: date-time
                nullable: true
        root:
          type: object
          properties:
            hash:
              type: string
            registered_by:
              type: string
              nullable: true
            registered:
              type: string
              format: date-time
              nullable: true
        lineage_depth:
          type: integer

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            type: object
            required: [error, code]
            properties:
              error:
                type: string
              code:
                type: string
                description: Machine-readable error code (e.g., VALIDATION, INVALID_HASH, PAYMENT_REQUIRED)
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            type: object
            required: [error, code]
            properties:
              error:
                type: string
              code:
                type: string
                description: Machine-readable error code (AUTH_MISSING or AUTH_INVALID)
              sigil_required:
                type: boolean
                deprecated: true
                description: "Deprecated: use code: SIGIL_REQUIRED instead"
    UnsupportedMediaType:
      description: Content-Type must be application/json
      content:
        application/json:
          schema:
            type: object
            required: [error, code]
            properties:
              error:
                type: string
              code:
                type: string
                description: Machine-readable error code (CONTENT_TYPE_REQUIRED)
    RateLimited:
      description: Too many requests
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until rate limit resets
      content:
        application/json:
          schema:
            type: object
            required: [error, code]
            properties:
              error:
                type: string
              code:
                type: string
                description: Machine-readable error code (RATE_LIMITED, HEARTBEAT_TOO_SOON, VOLUME_CAP_REACHED, or SPAWN_LIMIT_REACHED)
              retry_after_ms:
                type: integer
                description: Milliseconds until rate limit resets
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            type: object
            required: [error, code]
            properties:
              error:
                type: string
              code:
                type: string
                description: Machine-readable error code (SERVER_ERROR, DB_ERROR, or DB_NOT_CONFIGURED)


