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
| Term | Definition |
|---|---|
| session_id | A server-issued identifier sent to the client at session start. Required for all subsequent protocol steps. |
| bridge_id | A server-side-only identifier generated alongside the session_id. Never transmitted to the client. Embedded in the token signature. |
| token | A URL-safe, cryptographically random string issued after successful verification. The bearer credential for the verification. |
| token signature | An HMAC-SHA256 digest binding the token to its bridge_id and issuance timestamp. Prevents token forgery. |
| badge | A dynamically rendered SVG or PNG image at /badge/{token}.svg that reflects the current state of the token. |
| bridge | The server-side session mechanism that enforces drawing completion before token issuance. |
| human-likeness score | An integer from 0 to 100 representing the degree to which a submitted gesture resembles human-produced input. |
| validity threshold | The 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:
- Session start — the client requests a session. The server generates a
session_id(returned to the client) and abridge_id(retained server-side only). - 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. - 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
| State | Meaning |
|---|---|
| CREATED | Session started. drawing_completed: false, token_issued: false. |
| DRAWING_COMPLETE | Drawing submitted and accepted. Token staged. drawing_completed: true. |
| TOKEN_ISSUED | Token returned to client. Session consumed. token_issued: true. No further token requests accepted. |
| EXPIRED | Session 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:
| Field | Type | Required | Notes |
|---|---|---|---|
| prompt | string ≤200 | Yes | The prompt shown to the user |
| sessionId | string | Yes | From /api/session/start |
| startedAt | integer | Yes | Unix timestamp in milliseconds |
| endedAt | integer | Yes | Unix timestamp in milliseconds |
| strokes | array ≤500 | Yes | Array of stroke objects |
| strokes[].strokeId | integer | Yes | Stroke index |
| strokes[].points | array | Yes | Point samples |
| points[].x | double | Yes | X coordinate in pixels |
| points[].y | double | Yes | Y coordinate in pixels |
| points[].t | integer | Yes | Timestamp in milliseconds |
| points[].p | double | Yes | Pressure 0.0–1.0. Use 0.5 if unavailable. |
| displayName | string ≤60 | No | Self-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:
- At session start, the server generates a
session_id(public) and abridge_id(private). Only thesession_idis transmitted to the client. - When a drawing is accepted, the server stages the token internally, bound to the
bridge_idvia HMAC-SHA256. The token is not transmitted. - When the client presents its
session_idto/api/token, the server looks up the session, confirmsdrawing_completed: true, markstoken_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:
| Field | Description |
|---|---|
| token_id | 40-character URL-safe Base64 random string. The bearer credential. |
| issued_at | ISO 8601 UTC timestamp recorded at token creation. |
| bridge_id | The server-side bridge identifier (stored on the server, not in the token). |
| signature | HMAC-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
| State | Condition | Cache-Control |
|---|---|---|
| Verified | Token exists, not expired, signature valid | public, max-age=300 |
| Expired | Token exists, ExpiresAt in the past | public, max-age=3600 |
| Invalid | Token not found, or signature invalid | public, 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:
| Dimension | Weight | Signal |
|---|---|---|
| Velocity smoothness | 20% | Natural acceleration and deceleration |
| Curvature entropy | 30% | Organic variation in path direction |
| Pressure variance | 10% | Variation in stylus/touch pressure |
| Stroke rhythm | 20% | Natural inter-stroke timing variation |
| Path complexity | 20% | 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
- Token signing: HMAC-SHA256 with a server-side secret key. Signatures are verified at badge-render time using constant-time comparison to prevent timing attacks.
- Session single-use: Each session can produce at most one token. The
token_issuedflag is set atomically before returning the token. - Replay detection: SHA-256 hash of the raw gesture payload is stored and compared against all previous submissions.
- Request size limit: Gesture payloads are capped at 512 KB.
- Rate limiting: Per-endpoint fixed-window limits (see §11).
- Security headers:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Content-Security-Policy. - Admin endpoint: Protected by timing-safe key comparison. Configurable via environment variable.
11. Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
| /api/session/start | 10 requests | 1 minute |
| /api/verify | 5 requests | 1 minute |
| /api/token | 10 requests | 1 minute |
| /badge/{token}.* | 30 requests | 1 minute |
| /v/{token} | 30 requests | 1 minute |
| /api/check/{token} | 30 requests | 1 minute |
| /admin/logs | 3 requests | 15 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:
- Gesture traces (stroke coordinates, timestamps, pressure values) — used for scoring only
- Derived behavioural metrics (velocity, curvature, rhythm) — stored for audit
- Optional self-reported display name — stored as-is, not verified
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:
- Implement the Bridge mechanism as described in §6
- Generate a
bridge_idthat is never transmitted to the client - Sign tokens using HMAC-SHA256 and verify signatures at badge-render time
- Enforce session single-use for token issuance
- Return badge state based solely on token existence, age, and signature validity
- Not require accounts, logins, cookies, or personal data from the user