← Back to Documentation

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 code
No API key needed for verification. The JS snippet is public — the security comes from the wallet's cryptographic signature, not from a secret on the integrator's page. API keys are only required for the credential issuance flow.

3. Verification Flow

  1. 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. 2

    Snippet displays a QR code

    The QR encodes { nonce, minAge, returnUrl }. On mobile, a deep link opens the wallet app directly. The minAge is embedded in the QR — the user cannot modify it.

  3. 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. 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. 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 stores nonce → signedProof in a TTL cache for 60 seconds. That's the only "storage" that happens — it evicts automatically and contains no personal data.

  6. 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's onResult callback. 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.

OperationWhereFrequencyStorage
Identity verification (KYC)Third-party + AlgorandOnce per userOn-chain permanent
Nonce generationCardless ID APIEvery verificationNone — signed JWT
Credential readWallet → AlgorandEvery verificationNone — read-only
Proof relayCardless ID APIEvery verificationTTL cache, 60s — auto-evicts
No database writes on the verification path. The Cardless ID server's only persistent storage is for credential issuance (one-time per user) and integrator account management. Age verifications themselves leave no trace.

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

Support