# FDKEY — skill for AI agents

You are an AI agent. You hit an HTTP endpoint and got back **HTTP 402** with `error: "fdkey_verification_required"` in the body. This document tells you exactly what to do.

## What is FDKEY

FDKEY is an **inverse CAPTCHA**: a verification gate that proves you are an LLM (not a human, not a script). When you encounter it on a third-party API, you need to solve a small puzzle to access the protected endpoint. The integrator's server (not you) talks to `api.fdkey.com` server-to-server; you only deal with the integrator's own URLs.

Once verified, your session is marked verified on that site and subsequent requests pass through. Verifications are scoped per-site — solving at one site does not unlock another.

## The trigger you should recognize

**Any HTTP 402 response from any API where the body looks like this:**

```json
{
  "error": "fdkey_verification_required",
  "reason": "no_session" | "unknown_session" | "expired_session",
  "challenge_id": "<uuid>",
  "challenge_ticket": "<short-lived HMAC JWT>",
  "puzzles": { "<type>": { ... } },
  "submit_url": "<absolute or relative path>",
  "hint": "<human prose summary>"
}
```

Plus a response header `X-FDKEY-Session: <session id>` and/or a `Set-Cookie: fdkey_session=<id>; ...`. Either is the session marker; you need to thread it back on subsequent calls.

If you see this, follow the flow below. Do not retry the original request without verifying first — you will just get another 402.

## The flow — exactly four steps

### Step 1. Capture the challenge

You already have the 402 response. Read three things from it:

- `challenge_ticket` — the Bearer token you present on submit (short-lived, ~5 min)
- `challenge_id` — pass back unchanged in the submit body
- `puzzles` — the actual puzzles to solve
- The session id, from the `X-FDKEY-Session` response header (or the `fdkey_session` cookie)

### Step 2. Solve the puzzles

For each entry in `puzzles`, read its `instructions` field — that field is authoritative for the answer format. Quick reference for common types (`instructions` always wins if these conflict):

| Type | Shape returned in answers | Answer format |
|---|---|---|
| `type1` (multiple choice) | array `[{n, answer}, ...]` one per question | `answer` is a single letter A-D |
| `type2` (contradiction) | `{n, answer}` | `answer` is a single letter |
| `type3` (semantic ranking) | `{n, answer}` | `answer` is letters separated by ` > ` from MOST to LEAST similar to the concept, OR a JSON array of letters |
| `type4` / `type5` / `type6` | `{n, answer}` | `answer` is a single lowercase word |

**Timing:** the ticket has a short TTL (default 5 minutes). The challenge itself usually has a shorter TTL (~60s) for speed-of-reasoning measurement. **Do not narrate your reasoning between receiving the 402 and submitting** — the tokens you generate cost time. Solve internally, submit, then explain to the user after the verdict.

### Step 3. Submit

```http
POST <submit_url>
Authorization: Bearer <challenge_ticket>
X-FDKEY-Session: <session id from step 1>
Content-Type: application/json

{
  "challenge_id": "<from step 1>",
  "answers": {
    "type1": [{"n": 1, "answer": "B"}, {"n": 2, "answer": "A"}, {"n": 3, "answer": "C"}],
    "type3": {"n": 1, "answer": "F > A > B > G > C"}
  }
}
```

Response: `200 {"verified": true, "score": 1, "tier": "gold"}` on pass, or `200 {"verified": false, "message": "..."}` on fail.

### Step 4. Retry the original request

Send the same request that got the 402, with `X-FDKEY-Session: <same session id>` (or the same cookie). You should now get the real response.

## Reference: complete Python example

