Protocol v0.1 Documentation
← Back to kalyax.com
Kalyax Protocol v0.1
Human Verification Protocol — Formal Specification
Status: Draft  ·  Updated: March 2026  ·  kalyax.com

1. Abstract

The Kalyax Protocol defines a mechanism for verifying that an action was initiated by a human, using gesture biometrics as the verification signal. It produces a signed, time-limited token that can be embedded as a badge and verified by third parties without requiring accounts, cookies, or personal data.

The protocol consists of three phases: session initiation, drawing submission, and token issuance. Token issuance is gated behind a server-side Bridge that enforces completion of the drawing phase before a token can be issued.

2. Definitions

TermDefinition
session_idA server-issued identifier sent to the client at session start. Required for all subsequent protocol steps.
bridge_idA server-side-only identifier generated alongside the session_id. Never transmitted to the client. Embedded in the token signature.
tokenA URL-safe, cryptographically random string issued after successful verification. The bearer credential for the verification.
token signatureAn HMAC-SHA256 digest binding the token to its bridge_id and issuance timestamp. Prevents token forgery.
badgeA dynamically rendered SVG or PNG image at /badge/{token}.svg that reflects the current state of the token.
bridgeThe server-side session mechanism that enforces drawing completion before token issuance.
human-likeness scoreAn integer from 0 to 100 representing the degree to which a submitted gesture resembles human-produced input.
validity thresholdThe minimum human-likeness score required for a token to be marked isValid: true. Default: 40.

3. Protocol Overview

The full protocol flow consists of three sequential steps:

  1. Session start — the client requests a session. The server generates a session_id (returned to the client) and a bridge_id (retained server-side only).
  2. Drawing submission — the client submits a gesture payload including the session_id. The server analyses the gesture, computes a human-likeness score, generates a signed token, and stores it against the bridge session. The token is not returned to the client at this stage.
  3. Token issuance — the client presents its session_id. If and only if the drawing step has been completed, the server marks the session consumed (preventing replay) and returns the token to the client.

Invariant: No token can be issued unless a drawing submission for the same session was previously accepted and scored. This is enforced server-side and cannot be bypassed by the client.

4. Session Lifecycle

StateMeaning
CREATEDSession started. drawing_completed: false, token_issued: false.
DRAWING_COMPLETEDrawing submitted and accepted. Token staged. drawing_completed: true.
TOKEN_ISSUEDToken returned to client. Session consumed. token_issued: true. No further token requests accepted.
EXPIREDSession lifetime (15 minutes) elapsed. No token can be issued regardless of drawing state.

Session identifiers are single-use. Once a token has been issued, the session cannot be reused. Clients must call /api/session/start again for each new verification.

5. Endpoints

5.1 POST /api/session/start

Initiates a new verification session. No request body required.

Rate limit: 10 requests per minute.

Response 200:

{ "sessionId": "a3f9..." }

5.2 POST /api/verify

Submits a gesture for analysis. Returns the human-likeness score. Does not return a token.

Rate limit: 5 requests per minute.

Request body:

{
  "prompt":      "Draw a circle.",
  "sessionId":   "a3f9...",
  "startedAt":   1710000000000,
  "endedAt":     1710000002500,
  "strokes": [
    {
      "strokeId": 0,
      "points": [
        { "x": 150.0, "y": 200.0, "t": 1710000000000, "p": 0.5 }
      ]
    }
  ],
  "displayName": "alice"  // optional
}

Fields:

FieldTypeRequiredNotes
promptstring ≤200YesThe prompt shown to the user
sessionIdstringYesFrom /api/session/start
startedAtintegerYesUnix timestamp in milliseconds
endedAtintegerYesUnix timestamp in milliseconds
strokesarray ≤500YesArray of stroke objects
strokes[].strokeIdintegerYesStroke index
strokes[].pointsarrayYesPoint samples
points[].xdoubleYesX coordinate in pixels
points[].ydoubleYesY coordinate in pixels
points[].tintegerYesTimestamp in milliseconds
points[].pdoubleYesPressure 0.0–1.0. Use 0.5 if unavailable.
displayNamestring ≤60NoSelf-reported identifier. Not verified.

Response 200:

{ "isValid": true, "humanLikenessScore": 74 }

Response 403: Session invalid, expired, or already consumed.

