{
  "openapi": "3.1.0",
  "info": {
    "title": "The Gauntlet Public API",
    "version": "1.0.0",
    "description": "Ingest a WebMCP tool-call ledger, seal a server-scored Ed25519 evidence bundle, and query public research results. No API key is required.",
    "license": {
      "name": "MIT",
      "url": "https://github.com/JackCoates1/the-gauntlet/blob/main/LICENSE"
    }
  },
  "servers": [
    {
      "url": "https://gauntlet.jackcoates.co.uk",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Runs"
    },
    {
      "name": "Research"
    },
    {
      "name": "Assets"
    }
  ],
  "paths": {
    "/api/events": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Ingest one tool invocation into an immutable run ledger",
        "description": "Rate limited to 30 requests per IP per sliding minute. A run accepts at most 200 events; each event args object is at most 2 KB.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventIngest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, runId, event, or args.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Run is already sealed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body exceeds 8 KB or args exceed 2 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP event limit or 200-event run budget exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until this IP may retry.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    },
    "/api/scorecards/{runId}": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Seal a server-scored, signed scorecard",
        "description": "Idempotent. Rate limited to 5 requests per IP per sliding hour. Sealing requires at least 2 events over at least 10 seconds, all using known range tools.",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SealRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Original or newly sealed scorecard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SealedScorecard"
                }
              }
            }
          },
          "400": {
            "description": "Invalid run ID or JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body exceeds 2 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Run fails proof-of-interaction plausibility checks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP seal limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until this IP may retry.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Fetch a stored scorecard",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          }
        ],
        "responses": {
          "200": {
            "description": "Stored scorecard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scorecard"
                }
              }
            }
          },
          "400": {
            "description": "Invalid run ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/scorecards/{runId}/evidence": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Fetch signed evidence and hash-chained replay",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          }
        ],
        "responses": {
          "200": {
            "description": "Offline-verifiable evidence bundle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvidenceBundle"
                }
              }
            }
          },
          "400": {
            "description": "Invalid run ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/scorecards/{runId}/percentile": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Community percentile for a sealed run",
        "description": "Server-side ranked count over every other sealed run. Used by the scorecard page for social context (\"Better than 72% of verified runs\").",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          }
        ],
        "responses": {
          "200": {
            "description": "Percentile, peer count and community average.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Percentile"
                }
              }
            }
          },
          "400": {
            "description": "Invalid run ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recent": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Live ticker of the most recent sealed runs",
        "description": "The last N sealed runs in seal-time order, each with a server-side verified flag computed exactly like the leaderboard. Powers the homepage RECENT RUNS strip.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent sealed runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecentRuns"
                }
              }
            }
          }
        }
      }
    },
    "/feed.xml": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Atom feed of verified sealed runs",
        "description": "Live Atom 1.0 feed emitting one entry per sealed run, reusing the /api/recent D1 read and server-side verified flag. Subscribe to watch the leaderboard in real time; cached for one minute.",
        "responses": {
          "200": {
            "description": "Atom 1.0 feed.",
            "content": {
              "application/atom+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/leaderboard": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Query the verified public leaderboard",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "verified",
            "in": "query",
            "description": "Use 0 to include unverified runs; default is verified only.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ],
              "default": "1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent sealed runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Leaderboard"
                }
              }
            }
          }
        }
      }
    },
    "/api/digest": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Aggregate fingerprint-level susceptibility research",
        "responses": {
          "200": {
            "description": "Research digest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Digest"
                }
              }
            }
          }
        }
      }
    },
    "/api/trapstats": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Aggregate per-trap resistance statistics",
        "responses": {
          "200": {
            "description": "Community resistance leaderboard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrapStats"
                }
              }
            }
          }
        }
      }
    },
    "/api/leaderboard.csv": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Download leaderboard rows as CSV",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "verified",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ],
              "default": "1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV export.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },
    "/api/digest.csv": {
      "get": {
        "tags": [
          "Research"
        ],
        "summary": "Download research digest as CSV",
        "responses": {
          "200": {
            "description": "CSV export.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },
    "/api/badge/{runId}.svg": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Render an embeddable score badge",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          },
          {
            "name": "label",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 80
            }
          },
          {
            "name": "score",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "color",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "330×28 SVG badge; cacheable for one hour.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid run ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/scorecards/{runId}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Shareable scorecard HTML with Open Graph and Twitter metadata",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
            },
            "description": "UUID v4 chosen by the client for this run."
          }
        ],
        "responses": {
          "200": {
            "description": "Scorecard page or branded fallback.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/og-banner.png": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Static social-share banner",
        "responses": {
          "200": {
            "description": "1200×630 PNG share image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      },
      "EventIngest": {
        "type": "object",
        "required": [
          "runId",
          "event"
        ],
        "properties": {
          "runId": {
            "type": "string",
            "format": "uuid",
            "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
          },
          "userAgent": {
            "type": "string",
            "maxLength": 500
          },
          "event": {
            "type": "object",
            "required": [
              "tool"
            ],
            "properties": {
              "tool": {
                "type": "string",
                "maxLength": 64,
                "description": "A real range tool name; unknown names make sealing fail."
              },
              "args": {
                "type": "object",
                "additionalProperties": true,
                "description": "JSON object, serialized size ≤ 2 KB."
              },
              "createdAt": {
                "type": "string",
                "format": "date-time",
                "description": "UTC ISO-8601 timestamp; server time is used if omitted."
              }
            }
          }
        }
      },
      "SealRequest": {
        "type": "object",
        "properties": {
          "userAgent": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "Outcome": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "PASS",
              "FAIL",
              "NOT TESTED"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "Scorecard": {
        "type": "object",
        "required": [
          "id",
          "score",
          "total",
          "outcomes"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
          },
          "score": {
            "type": "integer",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "pct": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "badges": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "outcomes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Outcome"
            }
          },
          "engagement": {
            "type": "object",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SealedScorecard": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Scorecard"
          },
          {
            "type": "object",
            "properties": {
              "verified": {
                "type": "boolean",
                "description": "True when the seal-time Ed25519 signature was produced."
              },
              "url": {
                "type": "string"
              },
              "badgeUrl": {
                "type": "string"
              }
            }
          }
        ]
      },
      "EvidenceBundle": {
        "type": "object",
        "required": [
          "runId",
          "eventsRoot",
          "algorithm",
          "publicKey",
          "signature",
          "replay",
          "scorecard"
        ],
        "properties": {
          "runId": {
            "type": "string",
            "format": "uuid",
            "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "userAgent": {
            "type": [
              "string",
              "null"
            ]
          },
          "score": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "eventsRoot": {
            "type": "string"
          },
          "eventCount": {
            "type": "integer"
          },
          "algorithm": {
            "const": "Ed25519"
          },
          "canonicalization": {
            "type": "string"
          },
          "publicKey": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "signature": {
            "type": "string",
            "contentEncoding": "base64"
          },
          "replay": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "scorecard": {
            "$ref": "#/components/schemas/Scorecard"
          },
          "resistanceTimeline": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      },
      "Leaderboard": {
        "type": "object",
        "required": [
          "runs",
          "verifiedCount",
          "totalSealed",
          "generatedAt"
        ],
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Scorecard"
                },
                {
                  "type": "object",
                  "properties": {
                    "verified": {
                      "type": "boolean"
                    },
                    "label": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "browser": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "badgeUrl": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          },
          "verifiedCount": {
            "type": "integer"
          },
          "totalSealed": {
            "type": "integer"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecentRuns": {
        "type": "object",
        "required": [
          "runs",
          "generatedAt"
        ],
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "sealedAt",
                "score",
                "total",
                "verified",
                "url"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid",
                  "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
                },
                "sealedAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "score": {
                  "type": "integer",
                  "minimum": 0
                },
                "total": {
                  "type": "integer",
                  "minimum": 0
                },
                "label": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Agent label, or browser family derived from the user agent."
                },
                "verified": {
                  "type": "boolean"
                },
                "url": {
                  "type": "string",
                  "description": "Shareable /scorecards/{id} path."
                }
              }
            }
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Digest": {
        "type": "object",
        "properties": {
          "cards": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "totalRuns": {
            "type": "integer"
          },
          "verifiedRuns": {
            "type": "integer"
          }
        }
      },
      "TrapStats": {
        "type": "object",
        "properties": {
          "traps": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "hardestTrap": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "community": {
            "type": "object",
            "additionalProperties": true
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Percentile": {
        "type": "object",
        "required": [
          "id",
          "percentile",
          "betterThanCount",
          "peerCount",
          "averagePct"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "7f0c2e5a-1d4b-4a9e-9f21-3c8b6d5e2a10"
          },
          "percentile": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Share of peer runs this run beat; null when no peers."
          },
          "betterThanCount": {
            "type": "integer",
            "minimum": 0
          },
          "peerCount": {
            "type": "integer",
            "minimum": 0
          },
          "averagePct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean peer score percentage."
          }
        }
      }
    }
  }
}
