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
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.
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.
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.
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:
- The SD-JWT signature is valid (credential is authentic)
- The date-of-birth encoded in the credential yields an age ≥ the threshold as of today
- 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
- User visits
holder.ageguard.devand clicks Get your age credential. - The Veriff SDK launches a guided liveness check and document scan (passport, national ID, or driving licence).
- Veriff returns a verified session result to the AgeGuard Issuer Worker.
- The issuer extracts the date-of-birth, constructs the SD-JWT, signs it, and returns it to the browser.
- The credential is stored in
localStorageand 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)
<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:
// 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
https://verifier.ageguard.dev
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
{
"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
Submit a ZK proof for verification. Returns a signed session JWT on success.
Request body
| Field | Type | Description |
|---|---|---|
| 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)
{
"verified": true,
"age_threshold": 18,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_expires_at": "2026-04-26T12:05:00Z"
}
Response — 200 OK (not verified)
{
"verified": false,
"reason": "proof_invalid"
}
Failure reasons
| reason | Description | |
|---|---|---|
| proof_invalid | The UltraHonk proof did not verify against the provided public inputs. | |
| challenge_expired | The challenge nonce has expired (TTL exceeded or already used). | |
| challenge_unknown | The challenge was not issued by this verifier instance. | |
| credential_invalid | The SD-JWT credential could not be parsed or its signature is invalid. | |
| version_unsupported | The version field is not supported. |
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
{
"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
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:
| Secret | Type | Description |
|---|---|---|
| JWT_SECRET | string | HMAC secret used to sign session JWTs issued after successful verification. |
| VERIFIER_URL | string | Public 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.