A 200 response does not mean a token has been issued. The client must subsequently call /api/token.

5.3 POST /api/token

Exchanges a completed session for a verification token. One-time use per session.

Rate limit: 10 requests per minute.

Request body:

{ "sessionId": "a3f9..." }

Response 200:

{
  "verificationToken": "Xk9a...",
  "verificationUrl":   "https://kalyax.com/v/Xk9a...",
  "expiresAt":         "2026-04-10T00:00:00+00:00",
  "isValid":           true
}

Response 403: Drawing not yet completed, session expired, or token already issued.

5.4 GET /v/{token}

Returns a human-readable HTML verification page. Intended for sharing with third parties. Shows score, metrics, badge, and copy snippets. Rate limit: 30/min.

5.5 GET /badge/{token}.svg

Returns a dynamically rendered SVG badge reflecting the current token state. The signature is verified on every request. Cache headers are set per state (see §8).

Rate limit: 30 requests per minute.

5.6 GET /badge/{token}.png

Redirects (HTTP 302) to /badge/{token}.svg. Provided for compatibility with embed contexts that require a .png URL.

5.7 GET /api/check/{token}

Public token verification API. Allows third-party servers to independently verify a token.

Response 200 (token found):

{
  "exists":             true,
  "valid":              true,
  "expired":            false,
  "isValid":            true,
  "expiresAt":          "2026-04-10T00:00:00+00:00",
  "humanLikenessScore": 74,
  "signatureValid":     true
}

Response 200 (token not found):

{ "exists": false, "valid": false, "expired": false }

6. Bridge Mechanism

The Bridge is a server-side session mechanism that enforces the following invariant:

No token can be minted without a prior accepted drawing submission for the same session.

The Bridge operates as follows:

  1. At session start, the server generates a session_id (public) and a bridge_id (private). Only the session_id is transmitted to the client.
  2. When a drawing is accepted, the server stages the token internally, bound to the bridge_id via HMAC-SHA256. The token is not transmitted.
  3. When the client presents its session_id to /api/token, the server looks up the session, confirms drawing_completed: true, marks token_issued: true (preventing replay), and returns the staged token.

The bridge_id is never exposed to the client, never embedded in HTML, and not used at badge-render time. Its sole purpose is to bind the token signature to a specific server-side session, providing cryptographic proof that the token was issued through the legitimate protocol flow.

7. Token Structure

A token consists of:

FieldDescription
token_id40-character URL-safe Base64 random string. The bearer credential.
issued_atISO 8601 UTC timestamp recorded at token creation.
bridge_idThe server-side bridge identifier (stored on the server, not in the token).
signatureHMAC-SHA256(signing_key, token_id + "|" + issued_at + "|" + bridge_id)

Token lifetime defaults to 30 days. Tokens expire at a fixed ExpiresAt timestamp set at issuance and cannot be refreshed — a new verification is required.

8. Badge States

● Verified ● Expired ● Invalid
StateConditionCache-Control
VerifiedToken exists, not expired, signature validpublic, max-age=300
ExpiredToken exists, ExpiresAt in the pastpublic, max-age=3600
InvalidToken not found, or signature invalidpublic, max-age=60

The badge image updates automatically. Embedding HTML never needs to change — the badge URL resolves to the correct state on every request.

9. Humanness Scoring

The human-likeness score (0–100) is a weighted combination of five heuristic dimensions:

DimensionWeightSignal
Velocity smoothness20%Natural acceleration and deceleration
Curvature entropy30%Organic variation in path direction
Pressure variance10%Variation in stylus/touch pressure
Stroke rhythm20%Natural inter-stroke timing variation
Path complexity20%Direction changes and inflection points

A replay penalty of −50 points is applied when a gesture payload is an exact byte-for-byte repeat of a previous submission. A constant-velocity penalty is applied to the velocity sub-score when motion appears artificially uniform.

Tokens are marked isValid: true when the score meets or exceeds the validity threshold (default: 40). A token may be issued with isValid: false for low-scoring submissions.

10. Security

11. Rate Limits

EndpointLimitWindow
/api/session/start10 requests1 minute
/api/verify5 requests1 minute
/api/token10 requests1 minute
/badge/{token}.*30 requests1 minute
/v/{token}30 requests1 minute
/api/check/{token}30 requests1 minute
/admin/logs3 requests15 minutes

