AgeGuard / Documentation

AgeGuard Documentation

AgeGuard is a zero-knowledge age verification system. It lets your site confirm that a user meets a configurable age threshold — without ever receiving any personal data.

What AgeGuard is

AgeGuard combines government-grade identity verification with zero-knowledge cryptography to produce a reusable, privacy-preserving age credential. Once a user enrolls, subsequent verifications take seconds and require only a biometric gesture (Face ID, Touch ID, or Windows Hello) — no documents, no uploads, no data leaves the device.

The system is built on three open standards:

  • SD-JWT — selective-disclosure JSON Web Token for the identity credential
  • Noir / UltraHonk — a ZK proving system for the age proof circuit
  • WebAuthn / FIDO2 — biometric authentication to bind the credential to the user's device

The 4-step flow

1
Credential issued (one-time)

The user completes identity verification at holder.ageguard.dev using Veriff. The resulting date-of-birth claim is wrapped in a Schnorr-signed SD-JWT and stored in the browser's localStorage. This step is a one-time setup.

2
SDK embedded in your site

You add the AgeGuard SDK to your page with a single <script> tag (or npm install). Call AgeGuard.create() with your verifier URL and desired age threshold.

3
Biometric + ZK proof generated in the browser

When the user triggers verification, the SDK prompts for a biometric gesture, then uses the Barretenberg WASM backend to generate a Noir UltraHonk proof that the credential's date-of-birth satisfies the age threshold. The proof contains no personal data.

4
Verifier returns a session token

The proof is submitted to the AgeGuard Verifier Worker at verifier.ageguard.dev. If valid, it returns a signed session JWT. Your backend validates the JWT and grants access. No personal data passes through the verifier at any point.

How it works technically

Identity verification → SD-JWT credential

During enrollment, the user completes a liveness + document check via Veriff. The AgeGuard Issuer Worker reads the verified date-of-birth from the Veriff result, constructs a selective-disclosure JWT (SD-JWT) containing the dob claim, and signs it with a Schnorr key pair. The SD-JWT is returned to the browser and persisted in localStorage. The issuer does not retain any personal data after issuing the credential.

ZK proof generation

The age-proof Noir circuit takes as private inputs the credential's dob field and signature, and as public inputs the age_threshold, the current date, and a server-issued challenge nonce. The circuit proves:

  1. The SD-JWT signature is valid (credential is authentic)
  2. The date-of-birth encoded in the credential yields an age ≥ the threshold as of today
  3. The proof is bound to the provided challenge (replay prevention)

Proof generation runs entirely in the browser using the Barretenberg WASM library. Private data — the raw date of birth — never leaves the device.

Proof verification on Cloudflare Workers

The Verifier Worker runs a native UltraHonk verifier (compiled to WASM). It checks the proof against the public inputs, verifies the challenge has not expired (5-minute TTL), and — if all checks pass — issues a short-lived HMAC-signed session JWT to the caller.

The verifier receives: a proof blob, the public inputs (threshold + dates + challenge), and an optional client_id. It receives no personal data — not a name, not a date of birth, not a document image.

Enrollment flow

Users obtain their AgeGuard credential through a one-time enrollment at holder.ageguard.dev.

Steps

  1. User visits holder.ageguard.dev and clicks Get your age credential.
  2. The Veriff SDK launches a guided liveness check and document scan (passport, national ID, or driving licence).
  3. Veriff returns a verified session result to the AgeGuard Issuer Worker.
  4. The issuer extracts the date-of-birth, constructs the SD-JWT, signs it, and returns it to the browser.
  5. The credential is stored in localStorage and bound to the device via WebAuthn.

After enrollment, the credential is reused for every subsequent verification — no re-verification with Veriff is required. If the user clears their browser storage or switches devices, they must re-enroll.

Credential portability: Cross-device credential transfer (via QR export/import) is on the roadmap but not yet available. Each device requires its own enrollment today.

SDK Integration

The fastest way to integrate AgeGuard is the CDN script tag. An npm package is coming soon.

npm package coming soon. The @ageguard/sdk npm package is in active development and will be available for React, Vue, and vanilla JS projects. Until then, use the script tag approach below.

Script tag (HTML)

HTML
<script src="https://cdn.ageguard.dev/sdk/v1/ageguard.js"></script>
<script>
  const ag = AgeGuard.create({
    verifierUrl: 'https://verifier.ageguard.dev',
    ageThreshold: 18  // configurable: 13, 16, 18, 21, etc.
  });

  document.getElementById('verify-btn')
    .addEventListener('click', async () => {
      await ag.init();
      const result = await ag.verify();
      if (result.verified) {
        // User meets the age threshold — unlock content
        console.log('Session token:', result.token);
      }
    });
