Protocol

Tokens

Verification tokens are EdDSA JWTs scoped to a site key; their header, claims, lifetime, single-use semantics and the JWKS endpoint.

Format

A compact JWS with header {"alg":"EdDSA","typ":"JWT","kid":"inh-…"}. Only EdDSA is ever issued or accepted; alg: none and other families are rejected before any key lookup. Maximum length 8192 characters.

Claims
{
  "iss": "https://imnotahuman.ai",
  "sub": "agent_nova_8f2a",
  "aud": "site_live_exampleweb2ab3cd4ef5gh6j",
  "iat": 1789905607,
  "exp": 1789906207,
  "jti": "ch_mfr4q2xk7tbn3ycz5w6ah2ep4d",
  "inh": {
    "v": 1,
    "mode": "live",
    "challenge_id": "ch_mfr4q2xk7tbn3ycz5w6ah2ep4d",
    "method": "ed25519_challenge",
    "origin": "https://example.com",
    "action": "checkout",
    "intent": "purchase",
    "agent": { "id": "agent_nova_8f2a", "name": "Nova" },
    "subject_type": "agent"
  }
}

Claims

ClaimValueDescription
isshttps://imnotahuman.aiIssuer. Must match exactly.
subagent idThe verified agent: a registry id, agent_simulated_test, or the Web Bot Auth origin.
audsite keyThe site key that created the challenge. Compare to your own key.
iatintegerIssued at (unix seconds) = verification time.
expintegeriat + 600.
jtichallenge idUnique per token; equals the challenge id. Use it for replay tracking when verifying offline.
inh.v1Claim-set version.
inh.mode"live" | "test"Test tokens are rejected by tokens/verify unless allow_test is true.
inh.challenge_idstringSame as jti.
inh.methodVerificationMethoded25519_challenge, web_bot_auth or simulated.
inh.originstringOrigin the challenge was created from.
inh.action / inh.intentstringAs declared by the host when creating the challenge (empty strings when unset).
inh.agent{ id, name }Display information for the agent.
inh.subject_type"agent"Fixed in v1.

Single use

Each issued token is stored server-side keyed by jti. POST /api/v1/tokens/verify with consume: true marks it used atomically; any later presentation fails with TOKEN_ALREADY_USED. Without consume the endpoint only reports whether the token is still unconsumed. Offline verification cannot see consumption state.

JWKS

GET https://imnotahuman.ai/.well-known/jwks.json (also /api/v1/.well-known/jwks.json and /api/v1/jwks) returns the active signing keys with Cache-Control: public, max-age=300. Select the key by the token header's kid. Retired keys are removed from the set; cache no longer than a few minutes so rotation is picked up.

JWKS
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "_Gqb_W0_oiqrRCCw8P_X8DSeNSwTiMpA4rUS3OqqBZk",
      "kid": "inh-q2Xk7tBn",
      "use": "sig",
      "alg": "EdDSA"
    }
  ]
}

Verification checklist

  1. Parse the compact JWS; require alg = EdDSA and a kid.
  2. Verify the Ed25519 signature with the JWK whose kid matches.
  3. Check iss, exp > now, inh.v = 1.
  4. Check aud equals your site key.
  5. Reject inh.mode = "test" in production.
  6. Enforce single use: consume online, or track jti until exp.
  7. Compare inh.action / inh.intent / inh.origin to the operation at hand.