Requests exceeding limits receive HTTP 429. Limits are applied per IP address.

12. Privacy

The Kalyax Protocol is designed to verify humanness without identifying individuals. The following data is collected:

The following data is explicitly not collected: names, email addresses, IP addresses, device fingerprints, cookies, tracking identifiers, or any personal data beyond the gesture itself.

Expired sessions are deleted automatically by a background cleanup process.

13. Conformance

A conforming implementation of the Kalyax Protocol MUST:

Developer Guide
Embedding Kalyax human verification — Kalyax Protocol v0.1
Audience: web developers integrating Kalyax into a site or application

1. Overview

Kalyax lets you add human verification to your site without accounts, CAPTCHAs, or third-party cookies. The user draws a gesture on a canvas; Kalyax analyses it and issues a signed token. You display the token as a badge — a small image that updates automatically as the token expires.

The simplest integration uses an iframe — Kalyax handles the entire UI and returns the token to your page via postMessage. For full control, use the API directly.

2. Prerequisites

3. Integration Flow

Both the iframe and API-direct approaches follow the same underlying protocol steps:

  1. Start a bridge session → receive session_id
  2. User draws a gesture
  3. Submit gesture payload with session_id → receive score
  4. Exchange session_id for token → receive verificationToken
  5. Store token, display badge

4. Step 1 — Start a Session

Before the user can draw, your page must obtain a session_id. Call this on page load (or just before showing the canvas).

async function startSession() {
  const res = await fetch('https://kalyax.com/api/session/start', {
    method: 'POST',
  });
  const { sessionId } = await res.json();
  return sessionId;
}

const sessionId = await startSession();

Store the sessionId in memory only. Do not persist it to localStorage or cookies — it is a short-lived (15-minute) single-use identifier.

5. Step 2 — Capture the Gesture

You must capture pointer events from a canvas element and record each point as { x, y, t, p } where:

const strokes = [];
let currentStroke = null;
let startedAt = null;

canvas.addEventListener('pointerdown', e => {
  startedAt = startedAt ?? Date.now();
  currentStroke = { strokeId: strokes.length, points: [] };
  currentStroke.points.push({
    x: e.offsetX, y: e.offsetY,
    t: Date.now(), p: e.pressure || 0.5
  });
});

canvas.addEventListener('pointermove', e => {
  if (!currentStroke) return;
  currentStroke.points.push({
    x: e.offsetX, y: e.offsetY,
    t: Date.now(), p: e.pressure || 0.5
  });
});

canvas.addEventListener('pointerup', () => {
  if (currentStroke) {
    strokes.push(currentStroke);
    currentStroke = null;
  }
});

6. Step 3 — Submit the Drawing

When the user signals completion (e.g. clicks a submit button), POST the gesture payload to /api/verify. Include the sessionId obtained in Step 1.

async function submitDrawing(sessionId, strokes, startedAt) {
  const res = await fetch('https://kalyax.com/api/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      prompt:    'Draw a circle.',  // the prompt you showed the user
      sessionId,
      startedAt,
      endedAt:   Date.now(),
      strokes,
    }),
  });

  if (!res.ok) throw new Error(`Verify failed: ${res.status}`);
  return res.json(); // { isValid, humanLikenessScore }
}

The response contains isValid and humanLikenessScore but no token. A token is issued in Step 4 regardless of score — you may choose to gate further actions on isValid.

7. Step 4 — Exchange for Token

Immediately after a successful /api/verify response, call /api/token with the same sessionId.

async function getToken(sessionId) {
  const res = await fetch('https://kalyax.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sessionId }),
  });

  if (!res.ok) throw new Error(`Token failed: ${res.status}`);
  return res.json();
  // {
  //   verificationToken: "Xk9a...",
  //   verificationUrl:   "https://kalyax.com/v/Xk9a...",
  //   expiresAt:         "2026-04-10T00:00:00+00:00",
  //   isValid:           true
  // }
}

Store verificationToken in your database or user profile. It is the value you will use to construct badge URLs.

8. postMessage Integration (iframe embed)

If you embed https://kalyax.com in an iframe, the page fires a postMessage to the parent window automatically after token issuance.

