Verification Protocol
How Cardless ID age verification works — blockchain-native signed proofs, stateless relay, and a verification path that never touches a database.
1. Overview
Cardless ID separates two distinct operations that happen at very different frequencies:
Credential Issuance — once per user
A user goes through identity verification once. A W3C Verifiable Credential is issued and anchored to Algorand as a non-transferable NFT. This is the only step that requires Cardless ID's backend to do significant work.
Age Verification — every time a site checks
The integrator embeds a JS snippet. The user scans a QR code. The wallet reads the credential directly from Algorand and produces a cryptographically signed proof. The Cardless ID server relays the proof — it never queries the blockchain or a database.
This is analogous to Web3 login (Sign-In with Ethereum): the site asks a question, the wallet proves something using an on-chain asset, and the site gets a signed, verifiable answer. The blockchain is the source of truth; the API is just a lightweight relay.
2. The JS Snippet
Integrators add age verification with a single script tag. No backend required.
CDN embed
<script src="https://cdn.cardlessid.org/verify/latest/cardlessid-verify.js"></script>
<div id="age-gate"></div>
<script>
const verify = new CardlessIDVerify({
minAge: 21,
onVerified: function({ meetsRequirement }) {
if (meetsRequirement) {
// User is 21+ — grant access
}
}
});
verify.mount('#age-gate');
</script>npm / ES module
import { CardlessIDVerify } from '@cardlessid/verify';
const verify = new CardlessIDVerify({
minAge: 21,
onVerified({ meetsRequirement }) {
if (meetsRequirement) {
// grant access
}
},
});
verify.mount('#age-gate'); // renders QR code3. Verification Flow
- 1
Snippet fetches a nonce
On load, the snippet calls
GET /api/v/nonce. The server returns a short-lived signed nonce (expires in 5 minutes). No database write happens — the nonce is a signed JWT the server can verify later without storing anything. - 2
Snippet displays a QR code
The QR encodes
{ nonce, minAge, returnUrl }. On mobile, a deep link opens the wallet app directly. TheminAgeis embedded in the QR — the user cannot modify it. - 3
Wallet reads the credential from Algorand
The wallet app queries Algorand directly — no Cardless ID server involved. It reads the credential NFT held by the user's wallet address, inspects the ARC-69 metadata, and checks whether the birthdate satisfies
minAge. This is a read-only operation; no transaction is created and no gas is spent. - 4
Wallet signs a proof
If the credential exists and the age requirement is met, the wallet signs a proof with the user's Algorand private key:
{ "nonce": "eyJ...", "walletAddress": "XYZAQ5E3...", "minAge": 21, "meetsRequirement": true, "timestamp": 1744829470000 }The private key never leaves the wallet. The signature proves the holder of that wallet address made this assertion.
- 5
Wallet submits proof to Cardless ID API
The wallet POSTs the signed proof to
POST /api/v/submit. The server verifies the nonce signature (stateless check), then storesnonce → signedProofin a TTL cache for 60 seconds. That's the only "storage" that happens — it evicts automatically and contains no personal data. - 6
Snippet retrieves and delivers the proof
The snippet polls
GET /api/v/result/{nonce}every 1–2 seconds. Once found, it delivers the signed proof to the integrator'sonResultcallback. The integrator can verify the Algorand signature client-side or pass it to their backend.
4. Full Sequence Diagram
The Cardless ID API appears twice: once as a stateless nonce issuer, and once as a 60-second proof relay. It never queries Algorand. The integrator's backend is entirely optional.
5. The Signed Proof
The proof is the only thing that crosses the wire between the wallet and the integrator. It contains no personal data — only a boolean assertion and the wallet address.
Proof structure
{
"payload": {
"nonce": "eyJhbGciOiJIUzI1NiJ9...",
"walletAddress": "XYZAQ5E3EQFWMH3MQVK2BX...",
"minAge": 21,
"meetsRequirement": true,
"timestamp": 1744829470000
},
"signature": "base64url-encoded-ed25519-signature"
}Verifying the proof (client-side)
import { verifyProof } from '@cardlessid/verify';
const result = verifyProof(proof);
if (result.valid && result.payload.meetsRequirement) {
// Age verified — ed25519 signature and timestamp confirmed
}What the signature proves
The Ed25519 signature was produced by the private key corresponding to walletAddress. Since only the wallet holds that key, the signature proves the wallet owner made this assertion — it cannot be forged by the integrator page, Cardless ID's server, or anyone else.
What the on-chain check adds
The signature proves the wallet made the assertion. For higher assurance, an integrator can independently query Algorand to confirm the wallet holds a valid credential from a registered issuer. Without this check, someone could generate a fresh Algorand keypair and self-sign a proof with no credential backing it.
6. Scalability
Every component in the verification path is either stateless or ephemeral. There is no database bottleneck.
| Operation | Where | Frequency | Storage |
|---|---|---|---|
| Identity verification (KYC) | Third-party + Algorand | Once per user | On-chain permanent |
| Nonce generation | Cardless ID API | Every verification | None — signed JWT |
| Credential read | Wallet → Algorand | Every verification | None — read-only |
| Proof relay | Cardless ID API | Every verification | TTL cache, 60s — auto-evicts |
7. Security Properties
Replay prevention
Each nonce is a signed JWT with a 5-minute expiry. The TTL cache evicts proofs after 60 seconds. A captured proof cannot be reused — the nonce will be expired or already consumed.
Forgery prevention
The proof is signed with the wallet's Ed25519 private key. No one — not the integrator, not Cardless ID's server, not a network observer — can produce a valid signature without that key.
No personal data in transit
The proof contains only a wallet address and a boolean. No name, birthdate, or document information is transmitted during verification. The integrator learns only that the wallet address meets the age requirement.
Tamper-proof age requirement
The minAge is embedded in the nonce when it's issued and echoed back in the signed proof. The wallet signs the minAge it was asked to check — the integrator page cannot lower the requirement after the fact.
No integrator secret required
Unlike the legacy challenge/response flow, the JS snippet needs no API key. There is no secret to leak from the browser. Security is provided entirely by the wallet's private key and the on-chain credential.
8. Partner Wallet Apps
Third parties may build wallet apps that are compatible with the Cardless ID verification protocol. Because the wallet is responsible for reading the on-chain credential and producing the signed proof, a malicious wallet could lie about the result.
Partner wallets require audit and review
All partner wallet apps must be reviewed and approved by Cardless ID before deployment. The review verifies that the wallet correctly reads the on-chain credential, checks the age requirement honestly, and signs only truthful proofs.
Integrators who require stronger guarantees should independently query Algorand to confirm the wallet holds a valid credential issued by a registered issuer — regardless of what the wallet app claims. The wallet address in the signed proof is the lookup key.
9. Standards Compatibility
The Cardless ID verification protocol is designed to align with SIOPv2 (Self-Issued OpenID Provider v2) and OID4VP (OpenID for Verifiable Presentations) — the W3C and OpenID Foundation standards for decentralized identity presentation. This positions Cardless ID for interoperability with SSI ecosystem pilots including eIDAS 2.0 wallet programs.
Current: Custom flow
The verification flow described in this document uses a custom nonce + Algorand ed25519 signed proof format. This ships first and has no external dependencies.
Planned: SIOP-OID4VP envelope
A future update will wrap the same cryptographic proof in a standard W3C Verifiable Presentation, enabling any SIOP-OID4VP–compliant wallet to participate after audit and review.
Algorand wallets as DIDs
SIOP-OID4VP uses Decentralized Identifiers (DIDs) as wallet identifiers. Algorand wallet addresses map to DIDs via the did:algo method:
did:algo:XYZAQ5E3EQFWMH3MQVK2BX...A did:algo resolver reads the Algorand account state and produces a W3C DID Document containing the wallet's ed25519 public key. No transaction or gas required — it's a read-only node query.
The underlying infrastructure (stateless nonce, TTL cache proof relay) is unchanged between the custom flow and the SIOP-OID4VP flow. Only the message format changes — from a custom JSON proof to a standard Verifiable Presentation.
Related Documentation
Integration Guide
Full SDK reference, code examples, and security best practices for integrators.
Smart Contract Architecture
How credentials are anchored to Algorand and the issuer registry design.
Privacy Architecture
Data retention, zero-knowledge design, and privacy rights.
Credential Schema
W3C Verifiable Credential field definitions and structure.
Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Community: Discord (coming soon)
