Real-world scenarios
Concrete products you can build with Owl ID. Each shows the user-facing flow plus the SDK calls that drive it.
Two building blocks recur:
- Disclosures — plain SD-JWT VC claims (
given_name,nationalities, …). The holder reveals only the ones your DCQL query asks for. - Predicates — facts proven by the holder's wallet in zero knowledge on the device, then attested on Midnight (
age_over_18,verification_level,nationality_eu,resident,email_verified,unique_person). The underlying value never leaves the wallet; you request the predicate andverify/requestPresentationenforce it. See How Owl ID works.
requestPresentation returns a VerifyDcqlResponse — valid plus a perCredential map keyed by your DCQL credentials[].id.
1. Age gate for a bar / venue
Goal: confirm a customer is 18+ before serving alcohol. No name, no birthday — just the green check.
age_over_18 is a predicate: the wallet derives the holder's age from their credential, proves age ≥ 18 in zero knowledge on the device, and Midnight records an attestation. The bar sees valid: true and the issuer's did:web identifier — never the name, exact age, birthdate, address, or document number.
Privacy properties
- The witness (the birthdate) is consumed inside the on-device proof and never leaves the wallet.
- The verifier nonce is single-use — a recorded presentation cannot be replayed.
- The KB-JWT is signed by a wallet-held key the holder unlocked with their passkey; the wallet never exports the key.
- The first proof on a device takes a few seconds; the attestation is then reused across every later age check.
2. Proof of unique humanity — anti-bot signup
Goal: a forum, waitlist, or community wants one account per real human — no bots, no mass fake signups — without learning anyone's identity.
This is the unique_person predicate. The verifier picks a 32-byte scope: an epoch (the campaign) and an app_id (your app). The wallet proves the holder controls a unique personhood secret and derives a per-scope nullifier; Midnight rejects a second claim under the same scope. One human can attest once — and stays uncorrelated across other apps and campaigns.
The signup form learns nothing else — no email, no document, no profile data — and gets bot-resistance and one-account-per-human for free. A user who already signed up cannot do it twice: their nullifier for this (epoch, app_id) is already on-chain.
Pick the scope deliberately. Reuse the same
epocheverywhere you want "one claim total" (one signup ever). Rotate theepochper season/round when you want "one claim per round". Differentapp_ids never correlate to the same human.
3. Ticketing without scalping
Goal: gate a paid event. Each ticket is a credential issued at purchase; at the door the holder proves the ticket and that they are a unique human — so a screenshot, a resold QR, or one person walking in ten friends all fail.
Issuance — when someone buys a ticket, your back office mints a credential bound to the buyer's wallet key:
At the door — the scanner asks for the event + tier disclosures and a unique-person proof scoped to this event:
Why it beats QR-image tickets
- The KB-JWT binds to the door's single-use nonce — a screenshot replays as
valid: false, and the wallet signs each presentation fresh with the holder's unlocked key. - The
unique_personproof, scoped to this event'sepoch, lets each human through once. A resold or shared ticket fails the second scan: the buyer's nullifier for this event is already on-chain. - If a ticket is refunded or charged back, the issuer revokes the credential; the door sees it in real time via
subscribeRevocations. - Hidden disclosures stay hidden — put the buyer's name in the credential for support without the door scanner ever asking for it.
Multi-day passes — use a distinct epoch per day; the same credential then admits each human once per day.
4. KYC-gated onboarding
Goal: a remote-work or fintech platform requires every user to have completed at least KYC tier 2 with any approved provider.
given_name / family_name come back as plain disclosures. verification_level is a predicate — the wallet proves the KYC threshold ('basic' / 'substantial' / 'high', or a numeric level) on-device and the verifier confirms the on-chain attestation. The platform never sees the underlying KYC report or document image.
5. EU-only marketplace
Goal: a service operates only in the EU and needs to confirm sellers are EU nationals — without learning the exact country.
nationality_eu is a set-membership predicate: the wallet proves the holder's nationality is in the approved EU set in zero knowledge. The verifier learns only the boolean — not which country. (To collect the specific country instead, request the nationalities disclosure.)
6. Fair distribution — one human, one claim
Goal: an airdrop, a public-goods grant, or a one-person-one-vote ballot must reach each real human exactly once. No sybil farms, no wallet-splitting.
Each human's nullifier for owldao-grant-round-7 lands on-chain on first claim, so a second attempt — even from a different wallet or device — fails. The DAO learns "a unique human claimed", never who. Rotate the epoch to round-8 next time and every human can claim again.
7. Email-verified contact, age-bracketed content
Goal: smaller checks that pair a predicate with a disclosure.
email_verified, age_over_21, resident are predicates (proven on-device); email is a plain disclosure the holder reveals alongside the proof.
8. Stolen-device credential revocation
Goal: a user's phone is stolen. The issuer revokes the credential; subsequent verifications fail immediately and verifiers can drop cached results.
Issuer side (operator dashboard or back office):
Requires an API key with the admin permission. The verification service writes through to the on-chain revocation_registry (via the sidecar) and broadcasts the change over /ws/revocations.
Verifier side, optional live invalidation:
The next verify() call returns { valid: false, error: 'Credential revoked' }. The signed statuslist+jwt at the issuer's /status/{id} reflects the change on its next fetch — the verifier consults both the live mirror and the status list.
9. GDPR right-to-erasure
Goal: an EU resident exercises their right to be forgotten. Their identifying data on the platform is anonymized; cryptographic audit trails remain hash-only.
The user (or their support rep) triggers erasure from the operator dashboard. The platform:
- Revokes every active credential bound to the holder's
credential_idset. - Replaces stored claim data with anonymized placeholders.
- Retains hash-only audit records (compliance) but strips PII.
- Returns a signed receipt the user keeps as proof.
Owl ID is designed so the platform mostly stores hashes already — there is very little PII to erase. The flow exists for compliance, not because the system retains a copy of the user's documents.
Common patterns
- Render the QR full-screen so phones don't have to crop the camera frame.
- Use a short challenge TTL (60 s) for unattended kiosks, longer (5 min) for online flows.
- Cache verification results keyed by
credential_idhash; expire on TTL or a revocation push event. - Choose
unique_personscopes deliberately — a stableepochfor "once ever", a rotatingepochfor "once per round". Differentapp_ids never correlate. - Expect a one-time delay the first time a holder proves a predicate on a new device (a few seconds of on-device proving); it is reused after that.
- Map
result.errorto friendly messages — the platform returns codes likeCredential revoked,KB-JWT audience mismatch,Untrusted issuer,predicate not attested. - For unlinkability across verifiers, ask the issuer for a Batch (
OwlIssuer.issueBatch) and present a fresh one-time-use credential each time.