// Add this to your parent page
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://kalyax.com') return;
  if (event.data?.type !== 'kalyax:verified') return;

  const { token, verificationUrl, isValid, expiresAt } = event.data;
  console.log('Verified!', token);
  // store token, update UI, etc.
});

The message payload:

FieldTypeDescription
typestringAlways "kalyax:verified"
tokenstringThe verification token
verificationUrlstringFull URL to the verification page
isValidbooleanWhether the gesture met the validity threshold
expiresAtstringISO 8601 expiry timestamp

Always check event.origin === 'https://kalyax.com' before trusting the message.

9. Displaying the Badge

Once you have a verificationToken, construct the badge URL and embed it anywhere.

Web (SVG — recommended)

<a href="https://kalyax.com/v/TOKEN">
  <img src="https://kalyax.com/badge/TOKEN.svg"
       alt="Verified by Kalyax">
</a>

GitHub README (Markdown)

[![Verified by Kalyax](https://kalyax.com/badge/TOKEN.svg)](https://kalyax.com/v/TOKEN)

Email Signature (PNG)

Email clients block pasted HTML. Use your client's insert-image UI:

  1. Insert image using the image URL: https://kalyax.com/badge/TOKEN.png
  2. Link the image to: https://kalyax.com/v/TOKEN

The badge image updates automatically. The embed code never needs to change — the badge reflects the current token state on every request.

10. Verifying a Token Server-Side

To confirm a token is valid before taking an action (e.g. accepting a form submission), call /api/check/{token} from your server.

// Node.js example
async function checkToken(token) {
  const res = await fetch(
    `https://kalyax.com/api/check/${token}`
  );
  const data = await res.json();
  return data.valid && data.signatureValid && !data.expired;
}

Only trust a token if all three conditions are met: valid: true, signatureValid: true, expired: false.

11. Error Handling

StatusEndpointCauseAction
400/api/tokenMissing sessionIdEnsure sessionId is in body
403/api/verifyInvalid or expired sessionCall /api/session/start again
403/api/tokenDrawing not complete, expired, or already issuedRestart from session/start
429AnyRate limit exceededBack off and retry after 60s
500/api/tokenInternal error — session/verification mismatchRestart from session/start

Sessions expire after 15 minutes. If the user takes longer than 15 minutes between starting and submitting, call /api/session/start again to obtain a fresh session.

12. Complete Example

The following is a minimal end-to-end integration using the Kalyax API directly.

// 1. On page load — start bridge session
let sessionId = null;
let strokes = [];
let startedAt = null;

async function init() {
  const res = await fetch('/api/session/start', { method: 'POST' });
  ({ sessionId } = await res.json());
}

// 2. Canvas gesture capture
canvas.addEventListener('pointerdown', e => {
  startedAt = startedAt ?? Date.now();
  currentStroke = { strokeId: strokes.length, points: [] };
  currentStroke.points.push({ x: e.offsetX, y: e.offsetY, t: Date.now(), p: e.pressure || 0.5 });
});
canvas.addEventListener('pointermove', e => {
  if (currentStroke)
    currentStroke.points.push({ x: e.offsetX, y: e.offsetY, t: Date.now(), p: e.pressure || 0.5 });
});
canvas.addEventListener('pointerup', () => {
  if (currentStroke) { strokes.push(currentStroke); currentStroke = null; }
});

// 3. On submit button click
submitBtn.addEventListener('click', async () => {
  // Step 3: submit drawing
  const verifyRes = await fetch('https://kalyax.com/api/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      prompt: 'Draw a circle.', sessionId,
      startedAt, endedAt: Date.now(), strokes,
    }),
  });
  if (!verifyRes.ok) { return showError('Verification failed'); }

  // Step 4: exchange for token
  const tokenRes = await fetch('https://kalyax.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sessionId }),
  });
  if (!tokenRes.ok) { return showError('Token issuance failed'); }

  const { verificationToken, verificationUrl } = await tokenRes.json();

  // Step 5: display badge
  badgeEl.innerHTML = `
    <a href="${verificationUrl}">
      <img src="https://kalyax.com/badge/${verificationToken}.svg"
           alt="Verified by Kalyax">
    </a>`;

  // Refresh session for next verification
  sessionId = null; strokes = []; startedAt = null;
  await init();
});

init(); // start on load