{
  "openapi": "3.1.0",
  "info": {
    "title": "FDKEY Agent Demo API",
    "description": "Live demo of FDKEY's HTTP-based AI agent verification. An AI agent (with no FDKEY API key of its own) hits a protected endpoint, receives a 402 with an embedded puzzle and a short-lived HMAC ticket, solves the puzzle, submits the answers with the ticket, and retries the original endpoint to retrieve verified-only data. Useful as a target for ChatGPT Custom GPT Actions, Anthropic Claude tool-use, and other agent frameworks that consume OpenAPI specifications. The integrator's FDKEY API key stays server-side at fdkey.com; the agent never sees it.",
    "version": "1.0.0",
    "contact": {
      "name": "FDKEY",
      "url": "https://fdkey.com"
    },
    "license": {
      "name": "MIT",
      "url": "https://github.com/fdkey/sdks/blob/main/LICENSE"
    }
  },
  "servers": [
    {
      "url": "https://fdkey.com",
      "description": "Live demo at fdkey.com (no authentication required to call these endpoints; the demo's FDKEY API key is held server-side)."
    }
  ],
  "paths": {
    "/api/fdkey-demo/agent-secret": {
      "get": {
        "operationId": "getAgentSecret",
        "summary": "The protected endpoint — gated by FDKEY",
        "description": "The actual demo route. On the first call (unverified) this returns HTTP 402 with the puzzle data and a short-lived `challenge_ticket` in the body, plus an `X-FDKEY-Session` response header carrying the session id the agent should thread back on subsequent calls. After the agent calls `submitChallenge` with the answers + ticket and the verification succeeds, calling this endpoint again with the same `X-FDKEY-Session` returns 200 with the secret payload.",
        "responses": {
          "200": {
            "description": "Verified agent. Returns the secret payload that proves the agent passed the FDKEY gate on this session.",
            "headers": {
              "X-FDKEY-Session": {
                "description": "Session id (echoed). Keep threading this on every call.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AgentSecretResponse" }
              }
            }
          },
          "402": {
            "description": "Verification required. The body contains the puzzles to solve and the `challenge_ticket` to use on submitChallenge. The `X-FDKEY-Session` response header carries the session id you must thread back on submitChallenge and the retry of this endpoint.",
            "headers": {
              "X-FDKEY-Session": {
                "description": "The session id minted by the SDK for this verification attempt. The agent MUST send this back as the `X-FDKEY-Session` request header on `/api/fdkey/submit` and on the retry of this endpoint.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChallengeRequired" }
              }
            }
          }
        }
      }
    },
    "/api/fdkey/submit": {
      "post": {
        "operationId": "submitChallenge",
        "summary": "Submit the puzzle answers — relays to FDKEY's scoring server",
        "description": "Send the agent's answers along with the `challenge_ticket` (as a Bearer token in the Authorization header) and the session id (as `X-FDKEY-Session` request header). The SDK relays the submission to FDKEY's VPS using its own API key (which the agent never sees), verifies the returned JWT, and marks the session verified. Returns `{ verified: true, score, tier }` on success.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "description": "`Bearer <challenge_ticket>` — the HMAC-signed ticket from the 402 response.",
            "schema": { "type": "string", "example": "Bearer eyJhbGciOiJIUzI1NiI..." }
          },
          {
            "name": "X-FDKEY-Session",
            "in": "header",
            "required": true,
            "description": "Session id from the 402's `X-FDKEY-Session` response header. Must match the ticket's bound session.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubmitRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission processed. `verified: true` means the agent passed; `verified: false` means the puzzle answers didn't pass scoring — call `getAgentSecret` again to receive a fresh 402 with a new puzzle.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubmitResponse" }
              }
            }
          },
          "400": {
            "description": "Malformed body — `challenge_id` missing or `answers` not an object.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
            }
          },
          "401": {
            "description": "Ticket required, expired, invalid, or bound to a different session. Inspect `error` for the specific cause and call `getAgentSecret` again for a fresh ticket.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
            }
          }
        }
      }
    },
    "/api/fdkey/challenge": {
      "get": {
        "operationId": "refreshChallenge",
        "summary": "Fetch a fresh puzzle without re-hitting the protected endpoint",
        "description": "Optional. Returns a new puzzle (and a fresh `challenge_id`) bound to the existing session. Useful when the first puzzle expired or failed and the agent wants to retry without re-triggering the 402 flow. Requires a valid Bearer ticket and the same session id.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "description": "`Bearer <challenge_ticket>` from the prior 402.",
            "schema": { "type": "string" }
          },
          {
            "name": "X-FDKEY-Session",
            "in": "header",
            "required": false,
            "description": "Session id (from the 402's `X-FDKEY-Session` response header). Not strictly required for the challenge endpoint, but recommended for consistency.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Fresh challenge body (no `error`/`reason` fields; otherwise the same shape as the 402).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChallengeBody" }
              }
            }
          },
          "401": {
            "description": "Bearer ticket missing, expired, or invalid.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AgentSecretResponse": {
        "type": "object",
        "properties": {
          "secret": { "type": "string", "description": "The verified-only payload." },
          "message": { "type": "string" },
          "sessionId": { "type": "string" },
          "verifiedAt": { "type": "integer", "format": "int64", "description": "ms epoch when the session was verified" },
          "score": { "type": "number", "format": "float", "description": "Capability score, 0..1" },
          "tier": { "type": "string", "description": "Capability tier label (e.g. `gold`)" },
          "docs": { "type": "string", "format": "uri" }
        },
        "required": ["secret"]
      },
      "ChallengeRequired": {
        "allOf": [
          { "$ref": "#/components/schemas/ChallengeBody" },
          {
            "type": "object",
            "properties": {
              "error": { "type": "string", "enum": ["fdkey_verification_required"] },
              "reason": {
                "type": "string",
                "enum": ["no_session", "unknown_session", "expired_session"],
                "description": "Why the 402 fired. Recovery path is the same for all three (solve and submit)."
              },
              "challenge_ticket": {
                "type": "string",
                "description": "Short-lived HMAC-signed JWT bound to the session id. Pass as `Authorization: Bearer <ticket>` on submitChallenge. Default TTL: 5 minutes."
              }
            },
            "required": ["error", "reason", "challenge_ticket"]
          }
        ]
      },
      "ChallengeBody": {
        "type": "object",
        "description": "The puzzle data the agent must solve.",
        "properties": {
          "challenge_id": {
            "type": "string",
            "description": "Server-side identifier for this challenge. Include in the submit body."
          },
          "expires_at": { "type": "string", "format": "date-time" },
          "expires_in_seconds": { "type": "integer", "description": "Time until the puzzle expires (~60s on the live demo)." },
          "difficulty": { "type": "string", "enum": ["easy", "medium", "hard"] },
          "types_served": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Which puzzle types are served in this challenge (e.g. `[\"type1\",\"type3\"]`)."
          },
          "puzzles": {
            "type": "object",
            "description": "Per-type puzzle data. Each type carries its own `instructions` field telling the agent how to format the answer. Shape varies by type — read `instructions` and the per-puzzle keys (`questions`, `options`, `concept`, etc.). Examples: type1 = multiple-choice questions, type3 = semantic-ranking puzzle.",
            "additionalProperties": true
          },
          "submit_url": {
            "type": "string",
            "description": "Where to POST the answers (usually `/api/fdkey/submit`)."
          },
          "hint": { "type": "string", "description": "Human-readable summary of the flow." }
        },
        "required": ["challenge_id", "expires_at", "puzzles", "submit_url"]
      },
      "SubmitRequest": {
        "type": "object",
        "properties": {
          "challenge_id": {
            "type": "string",
            "description": "The `challenge_id` from the 402 body (or from a refreshChallenge response)."
          },
          "answers": {
            "type": "object",
            "description": "Per-type answers. Read each puzzle's `instructions` for the expected answer shape. Example for a type1+type3 challenge: `{\"type1\":[{\"n\":1,\"answer\":\"B\"},{\"n\":2,\"answer\":\"A\"},{\"n\":3,\"answer\":\"C\"}],\"type3\":{\"n\":1,\"answer\":\"F > A > B > G > C\"}}`. The exact shape follows the per-puzzle `instructions` field in the challenge body.",
            "additionalProperties": true
          }
        },
        "required": ["challenge_id", "answers"]
      },
      "SubmitResponse": {
        "type": "object",
        "properties": {
          "verified": { "type": "boolean" },
          "score": { "type": "number", "format": "float", "description": "Only present when `verified: true`." },
          "tier": { "type": "string", "description": "Only present when `verified: true`." },
          "message": { "type": "string", "description": "Only present when `verified: false`." }
        },
        "required": ["verified"]
      },
      "ErrorBody": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" }
        },
        "required": ["error"]
      }
    }
  }
}
