Skip to main content

SDK examples

Each snippet mirrors code that exists in-repo. HTML comments cite the path.


Precompute + present (standard pattern)

import { OpenAC, PredicateOp } from "openac-sdk";

const openac = await OpenAC.init({ assetsDir: "./assets" });
const keys = await openac.loadKeysFromUrl("https://cdn.example/keys", "1k");

// Precompute once per credential (~2 s)
const precomputed = await openac.precompute({
jwt: sdJwtToken,
disclosures: ["WyJzYWx0...", "..."],
issuerPublicKey: { kty: "EC", crv: "P-256", x: "...", y: "..." },
keys,
});

// Present per verification (~100 ms) — applies Reblind internally
const proof = await openac.present({
precomputed,
verifierNonce: "challenge-123",
devicePrivateKey: "0xabcdef...",
keys,
showInputOptions: {
normalizedClaimValues: [890615n],
predicates: [{ claimRef: 0, op: PredicateOp.GE, compareValue: 18n }],
},
});

The normalizedClaimValues entry represents a date formatted as YYYYMMDD (e.g. 890615 = 15 June 1989). The predicate GE 18 checks that the normalized age value is ≥ 18 when compared using the Show circuit's valueBits encoding.


Age eligibility — profile example

This is the Annex E.1 flow from the implementation profile. The relying party learns only pass or fail, never the date of birth.

// Verifier: send request
const request = {
audience: "merchant.example",
nonce: crypto.randomUUID(),
expires_at: new Date(Date.now() + 60_000).toISOString(),
statement: "age_eligibility",
threshold: 18,
accepted_credential_profiles: ["example.driver_license.profile"],
};

// Wallet: present
const proof = await openac.present({
precomputed,
verifierNonce: request.nonce,
devicePrivateKey,
keys,
showInputOptions: {
normalizedClaimValues: [birthdayAsYYYYMMDD],
predicates: [{ claimRef: birthdayIndex, op: PredicateOp.GE, compareValue: 18n }],
},
});

// Verifier: check
const result = await openac.verifyProof(proof.serialize(), verifyingKeys);
// result.valid && result.expressionResult === true → age_eligibility passed
// Issuer trust + revocation check must be done separately

Verifier interpretation: expressionResult === true means the date-of-birth claim satisfies ≥ threshold. The verifier must still validate issuer trust and credential status before granting access. See Integration patterns — OpenAC acceptance vs final relying-party acceptance.


Predicate composition (postfix)

import { PredicateOp, LogicToken, buildShowCircuitInputs, DEFAULT_SHOW_PARAMS } from "openac-sdk";

// Prove: age >= 18 AND nationality == 1 (encoded as integer)
const showInputs = buildShowCircuitInputs(DEFAULT_SHOW_PARAMS, nonce, sig, deviceKey, {
normalizedClaimValues: [25n, 1n],
predicates: [
{ claimRef: 0, op: PredicateOp.GE, compareValue: 18n },
{ claimRef: 1, op: PredicateOp.EQ, compareValue: 1n },
],
logicExpression: [
{ type: LogicToken.REF, value: 0 }, // ref to predicate[0]
{ type: LogicToken.REF, value: 1 }, // ref to predicate[1]
{ type: LogicToken.AND, value: 0 }, // AND the two refs
],
});

The postfix logic expression is evaluated by the EvalPredicate Circom component. LogicToken.REF pushes a predicate result; AND, OR, NOT combine the top of stack. See Generalized predicates — composing predicates.

Predicate success is not disclosure. The verifier learns that age >= 18 AND nationality == 1 is true. It does not learn the actual age or nationality value.


Human verification with duplicate prevention — profile example

This is the Annex E.2 flow from the implementation profile. It adds a revocation witness and nullifier as declared extensions.

Extension — not in SDK 0.1.0

The revocation witness and nullifier inputs shown below require an extension to the Show circuit. They are not available in openac-sdk 0.1.0. This example shows the intended interface for tracking purposes.

// Verifier: send request
const request = {
audience: "verifier.example",
app_id: "example-app",
challenge: crypto.randomUUID(),
expires_at: new Date(Date.now() + 60_000).toISOString(),
statement: "human_verification",
revocation_root: "accepted-root-identifier",
};

// Wallet: (future) compute revocation witness locally
// Preferred: download the full status structure, compute witness on device.
// Do NOT send the credential-specific revocation identifier to a witness server.
const witness = await computeRevocationWitness(credential, request.revocation_root);
if (!witness) throw new Error("No admissible prepared state or witness available");

// Wallet: present with revocation witness and nullifier
const proof = await openac.present({
precomputed,
verifierNonce: request.challenge,
devicePrivateKey,
keys,
showInputOptions: {
normalizedClaimValues: [...],
predicates: [...],
// future extensions:
revocationWitness: witness,
nullifierInput: deriveNullifier(credential, request.app_id),
},
});

// Verifier: process in this order to prevent invalid submissions blocking valid ones
const result = await openac.verifyProof(proof.serialize(), verifyingKeys);
if (!result.valid) throw new Error("Proof failed");
checkFreshnessAndExpiry(request, proof); // check challenge + expiry
checkRevocationRoot(result, request.revocation_root); // check root matches request
const nullifier = result.nullifier;
if (await nullifierStore.has(request.app_id, nullifier)) {
throw new Error("Duplicate: nullifier already accepted for this app_id");
}
await nullifierStore.record(request.app_id, nullifier); // durable storage required
return { result: "pass", statement: "human_verification" };

Verifier order is critical. Record the nullifier only after proof verification succeeds. And use durable storage — in-memory nullifier stores break duplicate prevention across restarts.


One-shot createProof

const proof = await openac.createProof({
jwt: sdJwtToken,
disclosures,
issuerPublicKey,
devicePrivateKey: "0xabcdef...",
verifierNonce: "challenge-123",
keys,
});

const result = await openac.verifyProof(proof.serialize(), verifyingKeys);

createProof combines precompute + present in a single call. Use it for testing or simple integrations where the latency cost of running Prepare inline is acceptable. For production wallets, prefer the split precompute / present pattern with a persistent prepared-state pool.


Where tests expand on these examples