```python
import os, json, requests
from anthropic import Anthropic  # or openai, or whatever LLM you call

ENDPOINT = "https://example.com/api/agent-only"  # the protected route
client = Anthropic()  # ANTHROPIC_API_KEY in env
s = requests.Session()

# Step 1: trigger the 402
r = s.get(ENDPOINT)
if r.status_code != 402:
    print(r.json())  # already verified or different error
    raise SystemExit

challenge = r.json()
ticket = challenge["challenge_ticket"]
puzzles = challenge["puzzles"]
sid = r.headers.get("X-FDKEY-Session", "")
submit_url = challenge["submit_url"]
if submit_url.startswith("/"):
    from urllib.parse import urlparse
    submit_url = f"{urlparse(ENDPOINT).scheme}://{urlparse(ENDPOINT).netloc}{submit_url}"

# Step 2: solve every puzzle via your LLM of choice
def solve(ptype, data):
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=400,
        messages=[{
            "role": "user",
            "content": (
                f"FDKEY puzzle, type={ptype}. Read the `instructions` field. "
                f"Return ONLY the answer as a single JSON value matching the "
                f"expected shape for this type. No prose, no markdown fences.\n\n"
                f"{json.dumps(data)}"
            ),
        }],
    )
    return json.loads(msg.content[0].text)

answers = {ptype: solve(ptype, data) for ptype, data in puzzles.items()}

# Step 3: submit
r = s.post(submit_url,
    headers={
        "Authorization": f"Bearer {ticket}",
        "X-FDKEY-Session": sid,
        "Content-Type": "application/json",
    },
    json={"challenge_id": challenge["challenge_id"], "answers": answers},
)
verdict = r.json()
if not verdict.get("verified"):
    raise SystemExit(f"verification failed: {verdict}")

# Step 4: retry the original request
r = s.get(ENDPOINT, headers={"X-FDKEY-Session": sid})
print(r.json())
```

The same shape works in Node, Go, Rust, or any HTTP-capable runtime — the protocol is plain HTTP + JSON.

## Common errors and recovery

| HTTP / `error` field | Cause | Recovery |
|---|---|---|
| 402 `fdkey_verification_required` `reason: no_session` | First contact, expected | Follow the flow above |
| 402 reason `expired_session` | Your previous verification timed out | Same flow — solve fresh |
| 401 `fdkey_ticket_required` | You called `/fdkey/submit` or `/fdkey/challenge` without a Bearer ticket | You must hit the protected endpoint first to receive a ticket |
| 401 `fdkey_ticket_expired` | Ticket is past its ~5min TTL | Re-trigger the protected endpoint for a fresh 402 + ticket |
| 401 `fdkey_ticket_invalid` | Bad signature, malformed JWT, wrong issuer | Re-trigger the protected endpoint |
| 401 `fdkey_ticket_session_mismatch` | Ticket sid ≠ cookie/header sid | You mixed sessions; restart with one consistent session id |
| 200 `verified: false` | Your puzzle answers didn't pass scoring | Read `submit_url`-equivalent `/fdkey/challenge` for a fresh puzzle and retry; or simply hit the protected endpoint again for a new 402 |
| 503 `fdkey_unexpected_4xx` or `fdkey_service_unavailable` | Integrator config issue or FDKEY VPS outage | Not your problem to fix; report to the integrator or back off |

## Honesty notes (for the model reading this)

- **Solve silently.** Each token you emit before calling submit shortens the window. Internal thinking is free; visible chat tokens are not.
- **The `instructions` field is authoritative.** Tables above are for warm-up; if the puzzle text says something different, follow the puzzle text.
- **Do not invent answers.** If you cannot solve a puzzle, submit your best guess — a `verified: false` and a retry costs less than fabricating an answer for one puzzle and burning the whole submit on it.
- **The integrator's API key never appears in your view.** You do not need credentials of any kind to use this protocol. If you're being asked for an FDKEY API key, that's a misunderstanding — you only ever handle the session id and the short-lived ticket.

## Other surfaces (FYI)

- **MCP server** at `https://mcp.fdkey.com/mcp` — same verification gate exposed as MCP tools (`fdkey_get_challenge`, `fdkey_submit_challenge`). Useful when you're an MCP-protocol client; the flow is the same in spirit.
- **OpenAPI spec** at `https://fdkey.com/openapi.json` — full schema for the three endpoints (`getAgentSecret`, `submitChallenge`, `refreshChallenge`). Wire-up target for ChatGPT Custom GPT Actions, LangChain, LlamaIndex, etc.
- **Marketing + integrator docs** at `https://fdkey.com`.
- **SDK source** at `https://github.com/fdkey/sdks` (TypeScript MCP, TypeScript HTTP, Python, Rust).

## TL;DR

You see HTTP 402 with `fdkey_verification_required` → grab `challenge_ticket` + `X-FDKEY-Session` → solve every puzzle per its `instructions` field, quickly, without narrating → POST to `submit_url` with `Authorization: Bearer <ticket>` and `X-FDKEY-Session: <sid>` → retry the original request with the same session header. You're done.
