{
  "openapi": "3.1.0",
  "info": {
    "title": "ECCA-AI public API",
    "version": "1.0.0",
    "summary": "Multi-engine answer verification: dispatch, adjudication, proof permalinks, keys, and checkout.",
    "description": "ECCA-AI runs a question through several independent AI engines, then adjudicates their answers into one result and shows the reasoning that settled it. This document is derived from the route handlers under api/ and is pinned by ecca_openapi_contract_test.js: every path maps to a handler file, and every documented error code is a literal the handler (or a module it requires) actually returns. Limits stated here are the code's own constants (client/ecca_tier_limits.js, lib/api_prelude.js, lib/ecca_mcp_server.js). Rate buckets are held in memory per serving instance, so they are an abuse brake, not a global guarantee. No uptime or latency service level is measured or promised. Academic reasoning and dialectic evaluation studio only, not a medical device.",
    "termsOfService": "https://eccaai.com/faq",
    "contact": {
      "name": "ECCA-AI",
      "url": "https://eccaai.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://eccaai.com",
      "description": "Production (sole canonical origin)"
    }
  ],
  "tags": [
    {
      "name": "solve",
      "description": "Engine dispatch and adjudication. Spends solve quota."
    },
    {
      "name": "mcp",
      "description": "Model Context Protocol over HTTP (JSON-RPC 2.0)."
    },
    {
      "name": "proof",
      "description": "Durable /p/<id> adjudicated-answer permalinks."
    },
    {
      "name": "keys",
      "description": "Self-serve API key lifecycle (session cookie)."
    },
    {
      "name": "billing",
      "description": "Checkout and entitlement (session cookie)."
    },
    {
      "name": "status",
      "description": "Config-truth probes. No secrets, no side effects."
    },
    {
      "name": "operator",
      "description": "Operator-only surfaces. Anything without the operator session is an indistinguishable 404."
    },
    {
      "name": "radar",
      "description": "Pre-dispatch topic lookups over consented records. Zero engine calls, zero spend."
    }
  ],
  "x-ecca-quotas": {
    "source": "client/ecca_tier_limits.js, lib/api_prelude.js, lib/ecca_mcp_server.js, lib/ratelimit.js",
    "freeDailySolves": 5,
    "freeDailyDeepSolves": 1,
    "freeDailyProofShares": 10,
    "freeDailyProofSharesPerIp": 30,
    "proWindowHours": 5,
    "proWindowFullSolves": 20,
    "freeStreamMaxOutputBytes": 32768,
    "proStreamMaxOutputBytes": 262144,
    "abuseBrakePerMinutePerIp": 60,
    "keyedRequestsPerMinutePerKeyId": 120,
    "resetsAt": "00:00 UTC (the quota key is the UTC date)",
    "scope": "Durable free daily pool per account for signed-in sessions and free-tier keys, or per client IP for anonymous web requests; every /api/mcp tool call is keyed (the IP pool is not reachable there), shared across solve surfaces. Pro has an account-wide five-hour window of deep checks starting with the first counted solve, shared by sessions and account-linked keys; subsequent attempts do not extend its reset. The usage snapshot includes used, limit, remaining, meter and resetAt; a full window returns 429 and Retry-After without an upsell. Enterprise and owner are outside this Pro counter, but service safety and capacity limits still apply. Per-minute abuse brakes remain per instance."
  },
  "x-ecca-error-catalog": {
    "bad-op": "body.op is not create or revoke.",
    "bad-state": "state is present but not an opaque token matching ^[A-Za-z0-9_-]{1,64}$.",
    "check-requires-candidate": "mode check needs body.candidate, the answer another assistant gave, as non-empty text; nothing was dispatched.",
    "empty-prompt": "prompt (q on /api/trap-radar) is missing or blank after trimming; nothing was dispatched.",
    "empty-proof-payload": "prompt is missing, or neither answer nor synthesis was supplied.",
    "Invalid MCP request": "Malformed MCP envelope, unknown method, or unsupported protocol version (400); disallowed Origin (403). A tool rejecting its input answers -32601 with its own message.",
    "invalid-checkout-attempt": "checkoutAttemptId is present but does not match ^[A-Za-z0-9_-]{8,128}$.",
    "invalid-document": "The supplied base64 data or PDF header is invalid.",
    "invalid-id": "The proof id is not 4-64 characters of [a-zA-Z0-9_-].",
    "invalid-key-id": "keyId is not 8-64 lowercase hex characters.",
    "invalid-plan": "plan is not one of the four eccaai-* plans.",
    "Missing required field 'query' or 'prompt'": "Neither query nor prompt was supplied.",
    "missing-image": "imageBase64 is empty after stripping any data: prefix.",
    "tournament-requires-options": "mode tournament needs body.options with at least two candidates; no default ranking is served.",
    "unsupported-image-type": "mimeType is not image/jpeg, image/png or image/webp.",
    "api-key-required": "A /api/mcp tools/call carried no API key (JSON-RPC -32001, data.reason key-required; the message names the free key). Every MCP tool call needs one; a free key is one click on /developers after sign-in, no card. initialize, ping and tools/list stay open.",
    "invalid-api-key": "An ecca_sk_ key was presented and failed signature verification (or keys are not configured on this deployment). Never a silent downgrade to anonymous.",
    "key-revoked": "The key verifies but is on the revocation denylist.",
    "unauthorized": "No verified session cookie (or, on /api/export-traces, the bearer does not match the export token).",
    "credits-required": "A valid free-tier key has used its 5 daily solves; a paid key tier continues.",
    "upgrade-required": "Publishing a proof permalink into search needs the ecca-proof-links entitlement (Pro); minting and unpublishing never do.",
    "sign-in-required": "Minting a proof link, or publishing one, needs a signed-in session (publishing: the owner's).",
    "proof-share-limit": "A free account, or its client network, has minted today's free proof links (x-ecca-quotas.freeDailyProofShares per account, freeDailyProofSharesPerIp per network). Resets at 00:00 UTC; Retry-After names the wait. Links already minted keep working.",
    "proof-share-limit-unavailable": "The free proof-share counter could not be read, so nothing was minted. Retry shortly.",
    "not-owner": "Only the account that minted a proof can publish or unpublish it.",
    "entitlement-unavailable": "The account tier could not be read; nothing was minted. Retry shortly.",
    "proof-read-failed": "The proof to publish could not be read; nothing was changed.",
    "proof-index-unreadable": "The owner's proof index could not be read, so the new proof could not be made purgeable; the mint was rolled back.",
    "proof-index-full": "The owner's proof index is at its cap; the mint was rolled back.",
    "proof-index-write-failed": "The owner's proof index could not be written; the mint was rolled back.",
    "checkout-not-enabled": "The requested plan is mapped but is not a live public checkout plan.",
    "checkout-not-rolled-out": "The server-side rollout stage (off by default) does not admit this caller; the body names the stage.",
    "forbidden-origin": "Key mutations require an allowlisted Origin header in addition to the session.",
    "key-not-found": "keyId is not one of the caller's keys. The same answer whether it exists elsewhere or not.",
    "no-key": "The account has neither a plan-provisioned key nor any self-serve keys yet.",
    "Not found": "Operator routes answer a plain-text 404 for every request without the operator session, wrong token, or wrong method.",
    "proof-not-found": "No persisted record exists at that id; nothing is substituted.",
    "method-not-allowed": "The HTTP method is not in the route's allowlist.",
    "already-subscribed": "The account already holds the requested tier or higher (body carries ownedTier); no Session is created.",
    "key-limit-reached": "The account's active-key cap is reached (the body's limit field states it); revoke one first.",
    "image-too-large": "imageBase64 exceeds 8000000 characters.",
    "document-too-large": "The PDF exceeds about 3 MB, or the extracted text exceeds 45000 characters.",
    "payload-too-large": "The request body exceeds the route's byte cap (4001024 on /api/document-ingest, 16777216 on /api/ecca-router, 1048576 on /api/proof-link, 2048 on /api/ecca-key, 262144 on /api/mcp, 4096 on /api/trap-radar, whose q is also capped at 500 characters).",
    "unreadable-document": "The PDF could not be parsed or has no readable text.",
    "free-daily-limit": "The free daily pool (5 solves per signed-in account or anonymous client IP) is spent; it resets at 00:00 UTC.",
    "pro-window-limit": "The Pro account window of deep checks is full. Retry-After and resetAt name its reset; solves and meter report the enforced count. No purchase is needed to reset it.",
    "key-rate-limited": "The key id exceeded 120 requests in the current minute.",
    "rate-limited": "The per-route abuse brake (60 requests per minute per client IP, per serving instance) tripped; retry_after_s names the window.",
    "Document ingestion failed": "Text extraction or the audit failed unexpectedly; internal details are not returned.",
    "internal-server-error": "An unexpected failure inside the key route.",
    "mint-error": "The freshly minted key failed its own verification; nothing was stored.",
    "store-write-failed": "The durable store refused the proof write.",
    "ai-unavailable": "Engines were configured and dispatched but none returned an answer.",
    "key-write-failed": "The durable store refused the key-list write; no key was created.",
    "revoke-failed": "The denylist write failed; the key is still active.",
    "stripe-error": "Stripe refused to create the Checkout Session; the reason stays in server logs.",
    "stripe-price-unverified": "Stripe could not be reached to verify the configured price before creating a Session.",
    "stripe-unreachable": "The Stripe request itself failed.",
    "transcription-unavailable": "The vision call returned nothing readable; no problem text is guessed.",
    "ai-not-configured": "Fail-closed: no engine is configured on this deployment.",
    "billing-catalog-mismatch": "Fail-closed: the configured Stripe price disagrees with the published catalog; mismatches lists the fields.",
    "billing-not-configured": "Fail-closed: billing is not configured for the requested plan.",
    "engines-cooling": "Every configured engine's circuit is open after repeated failures; retryAfterMs is the measured horizon.",
    "export-not-configured": "No export token is configured on this deployment.",
    "keys-not-configured": "No key-signing secret is configured on this deployment, so no key can be minted.",
    "probe-unavailable": "The capability probe threw; no map is returned so no badge is demoted.",
    "proof-links-unavailable": "Fail-closed: proof persistence is not configured on this deployment.",
    "spend-ceiling-reached": "The operator's daily engine-dispatch ceiling (only when one is configured) is reached; service resumes when the day rolls over.",
    "store-unconfigured": "Fail-closed: no durable store on this deployment.",
    "vision-not-configured": "No vision tier on this deployment; no network attempt is made.",
    "owner-monthly-cap": "MCP engine work is refused when owner monthly spend reaches its cap or is unknown. No further engine stage starts. Local tools and research remain available within service limits."
  },
  "components": {
    "securitySchemes": {
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ecca_sk_<payload>.<signature>",
        "description": "Signed stateless ECCA API key. Only values that start with ecca_sk_ are treated as keys; on /api/mcp a non-ECCA bearer is refused with 401 invalid-api-key; other routes may fall through as anonymous. A presented key that fails verification is an explicit 401, never a silent downgrade. Keys are minted on a Pro or Enterprise checkout, or created on /developers by any signed-in account: a free account holds one FREE-tier key (its daily pool per account), paid accounts have a higher cap reported by activeKeyLimit. Every /api/mcp tool call requires a key."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-ecca-api-key",
        "description": "Alternative carrier for the same ecca_sk_ key."
      },
      "SessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "HttpOnly same-origin session cookie set by /api/auth after sign-in on eccaai.com. Bearer headers are ignored on session-only routes. Mutations on /api/ecca-key additionally require an allowlisted Origin header."
      },
      "OperatorSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-ecca_ops",
        "description": "Operator-only. Installed by opening /ops/status?token=<operator token> once; every request without it is a plain 404."
      },
      "ExportToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Operator export token for /api/export-traces. Gated: the route is 503 until the deployment configures it."
      }
    },
    "schemas": {
      "AtomicClaimRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema",
          "id",
          "url",
          "sealed",
          "checkedAt",
          "engineCount",
          "measured",
          "splitCount",
          "claims",
          "limitation"
        ],
        "properties": {
          "schema": {
            "const": "ecca.claim-verification.v1"
          },
          "id": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{4,64}$"
          },
          "url": {
            "type": "string",
            "pattern": "^https://eccaai\\.com/p/[a-zA-Z0-9_-]{4,64}$"
          },
          "sealed": {
            "type": "boolean"
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "engineCount": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "measured": {
            "type": "boolean"
          },
          "splitCount": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "claims": {
            "type": "array",
            "maxItems": 72,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "id",
                "pointId",
                "text",
                "variants",
                "comparison",
                "category",
                "tie",
                "engines",
                "checks",
                "sources"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^claim-[a-f0-9]{24}$"
                },
                "pointId": {
                  "type": "string",
                  "pattern": "^point-[a-f0-9]{24}$"
                },
                "text": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 400
                },
                "variants": {
                  "type": "array",
                  "minItems": 1,
                  "maxItems": 12,
                  "items": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "engineId",
                      "text"
                    ],
                    "properties": {
                      "engineId": {
                        "type": "string",
                        "enum": [
                          "speed",
                          "depth",
                          "breadth",
                          "logic",
                          "context",
                          "agentic"
                        ]
                      },
                      "text": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 400
                      }
                    }
                  }
                },
                "comparison": {
                  "type": "string",
                  "enum": [
                    "split",
                    "agreement"
                  ]
                },
                "category": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "factual",
                    "interpretive",
                    "scope",
                    "ambiguity",
                    "temporal",
                    "hallucination",
                    null
                  ]
                },
                "tie": {
                  "type": "boolean"
                },
                "engines": {
                  "type": "array",
                  "minItems": 1,
                  "maxItems": 6,
                  "items": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "engineId",
                      "number",
                      "position"
                    ],
                    "properties": {
                      "engineId": {
                        "type": "string",
                        "enum": [
                          "speed",
                          "depth",
                          "breadth",
                          "logic",
                          "context",
                          "agentic"
                        ]
                      },
                      "number": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 6
                      },
                      "position": {
                        "type": "string",
                        "enum": [
                          "agrees",
                          "disagrees",
                          "not-measured"
                        ]
                      }
                    }
                  }
                },
                "checks": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "evidence",
                    "logic",
                    "assumptions"
                  ],
                  "properties": {
                    "evidence": {
                      "const": "untested"
                    },
                    "logic": {
                      "const": "untested"
                    },
                    "assumptions": {
                      "const": "untested"
                    }
                  }
                },
                "sources": {
                  "type": "array",
                  "maxItems": 0,
                  "description": "No sealed claim-source binding exists in legacy proofs. Answer-level URLs are not attached to a claim by inference."
                }
              }
            }
          },
          "limitation": {
            "const": "Agreement is not proof. Open the sources."
          }
        }
      },
      "ProofCitationRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema",
          "id",
          "url",
          "available",
          "checkedAt",
          "counts",
          "apa",
          "mla",
          "reason"
        ],
        "properties": {
          "schema": {
            "const": "ecca.proof-citation.v1"
          },
          "id": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{4,64}$"
          },
          "url": {
            "type": "string",
            "pattern": "^https://eccaai\\.com/p/[a-zA-Z0-9_-]{4,64}$"
          },
          "available": {
            "type": "boolean"
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "counts": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "compared",
              "split"
            ],
            "properties": {
              "compared": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 0
              },
              "split": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 0
              }
            }
          },
          "apa": {
            "type": [
              "string",
              "null"
            ]
          },
          "mla": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              null,
              "unsealed",
              "seal-date-unavailable",
              "question-unavailable"
            ]
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Present on some routes; always false on an error."
          },
          "error": {
            "type": "string",
            "description": "Stable machine-readable code. See x-ecca-error-codes on each response."
          },
          "message": {
            "type": "string"
          },
          "upgradeUrl": {
            "type": "string",
            "examples": [
              "/pricing"
            ]
          },
          "retry_after_s": {
            "type": "integer",
            "description": "Seconds until the abuse brake window rolls (rate-limited only)."
          },
          "limit": {
            "type": "integer",
            "description": "The cap that was hit (free-daily-limit, key-limit-reached)."
          }
        },
        "additionalProperties": true
      },
      "JsonRpcError": {
        "type": "object",
        "required": [
          "jsonrpc",
          "error"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0"
          },
          "id": {
            "description": "Echo of the caller's request id, or null."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "enum": [
                  -32000,
                  -32001,
                  -32002,
                  -32600,
                  -32601
                ]
              },
              "message": {
                "type": "string"
              },
              "data": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "upgradeUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "Engine": {
        "type": "object",
        "description": "One registered engine role. Identity only (id/name/role) on the registry read; live telemetry (status/ok/ms) after a dispatch. The registry holds up to 6 roles; how many answered is measured per response in meta.enginesLive.",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "speed",
              "depth",
              "breadth",
              "logic",
              "context",
              "agentic"
            ]
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "live",
              "gated",
              "unavailable",
              "unconfigured"
            ]
          },
          "ok": {
            "type": "boolean"
          },
          "ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "receipt": {
            "type": "string"
          }
        }
      },
      "RegistryResponse": {
        "type": "object",
        "required": [
          "ok",
          "providers",
          "count"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Engine"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "required": [
          "ok",
          "tier",
          "unlimited"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "tier": {
            "type": "string"
          },
          "unlimited": {
            "type": "boolean",
            "description": "Legacy compatibility flag for enterprise/owner exemptions from this counter; false for Pro. It is not a promise of unbounded service."
          },
          "solves": {
            "type": "object",
            "description": "Free daily count or Pro account window, read from the same durable counter the POST path enforces. Reads never spend quota or start a window. Pro adds windowHours, resetAt, retryAfter and meter; resetAt is null before the first counted solve.",
            "properties": {
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              }
            }
          }
        }
      },
      "SolveRequest": {
        "type": "object",
        "required": [
          "prompt"
        ],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "The question. Truncated to 64000 characters for free callers and 500000 for paid callers; an empty prompt is a 400 empty-prompt."
          },
          "context": {
            "type": "object",
            "description": "Optional context the engines see with the question, as fields: turns (up to the last 3 earlier {q, a} exchanges) and document ({label, text}, text up to 8000 characters). The server builds the engine prompt from them. A shared proof is sealed over the typed question plus this context, stores the context as the engines saw it (capped), and shows it on the public page under What the engines also saw and in /p/<id>.json.",
            "properties": {
              "turns": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "q": {
                      "type": "string"
                    },
                    "a": {
                      "type": "string"
                    }
                  }
                }
              },
              "document": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "depth": {
            "type": "string",
            "description": "\"fast\" routes to one cost-optimal engine; \"deep\" is a multi-engine dispatch: paid callers get the full configured matrix; free callers start with two engines and expand on measured divergence, and after the free daily deep solve is spent the request is downgraded to fast with meta.downgraded=true. Accepted aliases: intuitive (fast); standard, executive, pioneer (deep). Any other value is a 400 invalid-depth before any quota is spent.",
            "enum": [
              "fast",
              "deep",
              "intuitive",
              "standard",
              "executive",
              "pioneer"
            ],
            "examples": [
              "fast",
              "deep"
            ]
          },
          "mode": {
            "type": "string",
            "description": "Answer posture. cothink and socratic are chat lanes that always meter through the deep ladder. stress is a Socratic deep solve plus three probe variants of the question (asked again fresh with the question verbatim, its opposite, one premise changed), dispatched together; it is charged as STRESS_TEST_SOLVE_COST solves (client/ecca_tier_limits.js: the main solve plus one per probe) against the caller's free daily solves and free deep solve, or Pro usage-window solves, all reserved atomically before dispatch or none. If the main solve returns an error, every reserved solve (and the free deep solve) is given back and the error carries stressRefund; a reader who disconnects (client-aborted) after the engines started keeps the charge. It returns meta.stressTest (schema ecca.stress-test.v1): each main-answer claim with how many probes it survives (a matching claim with the same stance in that probe's answers; any matching claim with the opposite stance flips it; a failed probe never counts). Matching is lexical (shared words plus a count of negation words), not semantic. stress is refused with a 400 on depth fast, with a niche, with an adjudication mode, and with an API key. debate runs the question as a live three-round debate: round 1 is the normal deep dispatch (each engine that answers is a debater); in round 2 each debater sees only the other debaters' extracted central claims plus the single largest claim split as the debate question and answers in at most 120 words; in round 3 each returns structured JSON (changed_mind yes/no, a one-sentence final_position, side A/B/neither, dropped_claims naming ids of its own claims it withdraws; ids it was never shown are ignored). Rounds 2 and 3 are each cut off at DEBATE_ROUND_SECONDS (client/ecca_tier_limits.js, at most 25 s); a late engine is recorded as late. It is charged as DEBATE_SOLVE_COST solves, reserved atomically before dispatch like stress; a failed round 1 gives every solve back (debateRefund), and a round no engine answered is given back. It returns meta.debate (schema ecca.debate-live.v1): the rounds, a verdict computed only from the structured fields (changedMind, heldOut, noFinal, claimsDropped, split), the per-engine dropped counts, and finalAnswer: the engine whose final position dropped the fewest of its own claims, ties broken by not having changed its mind (DEBATE_HELD_UP_KEY), among engines shown at least one claim of their own; a remaining tie is reported as a tie, never one answer, and with no qualifying engine there is no final answer. Claims that name a round 3 field or steer the other engines are never shown to another engine; rounds 2 and 3 answer in the question's language. On a stream, debate_round, debate_turn and debate_done events arrive as each round starts and each engine lands. debate is refused with a 400 on depth fast, with a niche, with an adjudication mode, and with an API key. verify, tournament, premortem, audit and check are adjudication modes that also attach a deterministic modeReport. check reruns the question alone (the candidate is never shown to the engines) and returns check: the panel's canonical final answer against the candidate's. tutor is a Socratic solve that writes a hint ladder (Hint, Bigger hint, Show next step, Show answer); it never streams and returns tutor (schema ecca.tutor.v1): an id and the steps' kinds and labels only, never their text. tutor-reveal with tutorId and level returns exactly one step's text, in order (an unopened later step is a 409 tutor-step-order); it dispatches nothing and spends no quota. practice asks for PRACTICE_PROBLEMS similar problems (client/ecca_tier_limits.js), never streams, is charged as PRACTICE_SOLVE_COST solves, and returns practice (schema ecca.practice.v1): each problem with its answer and, when the free calculator checked it, check calculator-holds or calculator-fails. exam-topics reads prompt as a syllabus or study guide (at most EXAM_SYLLABUS_MAX_CHARS characters; blank is a 400 exam-topics-empty, too long a 413 exam-syllabus-too-long, and any context field a 400 exam-topics-no-context, all before any quota is spent), always runs at fast depth, never streams, is charged as EXAM_TOPICS_SOLVE_COST fast solves (never the free deep solve), and returns exam (schema ecca.exam-topics.v1): at most EXAM_MAX_TOPICS topic names and nothing else; when no topics come back (502 exam-topics-none) or the engines fail, the solve is given back and the reply carries refund. The day-by-day plan is built on the device with no request. Omit for the default \"direct\".",
            "enum": [
              "direct",
              "cothink",
              "socratic",
              "stress",
              "debate",
              "verify",
              "tournament",
              "premortem",
              "audit",
              "check",
              "tutor",
              "practice",
              "exam-topics",
              "tutor-reveal"
            ]
          },
          "goal": {
            "type": "string",
            "description": "MCQ goal intake. \"goal:check\" resolves to mode verify.",
            "examples": [
              "goal:check"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Required by mode tournament: at least two candidate options. Missing options is a 400 tournament-requires-options, never a default ranking."
          },
          "candidate": {
            "type": "string",
            "description": "Required by mode check: the answer another assistant gave to prompt. Never sent to the engines. A missing, blank or non-string candidate is a 400 check-requires-candidate, more than 65536 UTF-8 bytes is a 413 payload-too-large, and in mode check a blank or non-string prompt is a 400 empty-prompt; all are refused before any quota is spent."
          },
          "antagonist_mode": {
            "type": "boolean",
            "description": "Force one engine into an adversarial red-team role on chat lanes (adjudication modes set this themselves)."
          },
          "stream": {
            "type": "boolean",
            "description": "true (or ?stream=true) switches the response to text/event-stream."
          }
        }
      },
      "Synthesis": {
        "type": "object",
        "properties": {
          "ratified_conclusion": {
            "type": "string"
          },
          "adversarial_refutations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "alternative": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                },
                "confidenceBounds": {},
                "ruleId": {
                  "type": "string"
                }
              }
            }
          },
          "consensus_score": {
            "type": [
              "number",
              "null"
            ],
            "description": "Pairwise lexical convergence across engines that answered; null when fewer than two answered."
          },
          "convergence_method": {
            "type": "string",
            "enum": [
              "pairwise-lexical-jaccard",
              "insufficient-independent-responses"
            ]
          },
          "disagreements": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "divergence_score": {
            "type": [
              "number",
              "null"
            ]
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Engine"
            }
          },
          "enginesLive": {
            "type": "integer"
          },
          "total_latency_ms": {
            "type": "integer"
          },
          "trace_id": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "SolveResult": {
        "type": "object",
        "required": [
          "answer",
          "synthesis",
          "verification",
          "meta"
        ],
        "properties": {
          "answer": {
            "type": "string"
          },
          "synthesis": {
            "$ref": "#/components/schemas/Synthesis"
          },
          "verification": {
            "type": "object",
            "description": "Rule-based argument verification of the question text.",
            "properties": {
              "findings": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            },
            "additionalProperties": true
          },
          "meta": {
            "type": "object",
            "properties": {
              "proofSeal": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The server's seal over this answer, also the done event's proofSeal: {v, engines, issuedAt, sig}, HMAC-SHA256 over the stored question, the engines that answered ok, the stored answer and every other stored field. Post it back to /api/proof-link with the answer (body.proofSeal or meta.proofSeal) and the proof is stored sealed, so its page may say Checked by N AIs on ECCA. Any other share is stored unsealed and its page says Shared answer. Not checked by ECCA. null when sealing is not configured."
              },
              "latencyMs": {
                "type": "integer"
              },
              "providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Engine"
                }
              },
              "enginesConfigured": {
                "type": "integer"
              },
              "enginesLive": {
                "type": "integer",
                "description": "Measured per answer: engines that actually returned."
              },
              "convergenceScore": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "convergenceMethod": {
                "type": "string"
              },
              "disagreements": {
                "type": "array"
              },
              "divergenceScore": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "mode": {
                "type": "string"
              },
              "cascade": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Free deep solves only: gated / escalated outcome and engine counts."
              },
              "cache": {
                "type": "object",
                "description": "Present only on a labeled semantic-cache hit.",
                "properties": {
                  "hit": {
                    "const": true
                  },
                  "similarity": {
                    "type": "number"
                  },
                  "ageMs": {
                    "type": "integer"
                  }
                }
              },
              "downgraded": {
                "type": "boolean"
              },
              "reason": {
                "type": "string",
                "enum": [
                  "free-daily-deep-limit"
                ]
              },
              "deepLimit": {
                "type": "integer"
              },
              "upsell": {
                "type": "string"
              },
              "upgradeUrl": {
                "type": "string"
              },
              "streamTruncated": {
                "type": "boolean"
              }
            },
            "additionalProperties": true
          },
          "mode": {
            "type": "string",
            "description": "Echoed adjudication mode, when one was used."
          },
          "modeReport": {
            "type": [
              "object",
              "null"
            ],
            "description": "Deterministic report for verify / tournament / premortem / audit / check. For check it is the CheckResult plus proof and, when no proof was minted, proofRefusal."
          },
          "check": {
            "$ref": "#/components/schemas/CheckResult"
          },
          "proof": {
            "type": [
              "object",
              "null"
            ],
            "description": "Mode check only. The proof minted through POST /api/proof-link with the caller's own credentials, or null when that route did not mint one.",
            "properties": {
              "id": {
                "type": "string"
              },
              "path": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "proofRefusal": {
            "type": "object",
            "description": "Mode check only, present when proof is null: the body POST /api/proof-link answered plus its HTTP status. For a caller without a session it is error sign-in-required (status 401); a free account past its daily share cap gets proof-share-limit (status 429)."
          }
        }
      },
      "CheckResult": {
        "type": "object",
        "description": "Mode check: the dispatched panel's canonical final answer against the candidate's. Deterministic; agreement is never inferred from wording. agrees or dissents needs a checkable candidate answer and at least two engines with a checkable answer; otherwise the verdict is unmeasurable and summary says no agreement is claimed.",
        "required": [
          "verdict",
          "measurable",
          "candidateCanonical",
          "panelCanonical",
          "agreeingEngineIds",
          "dissentingEngineIds",
          "candidateShownToEngines"
        ],
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "agrees",
              "dissents",
              "unmeasurable"
            ]
          },
          "measurable": {
            "type": "boolean"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              null,
              "candidate-unextractable",
              "panel-unextractable",
              "panel-too-small",
              "panel-split"
            ],
            "description": "Why the verdict is unmeasurable; null when measurable."
          },
          "candidateCanonical": {
            "type": [
              "string",
              "null"
            ]
          },
          "panelCanonical": {
            "type": [
              "string",
              "null"
            ],
            "description": "The majority canonical final answer of the engines that gave one; null when none did or the panel split evenly."
          },
          "agreeingEngineIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Engines whose canonical final answer equals the candidate's."
          },
          "dissentingEngineIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Engines whose canonical final answer differs from the candidate's."
          },
          "unmeasuredEngineIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Engines that answered without a checkable final answer."
          },
          "candidateShownToEngines": {
            "const": false
          },
          "contradiction": {
            "type": [
              "object",
              "null"
            ],
            "description": "Set only when verdict is dissents. candidateSpan {start, end, text} is the candidate's final answer as written, located by the canonical-answer extractor's own match (end exclusive, UTF-16 offsets into the submitted candidate); correctClaim {canonical, engineIds, engineId, text} is the panel's answer as the selected engine wrote it, with every engine that reached it; citation {title, url} is the first https source the dispatch carried, else null; basis is canonical-extractor-match."
          },
          "summary": {
            "type": "string"
          }
        }
      },
      "SolveStream": {
        "type": "string",
        "description": "Server-sent events, in order: start {depth, streamLimitBytes, verificationState}; chunk {text} (repeated, metered against the tier byte ceiling); engine {id, name, role, ok, ms} and partial_adjudication {state, settledEngineIds, respondedEngineIds} as each engine settles; cascade (free deep solves); engines {providers, enginesLive}; disagreements {disagreements, divergenceScore} when any; meter {streamedBytes, chunkEvents, limitBytes, truncated, streamed, fallback}; mode_report {mode, report} for adjudication modes (for check, the CheckResult with proof); answer {answer}; synthesis {synthesis, verification, meta}; done {ok:true, adjudication, enginesLive, proofSeal, ...} where proofSeal is meta.proofSeal (post it back to /api/proof-link to store a sealed proof). A dispatch failure is a single error event carrying the SolveResult error body instead of answer/synthesis/done."
      },
      "TranscribeRequest": {
        "type": "object",
        "required": [
          "imageBase64",
          "mimeType"
        ],
        "properties": {
          "imageBase64": {
            "type": "string",
            "description": "Base64 image bytes; a data: URL prefix is stripped. At most 8000000 characters (about one phone photo). Held in memory only, never persisted or logged."
          },
          "mimeType": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp"
            ]
          }
        }
      },
      "TranscribeResponse": {
        "type": "object",
        "required": [
          "success",
          "transcription"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "transcription": {
            "type": "object",
            "properties": {
              "problem": {
                "type": "string",
                "maxLength": 6000
              },
              "subjectHint": {
                "type": "string",
                "maxLength": 40
              },
              "illegible": {
                "type": "array",
                "maxItems": 8,
                "items": {
                  "type": "string",
                  "maxLength": 200
                }
              },
              "multipleProblems": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "ProofRecord": {
        "type": "object",
        "properties": {
          "schema": {
            "const": "ecca.adjudicated-answer.v1"
          },
          "id": {
            "type": "string"
          },
          "prompt": {
            "type": "string",
            "maxLength": 6000
          },
          "answer": {
            "type": "string",
            "maxLength": 60000
          },
          "synthesis": {
            "type": "object",
            "properties": {
              "ratified_conclusion": {
                "type": "string"
              },
              "consensus_score": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "divergence_score": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "convergence_method": {
                "type": "string"
              },
              "providers": {
                "type": "array",
                "maxItems": 6,
                "items": {
                  "$ref": "#/components/schemas/Engine"
                }
              },
              "disagreements": {
                "type": "array",
                "maxItems": 10
              },
              "adversarial_refutations": {
                "type": "array",
                "maxItems": 20
              },
              "engine_outputs": {
                "type": "array",
                "maxItems": 6,
                "items": {
                  "$ref": "#/components/schemas/ProofEngineOutput"
                }
              }
            }
          },
          "verification": {
            "type": "object",
            "properties": {
              "findings": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "evidence": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "domain": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "permanence": {
            "const": "durable-no-expiry"
          },
          "fixturePurpose": {
            "type": "string",
            "description": "Non-empty only on the two reserved recorded fixtures (fixture-base-rate-reasoning, fixture-document-claim-boundary)."
          },
          "documentLabel": {
            "type": "string"
          },
          "documentExcerpt": {
            "type": "string"
          },
          "check": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present on proofs minted by mode check on POST /api/ecca-router: the CheckResult fields plus the candidate text (cut past 8000 characters, with candidateTruncated). Only the router's own in-process mint can store one; a check sent directly to this route is never stored. null otherwise."
          },
          "sources": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/ProofCitation"
            }
          }
        }
      },
      "ProofEngineOutput": {
        "type": "object",
        "description": "One engine's own answer as the dispatch reported it, copied: a registered engine id, its role, whether it returned, and its text (null when it did not answer; up to 8000 characters).",
        "properties": {
          "engine": {
            "type": "string",
            "enum": [
              "speed",
              "depth",
              "breadth",
              "logic",
              "context",
              "agentic"
            ]
          },
          "role": {
            "type": "string"
          },
          "ok": {
            "type": "boolean"
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 8000
          }
        }
      },
      "ProofCitation": {
        "type": "object",
        "description": "A citation from the dispatch's search receipt (meta.sources), copied. https only; a non-https or over-long url is dropped, never rewritten.",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 300
          },
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048
          }
        }
      },
      "ProofMachineRecord": {
        "type": "object",
        "description": "ecca.proof.v1: the machine-readable proof record embedded in every /p/{id} page as <script type=\"application/json\" id=\"ecca-proof-data\"> and served at /p/{id}.json. Every field is the stored value: agreement is the stored adjudication record (never recomputed; null when none was stored, and then verdict is null too). The owner is never included.",
        "required": [
          "schema",
          "id",
          "url",
          "question",
          "answer",
          "engines",
          "agreement",
          "verdict",
          "clashes",
          "citations",
          "createdAt"
        ],
        "properties": {
          "schema": {
            "const": "ecca.proof.v1"
          },
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "engines": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "$ref": "#/components/schemas/ProofEngineOutput"
            }
          },
          "agreement": {
            "type": [
              "object",
              "null"
            ],
            "description": "The stored adjudication record: method, inputs, agreeingEngineIds, dissentingEngineIds, agreementScore, canonicalAnswer, measurable.",
            "properties": {
              "method": {
                "type": "string",
                "enum": [
                  "unanimous",
                  "judge-merge",
                  "fallback-best-agreement",
                  "direct"
                ]
              },
              "inputs": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "agreeingEngineIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "dissentingEngineIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "agreementScore": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "canonicalAnswer": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "measurable": {
                "type": "boolean"
              }
            }
          },
          "verdict": {
            "type": [
              "string",
              "null"
            ],
            "description": "The one-line agreement verdict built from agreement, e.g. \"2 of 3 engines agree; Logic dissents\"."
          },
          "clashes": {
            "type": "array",
            "maxItems": 10,
            "description": "Claim against claim where two engines split, as stored.",
            "items": {
              "type": "object",
              "properties": {
                "engineA": {
                  "type": "string"
                },
                "engineB": {
                  "type": "string"
                },
                "claimA": {
                  "type": "string"
                },
                "claimB": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "frictionScore": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              }
            }
          },
          "citations": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/ProofCitation"
            }
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ProofCreateRequest": {
        "type": "object",
        "properties": {
          "op": {
            "enum": [
              "mint",
              "publish"
            ],
            "default": "mint",
            "description": "publish sets the owner's proof indexable (published: true) or back to noindex (published: false); prompt is required for mint, which is Pro."
          },
          "id": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{4,64}$",
            "description": "publish only: the proof id."
          },
          "published": {
            "type": "boolean",
            "description": "publish only."
          },
          "prompt": {
            "type": "string"
          },
          "answer": {
            "type": "string",
            "description": "Required unless synthesis is supplied."
          },
          "synthesis": {
            "type": "object"
          },
          "verification": {
            "type": "object"
          },
          "domain": {
            "type": "string",
            "pattern": "^[a-z0-9 _-]{1,80}$"
          },
          "documentLabel": {
            "type": "string"
          },
          "documentExcerpt": {
            "type": "string"
          }
        }
      },
      "ProofCreateResponse": {
        "type": "object",
        "required": [
          "ok",
          "id",
          "path",
          "url",
          "createdAt"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "id": {
            "type": "string",
            "description": "16 hex characters."
          },
          "key": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "examples": [
              "/p/0123456789abcdef"
            ]
          },
          "url": {
            "type": "string",
            "examples": [
              "https://eccaai.com/p/0123456789abcdef"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KeyRow": {
        "type": "object",
        "description": "Public view of one key. Never carries key material.",
        "properties": {
          "keyId": {
            "type": "string",
            "description": "Public handle: sha256(key) truncated. The only thing revoke needs."
          },
          "prefix": {
            "const": "ecca_sk_"
          },
          "label": {
            "type": "string",
            "maxLength": 40
          },
          "createdAt": {
            "type": "string"
          },
          "tier": {
            "type": "string",
            "enum": [
              "PRO",
              "ENTERPRISE",
              ""
            ]
          },
          "revoked": {
            "type": "boolean"
          }
        }
      },
      "KeyState": {
        "type": "object",
        "description": "GET state. Either the self-serve list alone ({ok, keys}), a pending purchase ({pending, reason, message, keys}), or the plan-provisioned key record ({key, tier, keyId, issuedAt, revoked, keys}) returned only to its owner.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KeyRow"
            }
          },
          "pending": {
            "type": "boolean"
          },
          "reason": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "Plan-provisioned key only."
          },
          "tier": {
            "type": [
              "string",
              "null"
            ]
          },
          "keyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "issuedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "revoked": {
            "type": "boolean"
          }
        }
      },
      "KeyMutation": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "op"
            ],
            "properties": {
              "op": {
                "const": "create"
              },
              "label": {
                "type": "string",
                "maxLength": 40
              }
            }
          },
          {
            "type": "object",
            "required": [
              "op",
              "keyId"
            ],
            "properties": {
              "op": {
                "const": "revoke"
              },
              "keyId": {
                "type": "string",
                "pattern": "^[0-9a-f]{8,64}$"
              }
            }
          }
        ]
      },
      "KeyCreated": {
        "type": "object",
        "required": [
          "ok",
          "key",
          "shownOnce",
          "keyId"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "key": {
            "type": "string",
            "description": "The secret. Returned exactly once; it is never stored server-side and cannot be shown again."
          },
          "shownOnce": {
            "const": true
          },
          "message": {
            "type": "string"
          },
          "keyId": {
            "type": "string"
          },
          "prefix": {
            "const": "ecca_sk_"
          },
          "label": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "tier": {
            "type": "string"
          },
          "revoked": {
            "const": false
          }
        }
      },
      "CheckoutRequest": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "eccaai-pro",
              "eccaai-pro-annual",
              "eccaai-enterprise",
              "eccaai-enterprise-annual"
            ],
            "default": "eccaai-pro",
            "description": "Omitted means eccaai-pro. Any other value is a 400 invalid-plan."
          },
          "state": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,64}$",
            "description": "Opaque client token echoed on the return redirect."
          },
          "checkoutAttemptId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{8,128}$",
            "description": "One id per plan/page load; retries reuse it so the Stripe idempotency key is stable."
          }
        }
      },
      "McpToolsList": {
        "type": "object",
        "properties": {
          "tools": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "enum": [
                    "ecca_synthesize",
                    "ecca_pre_mortem",
                    "ecca_decision_tournament",
                    "ecca_bts_divergence",
                    "ecca_refine",
                    "ecca_research",
                    "ecca_verify",
                    "ecca_psychology",
                    "ecca_learning",
                    "ecca_nutrition",
                    "ecca_money",
                    "ecca_claim_protocol_validate"
                  ]
                },
                "description": {
                  "type": "string"
                },
                "inputSchema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "AxiomRequest": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "maxLength": 64000
          },
          "prompt": {
            "type": "string",
            "description": "Alias for query."
          },
          "model": {
            "type": "string",
            "enum": [
              "axiom-vector",
              "axiom-core",
              "axiom-reasoner",
              "axiom-omni",
              "axiom-sovereign"
            ],
            "default": "axiom-reasoner",
            "description": "Label echoed in the response; every value dispatches through the same registered engines and carries no latency promise."
          },
          "depth": {
            "type": "string",
            "enum": [
              "intuitive",
              "executive",
              "pioneer",
              "fast",
              "deep"
            ],
            "default": "executive",
            "description": "intuitive and fast route one engine; executive, pioneer and deep route a multi-engine dispatch under the same policy as /api/ecca-router (free callers: two-engine cascade, one deep solve per day, then fast). Any other value is a 400 invalid-depth."
          },
          "antagonist": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "AxiomResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "const": true
          },
          "id": {
            "type": "string"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "object",
                  "properties": {
                    "role": {
                      "const": "assistant"
                    },
                    "content": {
                      "type": "string"
                    }
                  }
                },
                "finish_reason": {
                  "type": "string",
                  "enum": [
                    "converged",
                    "diverged_or_insufficient_engines",
                    "failed"
                  ]
                }
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "consensusScore": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "convergenceMethod": {
                "type": "string"
              },
              "enginesLive": {
                "type": "integer"
              },
              "latencyMs": {
                "type": "integer"
              },
              "engineMatrix": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Engine"
                }
              }
            }
          }
        }
      },
      "DocumentIngestRequest": {
        "type": "object",
        "properties": {
          "fileName": {
            "type": "string",
            "default": "document.pdf"
          },
          "textContent": {
            "type": "string"
          },
          "fileBase64": {
            "type": "string",
            "description": "Base64 file bytes, limited to 4,000,000 characters (about 3 MB decoded); .pdf names are text-extracted, anything else is read as UTF-8. The extracted text must be at most 45,000 characters."
          },
          "analysisMode": {
            "type": "string",
            "default": "deep_audit"
          }
        }
      },
      "DocumentIngestResponse": {
        "type": "object",
        "properties": {
          "success": {
            "const": true
          },
          "cleanText": {
            "type": "string"
          },
          "documentMetadata": {
            "type": "object",
            "properties": {
              "fileName": {
                "type": "string"
              },
              "wordCount": {
                "type": "integer"
              },
              "analyzedAt": {
                "type": "string"
              },
              "analysisMode": {
                "type": "string"
              }
            }
          },
          "firstPrinciplesSynthesis": {
            "type": "string"
          },
          "riskHeatmap": {
            "type": "array"
          },
          "contradictionsDetected": {
            "type": "array"
          },
          "unstatedAssumptions": {
            "type": "array"
          },
          "consensusGrounding": {
            "type": "object",
            "properties": {
              "score": {
                "const": null,
                "description": "Nothing measures a grounding probability, so none is served."
              },
              "status": {
                "type": "string",
                "enum": [
                  "engine-audited",
                  "engine-unavailable"
                ]
              },
              "riskItemCount": {
                "type": "integer"
              }
            }
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "service": {
            "const": "eccaai"
          },
          "checks": {
            "type": "object",
            "description": "Booleans and counts only: runtime, auth_configured, email_configured, operator_email, store_configured, store_backend, events_writing, events_sink, events_reason, enginesConfigured, node_version."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CapabilityStatusResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "const": true
          },
          "capabilities": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "live": {
                  "type": [
                    "boolean",
                    "null"
                  ],
                  "description": "null means the predicate threw; the page leaves the shipped badge untouched."
                }
              }
            }
          }
        }
      },
      "EntitlementResponse": {
        "type": "object",
        "required": [
          "tier",
          "signedIn",
          "features"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "paid",
              "enterprise"
            ]
          },
          "signedIn": {
            "type": "boolean"
          },
          "features": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "Feature name to whether the caller's tier unlocks it (export, delete-account, ecca-unlimited, ecca-pro-matrix, ecca-cpow-export, ecca-proof-links, ecca-calibration-history, ecca-studio-ide, ecca-enterprise-api)."
          },
          "purchaseReceipt": {
            "type": [
              "object",
              "null"
            ]
          },
          "degraded": {
            "type": "boolean",
            "description": "true when the route fell back to the free-tier view after a backend hiccup."
          }
        }
      },
      "TrapRadarResponse": {
        "type": "object",
        "required": [
          "ok",
          "schema",
          "topic",
          "label",
          "floor",
          "engineCalls",
          "sample",
          "basis",
          "unit",
          "splitRate",
          "n",
          "cause",
          "note"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "schema": {
            "type": "string",
            "const": "ecca.trap-radar.v1"
          },
          "topic": {
            "type": "string",
            "description": "Topic id from the shared classifier (lib/ecca_topic.js)."
          },
          "label": {
            "type": "string"
          },
          "floor": {
            "type": "integer",
            "description": "Consented records a topic needs before real counts are returned (25)."
          },
          "engineCalls": {
            "type": "integer",
            "const": 0
          },
          "sample": {
            "type": "boolean",
            "description": "Set whenever the topic is under the floor; the counts are then the labelled sample fixture's, or absent."
          },
          "basis": {
            "type": "string",
            "enum": [
              "consented-records",
              "sample-fixture",
              "none"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "claims",
              "AIs",
              null
            ],
            "description": "claims for consented records (share of claims that split); AIs for the sample fixture (share of AIs that dissented)."
          },
          "splitRate": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "n": {
            "type": "integer",
            "minimum": 0,
            "description": "Records behind splitRate: consented records, or sample rows when the sample flag is set."
          },
          "cause": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most common recorded split cause."
          },
          "note": {
            "type": "string"
          }
        }
      },
      "MissCounterResponse": {
        "type": "object",
        "required": [
          "schema",
          "basis",
          "label",
          "agreedPanels",
          "agreedAndKeyDisagrees",
          "agreedAndKeyAgrees",
          "notMachineGraded",
          "n",
          "runDate",
          "setHash",
          "source",
          "engineCalls",
          "note"
        ],
        "properties": {
          "schema": {
            "type": "string",
            "const": "ecca.miss-counter.v1"
          },
          "basis": {
            "type": "string",
            "enum": [
              "live-record",
              "sample",
              "none"
            ],
            "description": "live-record: the stored calibration record has full-panel mode evidence for every counted question. sample: a summary without full live-panel evidence, labelled SAMPLE. none: no record stored yet; every count is null."
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "SAMPLE",
              null
            ],
            "description": "SAMPLE whenever basis is sample; null otherwise."
          },
          "agreedPanels": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Questions where the AIs agreed (two or more comparable answers, none dissenting)."
          },
          "agreedAndKeyDisagrees": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Agreed panels whose machine-graded answer did not match the reference key: the miss count. Answers the rules cannot grade are never counted here."
          },
          "agreedAndKeyAgrees": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Agreed panels whose machine-graded answer matched the reference key."
          },
          "notMachineGraded": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Agreed panels whose answer needs a human grader; reported apart, never counted as a miss."
          },
          "n": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Questions counted in the run (failed and disputed questions excluded)."
          },
          "runDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "YYYY-MM-DD of the run, or null."
          },
          "setHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hash of the frozen calibration set the run used, or null."
          },
          "source": {
            "type": [
              "object",
              "null"
            ],
            "description": "The stored record behind the counts: its id and path.",
            "properties": {
              "id": {
                "type": "string"
              },
              "path": {
                "type": "string"
              }
            }
          },
          "engineCalls": {
            "type": "integer",
            "const": 0
          },
          "note": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "MethodNotAllowed": {
        "description": "Method not in the route's allowlist.",
        "x-ecca-error-codes": [
          "method-not-allowed"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "method-not-allowed"
            }
          }
        }
      },
      "AbuseBrake": {
        "description": "Per-route abuse brake: 60 requests per minute per client IP, per serving instance.",
        "x-ecca-error-codes": [
          "rate-limited"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "rate-limited",
              "retry_after_s": 60
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Body exceeds the route's cap.",
        "x-ecca-error-codes": [
          "payload-too-large"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "payload-too-large"
            }
          }
        }
      },
      "QuotaKeyRefused": {
        "description": "A presented ecca_sk_ key failed verification, or is on the revocation denylist. Never a downgrade to anonymous.",
        "x-ecca-error-codes": [
          "invalid-api-key",
          "key-revoked"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "invalid-api-key",
              "message": "The supplied ECCA API key failed verification.",
              "upgradeUrl": "/pricing"
            }
          }
        }
      },
      "QuotaCreditsRequired": {
        "description": "A valid free-tier key has used its 5 daily solves.",
        "x-ecca-error-codes": [
          "credits-required"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "credits-required",
              "message": "This API key has used its 5 free daily solves. Upgrade to a paid key tier to continue programmatic use.",
              "upgradeUrl": "/pricing"
            }
          }
        }
      },
      "QuotaExhausted": {
        "description": "Free daily solve pool exhausted (free-daily-limit, UTC midnight), Pro account window full (pro-window-limit, resetAt and Retry-After), or per-key minute bucket full (key-rate-limited, next minute). Pro refusals include solves and meter; they do not offer an upsell.",
        "x-ecca-error-codes": [
          "free-daily-limit",
          "pro-window-limit",
          "key-rate-limited"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "free-daily-limit",
              "message": "You have reached your 5 free daily meta-intelligence solves. Free solves reset at 00:00 UTC; pricing reports whether Pro checkout is available.",
              "upgradeUrl": "/pricing"
            }
          }
        }
      },
      "SpendCeiling": {
        "description": "The operator's daily engine-dispatch ceiling is reached (only when one is configured). Explicit refusal, never a silent drop.",
        "x-ecca-error-codes": [
          "spend-ceiling-reached"
        ],
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "example": {
              "error": "spend-ceiling-reached",
              "message": "The operator's daily engine-dispatch ceiling has been reached. Service resumes when the day rolls over or the ceiling is raised."
            }
          }
        }
      },
      "OperatorNotFound": {
        "description": "No operator session, wrong token, or wrong method: a plain-text 404 indistinguishable from an unknown path.",
        "x-ecca-error-codes": [
          "Not found"
        ],
        "content": {
          "text/plain": {
            "schema": {
              "type": "string",
              "const": "Not found"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/ecca-router": {
      "get": {
        "tags": [
          "solve"
        ],
        "summary": "Engine registry, or the caller's own free-quota usage",
        "description": "Without parameters: the registered engine roster (identity only), cacheable. With usage=1: the requester's consumption read from the same counter the POST path enforces with; no-store, spends nothing.",
        "operationId": "routerDescribe",
        "parameters": [
          {
            "name": "usage",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Return the free daily count or the Pro account usage-window count and reset receipt. An unreadable counter returns 503, never a fabricated zero."
          }
        ],
        "responses": {
          "200": {
            "description": "Registry or usage.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RegistryResponse"
                    },
                    {
                      "$ref": "#/components/schemas/UsageResponse"
                    }
                  ]
                },
                "examples": {
                  "usageFree": {
                    "value": {
                      "ok": true,
                      "tier": "free",
                      "unlimited": false,
                      "solves": {
                        "used": 2,
                        "remaining": 3,
                        "limit": 5
                      }
                    }
                  },
                  "usagePaid": {
                    "value": {
                      "ok": true,
                      "tier": "pro",
                      "unlimited": false,
                      "solves": {
                        "used": 7,
                        "remaining": 13,
                        "limit": 20,
                        "windowHours": 5,
                        "resetAt": "2026-10-07T04:00:00.000Z",
                        "retryAfter": 10800,
                        "meter": "7 of 20 deep checks used in this 5-hour window, resets at 04:00 UTC"
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          }
        }
      },
      "post": {
        "tags": [
          "solve"
        ],
        "summary": "Solve: dispatch a question and receive the adjudicated answer",
        "description": "Anonymous callers draw the free daily pool for their client IP; a valid free-tier ecca_sk_ key draws its own account pool. Pro deep checks reserve the account window shared across sessions and account-linked keys. A full window returns 429 with meter, resetAt and Retry-After, not a downgrade or upsell. Fast single-engine solves do not consume this allowance. Enterprise and owner remain outside the Pro counter. Body up to 16777216 bytes. Set stream=true for server-sent events.",
        "operationId": "routerSolve",
        "security": [
          {},
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "stream",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SolveRequest"
              },
              "example": {
                "prompt": "A test is 99% sensitive and 95% specific for a condition with 1% prevalence. What is the probability a positive result is a true positive?",
                "depth": "deep"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Adjudicated answer (buffered) or an event stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolveResult"
                }
              },
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/SolveStream"
                }
              }
            }
          },
          "400": {
            "description": "Empty prompt, mode tournament without at least two options, or mode check without a candidate.",
            "x-ecca-error-codes": [
              "empty-prompt",
              "tournament-requires-options",
              "check-requires-candidate"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "empty-prompt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/QuotaKeyRefused"
          },
          "402": {
            "$ref": "#/components/responses/QuotaCreditsRequired"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "description": "Abuse brake (rate-limited), the per-key minute bucket for a keyed caller (key-rate-limited), or the free daily pool is spent (free-daily-limit). A spent free deep solve is not a refusal: the request runs on the fast path with meta.downgraded=true.",
            "x-ecca-error-codes": [
              "rate-limited",
              "key-rate-limited",
              "free-daily-limit"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "ok": false,
                  "error": "free-daily-limit",
                  "limit": 5,
                  "message": "You have reached your 5 free daily meta-intelligence solves. Free solves reset at 00:00 UTC; pricing reports whether Pro checkout is available before any purchase starts.",
                  "upgradeUrl": "/pricing"
                }
              }
            }
          },
          "502": {
            "description": "Engines were configured and dispatched but none returned an answer.",
            "x-ecca-error-codes": [
              "ai-unavailable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "ai-unavailable",
                  "status": 502
                }
              }
            }
          },
          "503": {
            "description": "Fail-closed: no engine is configured on this deployment (ai-not-configured), every configured engine's circuit is open with a measured retryAfterMs (engines-cooling), or the operator dispatch ceiling is reached (spend-ceiling-reached).",
            "x-ecca-error-codes": [
              "ai-not-configured",
              "engines-cooling",
              "spend-ceiling-reached"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "engines-cooling",
                  "status": 503,
                  "retryAfterMs": 60000
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "tags": [
          "mcp"
        ],
        "summary": "MCP server info and tool list",
        "operationId": "mcpDescribe",
        "responses": {
          "200": {
            "description": "Server info and every currently supported tool.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "const": "2.0"
                    },
                    "protocolVersion": {
                      "const": "2025-06-18"
                    },
                    "serverInfo": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "const": "ecca-ai-sovereign-mesh"
                        },
                        "version": {
                          "const": "6.1.1"
                        }
                      }
                    },
                    "tools": {
                      "$ref": "#/components/schemas/McpToolsList/properties/tools"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported MCP-Protocol-Version header.",
            "x-ecca-error-codes": [
              "Invalid MCP request"
            ]
          },
          "403": {
            "description": "The supplied Origin is not allowed.",
            "x-ecca-error-codes": [
              "Invalid MCP request"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              }
            }
          },
          "405": {
            "description": "The optional SSE listener is not supported (empty response); other unsupported HTTP methods return method-not-allowed.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ]
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          }
        },
        "description": "JSON discovery is available for ordinary GET requests. Accept: text/event-stream returns 405 because the optional server event stream is not supported.",
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "required": false,
            "description": "Negotiated protocol version on requests after initialization. Unsupported values return 400.",
            "schema": {
              "type": "string",
              "enum": [
                "2025-06-18",
                "2025-03-26",
                "2024-11-05"
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "JSON-RPC 2.0: initialize, ping, tools/list, tools/call",
        "description": "Stateless Streamable HTTP with JSON replies. Valid requests echo the caller's id; accepted notifications return HTTP 202 with no body. initialize, ping and tools/list are open; EVERY tools/call requires an API key (an unkeyed call is -32001 with data.reason key-required and a message naming the one-click free key on /developers), so the anonymous IP pool is not reachable here. Solve tools draw the key's allowance: a FREE key its daily pool per account, a Pro key its account window of deep checks; refine and research are metered under the same key; the remaining tools are local computation. A presented-but-invalid or revoked key is refused on every POST method. No session ID is required. Owner engine work returns 429 owner-monthly-cap at the monthly cap or unknown spend, before another engine stage starts.",
        "operationId": "mcpCall",
        "security": [
          {},
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": {
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "number"
                    ],
                    "description": "Required for requests. Omit for notifications."
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "initialize",
                      "ping",
                      "notifications/initialized",
                      "tools/list",
                      "tools/call"
                    ]
                  },
                  "params": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "arguments": {
                        "type": "object"
                      },
                      "protocolVersion": {
                        "type": "string"
                      },
                      "clientInfo": {
                        "type": "object"
                      },
                      "capabilities": {
                        "type": "object"
                      }
                    }
                  }
                }
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "ecca_synthesize",
                  "arguments": {
                    "query": "Does correlation between two time series imply a causal link?",
                    "depth": "executive"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result. tools/call returns result.content[0].text and standard result.structuredContent; result.structuredData is retained for existing integrations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "const": "2.0"
                    },
                    "id": {},
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Notification accepted. Empty response body; notifications never dispatch tools."
          },
          "400": {
            "description": "Malformed envelope or JSON, unsupported protocol version, or unknown method (-32600 Invalid MCP request), or a tool rejected its input (-32601 with the tool message).",
            "x-ecca-error-codes": [
              "Invalid MCP request"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "error": {
                    "code": -32600,
                    "message": "Invalid MCP request"
                  }
                }
              }
            }
          },
          "401": {
            "description": "-32001: a tools/call carried no key (data.reason key-required; the message names the free key on /developers), the presented key failed verification (data.reason names why), or the key is revoked.",
            "x-ecca-error-codes": [
              "api-key-required",
              "invalid-api-key",
              "key-revoked"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "error": {
                    "code": -32001,
                    "message": "invalid-api-key",
                    "data": {
                      "reason": "bad-signature"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "-32000: a valid free-tier key has used its daily solves (data.reason credits-required).",
            "x-ecca-error-codes": [
              "credits-required"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              }
            }
          },
          "403": {
            "description": "The supplied Origin is not allowed.",
            "x-ecca-error-codes": [
              "Invalid MCP request"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "description": "Abuse brake (rate-limited, plain body) or the per-key minute bucket (-32002 key-rate-limited). Tool calls are always keyed here, so the anonymous free pool's free-daily-limit never comes from this route; a spent FREE key is 402 credits-required and a full Pro window is -32000 pro-window-limit with Retry-After.",
            "x-ecca-error-codes": [
              "rate-limited",
              "key-rate-limited",
              "owner-monthly-cap"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorBody"
                    }
                  ]
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "error": {
                    "code": -32002,
                    "message": "key-rate-limited"
                  }
                }
              }
            }
          },
          "503": {
            "description": "-32000 with data.reason spend-ceiling-reached.",
            "x-ecca-error-codes": [
              "spend-ceiling-reached"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "required": false,
            "description": "Negotiated protocol version on requests after initialization. Unsupported values return 400.",
            "schema": {
              "type": "string",
              "enum": [
                "2025-06-18",
                "2025-03-26",
                "2024-11-05"
              ]
            }
          }
        ]
      }
    },
    "/api/v1/axiom/synthesize": {
      "get": {
        "tags": [
          "solve"
        ],
        "summary": "Axiom REST model list",
        "operationId": "axiomDescribe",
        "responses": {
          "200": {
            "description": "Service label, version, the live engine registry, the accepted depths, and the model labels.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "service": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    },
                    "engines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "depths": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "routesTo": {
                            "enum": [
                              "fast",
                              "deep"
                            ]
                          }
                        }
                      }
                    },
                    "models": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "label": {
                            "const": true
                          }
                        }
                      }
                    },
                    "modelNote": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          }
        }
      },
      "post": {
        "tags": [
          "solve"
        ],
        "summary": "Chat-completions-shaped synthesis over the same engine registry",
        "description": "Wraps the ecca_synthesize tool. Draws the same solve pool as /api/ecca-router and /api/mcp.",
        "operationId": "axiomSynthesize",
        "security": [
          {},
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          },
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AxiomRequest"
              },
              "example": {
                "query": "Is the harmonic series convergent?",
                "depth": "pioneer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Synthesis in choices[0].message.content; finish_reason is converged only when at least two engines answered and consensus is at least 0.70.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AxiomResponse"
                }
              }
            }
          },
          "400": {
            "description": "Neither query nor prompt supplied.",
            "x-ecca-error-codes": [
              "Missing required field 'query' or 'prompt'"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "ok": false,
                  "error": "Missing required field 'query' or 'prompt'"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/QuotaKeyRefused"
          },
          "402": {
            "$ref": "#/components/responses/QuotaCreditsRequired"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExhausted"
          },
          "500": {
            "description": "The tool threw; error carries its message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SpendCeiling"
          }
        }
      }
    },
    "/api/ecca-transcribe": {
      "post": {
        "tags": [
          "solve"
        ],
        "summary": "Transcribe one homework photo to text (never solves)",
        "description": "Spends one solve from the shared pool because it makes a live vision call. Fail-closed: 503 with no network attempt when the deployment has no vision tier.",
        "operationId": "transcribe",
        "security": [
          {},
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          },
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TranscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Faithful transcription, clamped field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscribeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Wrong image type or no image bytes.",
            "x-ecca-error-codes": [
              "unsupported-image-type",
              "missing-image"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "unsupported-image-type",
                  "message": "Use a JPEG, PNG, or WebP photo."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/QuotaKeyRefused"
          },
          "402": {
            "$ref": "#/components/responses/QuotaCreditsRequired"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "description": "More than 8000000 base64 characters.",
            "x-ecca-error-codes": [
              "image-too-large"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/QuotaExhausted"
          },
          "502": {
            "description": "The vision call returned nothing readable. Honest failure, never a guessed problem.",
            "x-ecca-error-codes": [
              "transcription-unavailable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "No vision tier on this deployment (vision-not-configured), or the operator dispatch ceiling (spend-ceiling-reached).",
            "x-ecca-error-codes": [
              "vision-not-configured",
              "spend-ceiling-reached"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "vision-not-configured",
                  "message": "Photo transcription is not available on this deployment."
                }
              }
            }
          }
        }
      }
    },
    "/api/document-ingest": {
      "post": {
        "tags": [
          "solve"
        ],
        "summary": "Extract text from a document and run a dialectical audit",
        "description": "Spends one solve from the shared pool. When the audit engine cannot be reached the response still succeeds and consensusGrounding.status says engine-unavailable.",
        "operationId": "documentIngest",
        "security": [
          {},
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          },
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentIngestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audit result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentIngestResponse"
                }
              }
            }
          },
          "400": {
            "description": "The base64 data or PDF header is invalid.",
            "x-ecca-error-codes": [
              "invalid-document"
            ]
          },
          "401": {
            "$ref": "#/components/responses/QuotaKeyRefused"
          },
          "402": {
            "$ref": "#/components/responses/QuotaCreditsRequired"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "description": "The request body, encoded file, or extracted text exceeds the route limit.",
            "x-ecca-error-codes": [
              "payload-too-large",
              "document-too-large"
            ]
          },
          "422": {
            "description": "The PDF cannot be parsed or contains no readable text.",
            "x-ecca-error-codes": [
              "unreadable-document"
            ]
          },
          "429": {
            "$ref": "#/components/responses/QuotaExhausted"
          },
          "500": {
            "description": "Extraction or audit threw.",
            "x-ecca-error-codes": [
              "Document ingestion failed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SpendCeiling"
          }
        }
      }
    },
    "/p/{id}/claims.json": {
      "get": {
        "tags": [
          "proof"
        ],
        "summary": "Read claim rows",
        "description": "Read atomic claim rows from sealed semantic comparisons. Missing comparisons remain unmeasured; source, logic and assumption checks stay untested when no sealed per-claim evidence exists. This read does not run engines or consume solve quota. Responses are no-store and noindex; deleted proofs return 404.",
        "operationId": "proofClaims",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read-only projection of the public proof record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AtomicClaimRecord"
                }
              }
            }
          },
          "400": {
            "description": "Invalid proof identifier.",
            "x-ecca-error-codes": [
              "invalid-id"
            ]
          },
          "404": {
            "description": "Proof missing or deleted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ]
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "Proof persistence or deletion state unavailable.",
            "x-ecca-error-codes": [
              "proof-links-unavailable",
              "proof-read-failed"
            ]
          }
        }
      },
      "head": {
        "tags": [
          "proof"
        ],
        "summary": "Read status and headers without a response body",
        "operationId": "proofClaimsHead",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Same status and headers as GET, no response body."
          },
          "404": {
            "description": "Proof missing or deleted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ]
          },
          "503": {
            "description": "Proof persistence or deletion state unavailable.",
            "x-ecca-error-codes": [
              "proof-links-unavailable",
              "proof-read-failed"
            ]
          }
        }
      }
    },
    "/p/{id}/citation.json": {
      "get": {
        "tags": [
          "proof"
        ],
        "summary": "Cite this check",
        "description": "Read both APA and MLA plain-text citations. Counts come from the stored seal and valid claim points; the date is the seal date in UTC. Missing split counts remain UNKNOWN. Unsealed or undated records return available=false with null citation strings. This read does not run engines or consume solve quota. Responses are no-store and noindex; deleted proofs return 404.",
        "operationId": "proofCitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read-only projection of the public proof record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofCitationRecord"
                }
              }
            }
          },
          "400": {
            "description": "Invalid proof identifier.",
            "x-ecca-error-codes": [
              "invalid-id"
            ]
          },
          "404": {
            "description": "Proof missing or deleted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ]
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "Proof persistence or deletion state unavailable.",
            "x-ecca-error-codes": [
              "proof-links-unavailable",
              "proof-read-failed"
            ]
          }
        }
      },
      "head": {
        "tags": [
          "proof"
        ],
        "summary": "Read status and headers without a response body",
        "operationId": "proofCitationHead",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Same status and headers as GET, no response body."
          },
          "404": {
            "description": "Proof missing or deleted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ]
          },
          "503": {
            "description": "Proof persistence or deletion state unavailable.",
            "x-ecca-error-codes": [
              "proof-links-unavailable",
              "proof-read-failed"
            ]
          }
        }
      }
    },
    "/api/proof-link": {
      "get": {
        "tags": [
          "proof"
        ],
        "summary": "Read a persisted adjudicated answer",
        "description": "JSON by default; HTML when the Accept header includes text/html and format is not json. The two recorded fixtures are served without a store.",
        "operationId": "proofRead",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The record ({ok, proof, fixture?}) or its HTML page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "proof": {
                      "$ref": "#/components/schemas/ProofRecord"
                    },
                    "fixture": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Malformed id.",
            "x-ecca-error-codes": [
              "invalid-id"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "No record at that id. Nothing is substituted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "Fail-closed: proof persistence is not configured on this deployment.",
            "x-ecca-error-codes": [
              "proof-links-unavailable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "ok": false,
                  "error": "proof-links-unavailable",
                  "message": "Proof link persistence is not configured on this deployment."
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "proof"
        ],
        "summary": "Mint a durable /p/<id> permalink (free with a session), or publish one you own",
        "description": "Minting is free for any signed-in session; a free account mints under a daily cap per account and per client network (x-ecca-quotas.freeDailyProofShares, freeDailyProofSharesPerIp), and Pro mints outside it. Every minted record stores its owner, is purged with the owner's account delete, and is served noindex until the owner publishes it with {op: \"publish\", id, published: true}; publishing needs the owner's session and an active Pro plan, unpublishing only the owner's session. Body up to 1048576 bytes. Records have no expiry.",
        "operationId": "proofCreate",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProofCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permalink minted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofCreateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing prompt, or neither answer nor synthesis; or a publish with a malformed id.",
            "x-ecca-error-codes": [
              "empty-proof-payload",
              "invalid-id"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "Mint or publish without a signed-in session.",
            "x-ecca-error-codes": [
              "sign-in-required"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "402": {
            "description": "Caller is not entitled to ecca-proof-links: publishing into search (minting and unpublishing are always allowed).",
            "x-ecca-error-codes": [
              "upgrade-required"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "ok": false,
                  "error": "upgrade-required",
                  "feature": "ecca-proof-links",
                  "message": "Publishing a proof link requires ECCA AI Pro. You can still unpublish it.",
                  "upgradeUrl": "/pricing"
                }
              }
            }
          },
          "403": {
            "description": "Publish refused: the caller does not own this proof.",
            "x-ecca-error-codes": [
              "not-owner"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "Publish target does not exist.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "description": "The per-route abuse brake tripped (rate-limited), or a free account or its client network has used today's free proof shares (proof-share-limit, resets 00:00 UTC, Retry-After).",
            "x-ecca-error-codes": [
              "rate-limited",
              "proof-share-limit"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "500": {
            "description": "The store refused the write.",
            "x-ecca-error-codes": [
              "store-write-failed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Fail-closed: persistence is not configured, the tier or the free share counter could not be read, or the proof or owner index could not be read or recorded (the mint is rolled back).",
            "x-ecca-error-codes": [
              "proof-links-unavailable",
              "entitlement-unavailable",
              "proof-share-limit-unavailable",
              "proof-read-failed",
              "proof-index-unreadable",
              "proof-index-full",
              "proof-index-write-failed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/p/{id}": {
      "get": {
        "tags": [
          "proof"
        ],
        "summary": "Shareable permalink page for a persisted answer",
        "description": "Rewrite of /api/proof-link?id=<id>. Browsers get the HTML record page (noindex, follow, with Open Graph tags); JSON clients get the record.",
        "operationId": "proofPermalink",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Record page or JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "proof": {
                      "$ref": "#/components/schemas/ProofRecord"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed id.",
            "x-ecca-error-codes": [
              "invalid-id"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "Not found page or JSON.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ],
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "Persistence unavailable page or JSON.",
            "x-ecca-error-codes": [
              "proof-links-unavailable"
            ],
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/p/{id}.json": {
      "get": {
        "tags": [
          "proof"
        ],
        "summary": "Machine-readable proof record for a persisted answer",
        "description": "Rewrite of /api/proof-link?id=<id>&format=record. Always JSON (Access-Control-Allow-Origin: *), so an assistant can fetch and cite the record: the question, each engine's answer, the stored agreement record, the clashes, and the citations. Byte-for-byte the record embedded in the /p/{id} page. Purged and missing proofs are 404.",
        "operationId": "proofMachineRecord",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{4,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ecca.proof.v1 record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofMachineRecord"
                }
              }
            }
          },
          "400": {
            "description": "Malformed id.",
            "x-ecca-error-codes": [
              "invalid-id"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "No record at that id, or it was purged. Nothing is substituted.",
            "x-ecca-error-codes": [
              "proof-not-found"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "Persistence unavailable.",
            "x-ecca-error-codes": [
              "proof-links-unavailable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/ecca-key": {
      "get": {
        "tags": [
          "keys"
        ],
        "summary": "The signed-in developer's key state",
        "description": "Self-serve rows carry only keyId, prefix, label, createdAt, tier and revoked; a secret is never listed. The plan-provisioned key record is returned to its owner only.",
        "operationId": "keysList",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Key state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyState"
                }
              }
            }
          },
          "401": {
            "description": "No verified session.",
            "x-ecca-error-codes": [
              "unauthorized"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "No plan-provisioned key and no self-serve keys yet.",
            "x-ecca-error-codes": [
              "no-key"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "no-key",
                  "message": "No ECCA API key exists for this account. Create one below, or one mints automatically on an ECCAAI plan purchase.",
                  "keys": []
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "500": {
            "description": "Unexpected failure.",
            "x-ecca-error-codes": [
              "internal-server-error"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Fail-closed: no durable store on this deployment.",
            "x-ecca-error-codes": [
              "store-unconfigured"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Create or revoke a key (signed-in account)",
        "description": "op create mints a key whose secret is returned once and never stored; the activeKeyLimit field reports the account cap. op revoke writes the store-backed denylist every keyed surface consults; it is owner-scoped. Body up to 2048 bytes. Requires an allowlisted Origin in addition to the session.",
        "operationId": "keysMutate",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyMutation"
              },
              "examples": {
                "create": {
                  "value": {
                    "op": "create",
                    "label": "staging worker"
                  }
                },
                "revoke": {
                  "value": {
                    "op": "revoke",
                    "keyId": "0123456789ab"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "keyId": {
                      "type": "string"
                    },
                    "revoked": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Created. Copy the secret now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            }
          },
          "400": {
            "description": "op is not create/revoke, or keyId is malformed.",
            "x-ecca-error-codes": [
              "bad-op",
              "invalid-key-id"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "No verified session.",
            "x-ecca-error-codes": [
              "unauthorized"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "403": {
            "description": "Origin not allowlisted (forbidden-origin). Any signed-in account may create a key; the free tier is not refused here.",
            "x-ecca-error-codes": [
              "forbidden-origin"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "forbidden-origin"
                }
              }
            }
          },
          "404": {
            "description": "keyId is not one of the caller's keys. Never a cross-account oracle.",
            "x-ecca-error-codes": [
              "key-not-found"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The account's active-key cap is reached: the limit field states the cap that applied.",
            "x-ecca-error-codes": [
              "key-limit-reached"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "key-limit-reached",
                  "message": "This account has reached its active-key cap. Revoke one before creating another."
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "500": {
            "description": "Mint self-check failed or unexpected failure.",
            "x-ecca-error-codes": [
              "mint-error",
              "internal-server-error"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "502": {
            "description": "The store refused the write.",
            "x-ecca-error-codes": [
              "key-write-failed",
              "revoke-failed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Fail-closed: no store (store-unconfigured) or no key-signing secret on this deployment (keys-not-configured). Nothing is created.",
            "x-ecca-error-codes": [
              "store-unconfigured",
              "keys-not-configured"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/checkout": {
      "post": {
        "tags": [
          "billing"
        ],
        "summary": "Create a Stripe Checkout Session for the signed-in account (gated)",
        "description": "Session cookie only: an anonymous call is 401. Fail-closed per plan: without billing configuration the route refuses. The rollout stage is server-side and off by default; a caller the stage does not admit gets 403 checkout-not-rolled-out. The configured price is verified against the published catalog before any Session is created. The response never includes a price id or secret.",
        "operationId": "checkoutCreate",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              },
              "example": {
                "plan": "eccaai-pro",
                "checkoutAttemptId": "attempt_9f1c2d3e4b"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Redirect the buyer to url.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "id"
                  ],
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed state, unknown plan, or malformed checkoutAttemptId.",
            "x-ecca-error-codes": [
              "bad-state",
              "invalid-plan",
              "invalid-checkout-attempt"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "Anonymous checkout is refused; sign in first.",
            "x-ecca-error-codes": [
              "unauthorized"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "The plan is not a live checkout plan (checkout-not-enabled), or the rollout stage does not admit this caller (checkout-not-rolled-out, with stage).",
            "x-ecca-error-codes": [
              "checkout-not-enabled",
              "checkout-not-rolled-out"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "checkout-not-rolled-out",
                  "stage": "off"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The account already holds this tier or higher; no Session is created.",
            "x-ecca-error-codes": [
              "already-subscribed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "already-subscribed",
                  "ownedTier": "pro"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "502": {
            "description": "Stripe could not verify the configured price (stripe-price-unverified), refused the Session (stripe-error), or was unreachable (stripe-unreachable).",
            "x-ecca-error-codes": [
              "stripe-price-unverified",
              "stripe-error",
              "stripe-unreachable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Fail-closed: billing is not configured for this plan (billing-not-configured), or the configured price disagrees with the published catalog (billing-catalog-mismatch, with mismatches).",
            "x-ecca-error-codes": [
              "billing-not-configured",
              "billing-catalog-mismatch"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "billing-not-configured",
                  "plan": "eccaai-pro"
                }
              }
            }
          }
        }
      }
    },
    "/api/entitlement": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "The caller's tier and feature map",
        "description": "Anonymous callers get the free-tier view rather than a 401; the map is not a secret. On a backend hiccup the route degrades to the free view with degraded:true, never to more access.",
        "operationId": "entitlement",
        "security": [
          {},
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Tier and features.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementResponse"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          }
        }
      }
    },
    "/api/export-traces": {
      "get": {
        "tags": [
          "solve"
        ],
        "summary": "Export stored solve traces as NDJSON (gated: operator export token)",
        "description": "Fail-closed: 503 until the deployment configures an export token; 401 for any other bearer. Pages by cursor; X-Trace-Count and X-Next-Cursor headers carry pagination.",
        "operationId": "exportTraces",
        "security": [
          {
            "ExportToken": []
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One JSON object per line: id, timestamp, prompt, options, selectedAnswer, correctAnswer, rationale, confidencePercent, latencyMs, domain, provider, calibrationScore.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Bearer does not match the export token.",
            "x-ecca-error-codes": [
              "unauthorized"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AbuseBrake"
          },
          "503": {
            "description": "No export token configured on this deployment.",
            "x-ecca-error-codes": [
              "export-not-configured"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/capability-status": {
      "get": {
        "tags": [
          "status"
        ],
        "summary": "Per-capability config truth for this deployment",
        "description": "Booleans only; no key, endpoint, model id or variable name can reach the payload. Makes zero outbound calls.",
        "operationId": "capabilityStatus",
        "responses": {
          "200": {
            "description": "Capability map.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CapabilityStatusResponse"
                }
              }
            }
          },
          "405": {
            "description": "Only GET, HEAD and OPTIONS.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "60 per minute per client IP.",
            "x-ecca-error-codes": [
              "rate-limited"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "ok": false,
                  "error": "rate-limited"
                }
              }
            }
          },
          "503": {
            "description": "The probe itself threw; no capability map is returned so no badge is demoted.",
            "x-ecca-error-codes": [
              "probe-unavailable"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "status"
        ],
        "summary": "Uptime probe",
        "description": "Structured booleans and counts, no secrets, no side effects. Reports whether an event sink is writing rather than failing the probe over it.",
        "operationId": "health",
        "responses": {
          "200": {
            "description": "Runtime nominal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "405": {
            "description": "Only GET, HEAD and OPTIONS.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "60 per minute per client IP, with Retry-After.",
            "x-ecca-error-codes": [
              "rate-limited"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Runtime check failed; same body shape with ok:false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/ops/status": {
      "get": {
        "tags": [
          "operator"
        ],
        "summary": "Operator release status page (operator-only)",
        "description": "Rewrite of /api/ops-status. Opening it once with ?token=<operator token> installs the operator session cookie and answers 303 back to /ops/status. Renders names and booleans only; never an environment value, request body, identity or Stripe identifier. noindex, no-store, refreshes every 30 seconds.",
        "operationId": "opsStatus",
        "security": [
          {
            "OperatorSession": []
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One-time bootstrap; never reuse in links."
          }
        ],
        "responses": {
          "200": {
            "description": "Status page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "Session installed; Location: /ops/status."
          },
          "404": {
            "$ref": "#/components/responses/OperatorNotFound"
          }
        }
      }
    },
    "/ops/self-check": {
      "post": {
        "tags": [
          "operator"
        ],
        "summary": "Run the when-it-lands self-check (operator-only)",
        "description": "Rewrite of /api/ops-self-check. Creates an unreachable live Checkout Session, expires it immediately, rehearses signed fulfilment and captures one real dispatch. Never creates a charge. Up to 180 seconds.",
        "operationId": "opsSelfCheck",
        "security": [
          {
            "OperatorSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Proof page listing each step as PASS or NOT PASSED.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/OperatorNotFound"
          }
        }
      }
    },
    "/api/trap-radar": {
      "get": {
        "tags": [
          "radar"
        ],
        "summary": "Trap Radar: how often questions like this split the AIs",
        "description": "Classifies q into a topic in memory and returns that topic's split rate from consented clash-library records only. Under 25 consented records the reply sets the sample flag and carries the labelled sample fixture's counts. Zero engine calls, zero spend. Private 60-second cache, no shared cache.",
        "operationId": "trapRadarGet",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 500
            },
            "description": "The question being typed. Prefer POST so it stays out of the URL."
          }
        ],
        "responses": {
          "200": {
            "description": "Topic split rate, or SAMPLE counts under the floor. The question is not stored, logged or echoed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrapRadarResponse"
                }
              }
            }
          },
          "400": {
            "description": "q is missing or blank after trimming; nothing was dispatched.",
            "x-ecca-error-codes": [
              "empty-prompt"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "405": {
            "description": "Only GET, POST and OPTIONS.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "413": {
            "description": "q is longer than 500 characters, or the POST body is over 4096 bytes.",
            "x-ecca-error-codes": [
              "payload-too-large"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "60 per minute per client IP, with Retry-After.",
            "x-ecca-error-codes": [
              "rate-limited"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "radar"
        ],
        "summary": "Trap Radar (question in the body)",
        "description": "Same as GET with the question in a JSON body, so it never appears in a URL.",
        "operationId": "trapRadar",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "q"
                ],
                "properties": {
                  "q": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Topic split rate, or SAMPLE counts under the floor. The question is not stored, logged or echoed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrapRadarResponse"
                }
              }
            }
          },
          "400": {
            "description": "q is missing or blank after trimming; nothing was dispatched.",
            "x-ecca-error-codes": [
              "empty-prompt"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "405": {
            "description": "Only GET, POST and OPTIONS.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "413": {
            "description": "q is longer than 500 characters, or the POST body is over 4096 bytes.",
            "x-ecca-error-codes": [
              "payload-too-large"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "60 per minute per client IP, with Retry-After.",
            "x-ecca-error-codes": [
              "rate-limited"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/miss-counter": {
      "get": {
        "tags": [
          "status"
        ],
        "summary": "Miss counter: how often the AIs agreed and still did not match the key",
        "description": "Counts from the stored live calibration record /scoreboard reads: agreed panels, agreed panels whose machine-graded answer did not match the reference key, those that matched, and those that need a human grader (never counted as a miss), with n and the run date. With no record stored, basis is none and every count is null. Zero engine calls, no user data. Public cache: 60 seconds in browsers, 300 seconds shared.",
        "operationId": "missCounter",
        "responses": {
          "200": {
            "description": "Counts from the stored record, or nulls with basis none.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MissCounterResponse"
                }
              }
            }
          },
          "405": {
            "description": "Only GET, HEAD and OPTIONS.",
            "x-ecca-error-codes": [
              "method-not-allowed"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "60 per minute per client IP, with Retry-After.",
            "x-ecca-error-codes": [
              "rate-limited"
            ],
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    }
  },
  "x-ecca-mcp": {
    "serverVersion": "6.1.1",
    "registryVersion": "1.0.2",
    "protocolVersion": "2025-06-18",
    "tools": [
      "ecca_synthesize",
      "ecca_pre_mortem",
      "ecca_decision_tournament",
      "ecca_bts_divergence",
      "ecca_refine",
      "ecca_research",
      "ecca_verify",
      "ecca_psychology",
      "ecca_learning",
      "ecca_nutrition",
      "ecca_money",
      "ecca_claim_protocol_validate"
    ],
    "claimProtocol": "TODO: add claim_protocol_validate after PR #379 merges"
  }
}
