Skip to main content
This guide covers trait discovery, Groth16 proof generation, simulation, and submission to OpaqueReputationVerifier.

Prerequisites

  • WASM module (wasmModuleSpecifier)
  • Native announcement rows with V2 PSR attestation metadata
  • snarkjs (pulled in by @opaquecash/psr-prover)
  • Circuit artifacts (default: hosted on opaque.cash /circuits/...)

Step 1: Discover traits

const chain = "ethereum" as const;
const rows = await client.fetchAnnouncementRows(chain);
const traits = await client.discoverTraitsV2(rows, { chain });

if (traits.length === 0) {
  throw new Error("No V2 reputation traits found in inbox");
}

const trait = traits[0];
discoverTraitsV2 validates each 0xB2 marker against the chain’s schema registry and returns only traits issued by an authorized schema authority or delegate. Fetch rows from the native chain, not UAB, because relayed payloads cannot carry the full V2 metadata.

Step 2: Reconstruct the stealth signing key

const stealthPrivKeyBytes = client.getStealthSignerPrivateKeyForReputationTrait(trait);
// Or from a scan output:
// client.getStealthSignerPrivateKey(output)

Step 3: Build the action scope

import { buildActionScope, externalNullifierFromScope } from "@opaquecash/opaque";

const scope = buildActionScope({
  chainId: 11155111,
  module: "my-app",
  actionId: "premium-gate",
});
const externalNullifier = externalNullifierFromScope(scope).toString();
Or use the equivalent static helpers on the client (OpaqueClient.buildReputationActionScope / reputationExternalNullifierFromScope).

Step 4: Generate the proof

const proofData = await client.generateReputationProof({
  trait,
  stealthPrivKeyBytes,
  externalNullifier,
  onProgress: (stage, pct) => console.log(stage, pct),
  // artifacts: { wasmPath, zkeyPath }  // override default hosted paths
});

// proofData: { proof, publicSignals, nullifier, attestationId }
When trait comes from discoverTraitsV2, the SDK uses the trait’s V2 Merkle leaf preimage fields automatically. You only need to pass issuerPkX, traitDataHash, or nonce yourself for custom witness construction.

Step 5: Fetch a valid Merkle root

const merkleRoot = await client.fetchLatestValidReputationRoot();
const valid = await client.isReputationRootValid(merkleRoot);

const history = await client.fetchReputationRootHistory();

Step 6: Simulate (optional)

await client.simulateReputationVerification(walletClient, {
  proofData,
  merkleRoot,
  externalNullifier,
});

Step 7: Verify view (read-only)

const ok = await client.verifyReputationProofView({
  proofData,
  merkleRoot,
  externalNullifier,
});

Step 8: Submit on-chain

Consumes the nullifier on success:
const { txHash } = await client.submitReputationVerification("ethereum", {
  proofData,
  merkleRoot,
  externalNullifier,
});
Solana:
await client.submitReputationVerification("solana", {
  proofData,
  merkleRoot,
  externalNullifier,
});

Full flow diagram

Each (trait, externalNullifier) pair can only be verified once. Choose scopes carefully per action you gate.