</script>

AgeGuard.create() options

Option Type Description
verifierUrl string Base URL of the AgeGuard Verifier Worker. Use https://verifier.ageguard.dev for the hosted service, or your own self-hosted URL. required
ageThreshold number Minimum age in years to prove. Supported values: 13, 16, 18, 21 (or any integer 13–21). Defaults to 18. optional
clientId string Optional identifier for your application, logged by the verifier for audit purposes. optional

Handling the result

The ag.verify() promise resolves with a result object:

TypeScript
// Success
{
  verified: true,
  age_threshold: 18,
  token: "eyJ...",           // HMAC-signed session JWT
  token_expires_at: "2026-04-26T12:05:00Z"
}

// Failure
{
  verified: false,
  reason: "proof_invalid"  // or "challenge_expired", "credential_invalid"
}

Pass the token to your backend and validate it with your shared secret. The token payload includes age_threshold and exp (expiry) claims.

Verifier API

The AgeGuard Verifier is a Cloudflare Worker deployed at https://verifier.ageguard.dev. All endpoints accept and return JSON. CORS is enabled for registered origins.

Base URL

URL
https://verifier.ageguard.dev

GET /challenge

GET /challenge

Fetch a one-time challenge nonce. The challenge must be included in the proof (it is a public input to the ZK circuit) and in the POST /verify body. Challenges expire after 300 seconds.

Response — 200 OK

JSON
{
  "challenge":      "a3f2...8e1c",  // 64-character hex string
  "expires_at":     "2026-04-26T12:05:00Z",
  "ttl_seconds":    300
}

Notes

  • The challenge is a 64-character lowercase hex string (256-bit random nonce).
  • Each challenge is single-use. Replaying a used challenge will return a verification failure.
  • The 5-minute TTL accounts for proof generation time (typically 2–10 seconds on modern hardware).

POST /verify

POST /verify

Submit a ZK proof for verification. Returns a signed session JWT on success.

Request body

FieldTypeDescription
version number Protocol version. Must be 1. required
proof_system string Proof system identifier. Must be "ultrahonk". required
proof string Base64-encoded UltraHonk proof blob generated by the SDK. required
credential string The SD-JWT credential string (used to reconstruct public inputs). required
challenge string The 64-char hex challenge obtained from GET /challenge. required
age_threshold number The age threshold used to generate the proof (integer, 13–21). Defaults to 18. optional
client_id string Optional identifier for the calling application. Logged for audit purposes only. optional

Response — 200 OK (verified)

JSON
{
  "verified":         true,
  "age_threshold":    18,
  "token":            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_expires_at": "2026-04-26T12:05:00Z"
}

Response — 200 OK (not verified)

JSON
{
  "verified": false,
  "reason":   "proof_invalid"
}

Failure reasons

reason Description
proof_invalidThe UltraHonk proof did not verify against the provided public inputs.
challenge_expiredThe challenge nonce has expired (TTL exceeded or already used).
challenge_unknownThe challenge was not issued by this verifier instance.
credential_invalidThe SD-JWT credential could not be parsed or its signature is invalid.
version_unsupportedThe version field is not supported.

GET /.well-known/ageguard.json

GET /.well-known/ageguard.json

Returns verifier metadata. Clients can use this to discover the supported proof systems, protocol version, and verifier identity.

Response — 200 OK

JSON
{
  "name":            "AgeGuard Verifier",
  "version":         1,
  "verifier_id":     "https://verifier.ageguard.dev",
  "proof_systems":   ["ultrahonk"],
  "age_thresholds":  [13, 16, 18, 21],
  "docs":            "https://docs.ageguard.dev"
}

Self-Hosting

The AgeGuard Verifier Worker is open source under the MIT licence and can be self-hosted on your own Cloudflare account.

Prerequisites

  • A Cloudflare account with Workers enabled
  • Wrangler CLI v3+
  • Node.js 18+

Deploying

Shell
git clone https://github.com/AgeGuard/ageguard
cd ageguard/packages/verifier
npm install
npx wrangler deploy --env production

Configure the following secrets in your Cloudflare dashboard or via wrangler secret put:

SecretTypeDescription
JWT_SECRETstringHMAC secret used to sign session JWTs issued after successful verification.
VERIFIER_URLstringPublic base URL of your deployed verifier (used as verifier_id in metadata and public inputs).

When self-hosting, the SDK's verifierUrl option must point to your deployment. The ZK circuit's public inputs include the verifier_id, so proofs generated against one verifier URL cannot be replayed against a different verifier URL.