Skip to main content
NOMEVEX The exam should look like the work. The evidence should stand up to scrutiny.
Developer Documentation

Integration guides, LTI 1.3 setups, telemetry schemas, and API references for Nomevex.

Verification SDK

Independently verify exam session integrity using cryptographic proofs.

What exists

There is exactly one verification SDK: the Rust crate verification-sdk, at microservices/verification-sdk in this codebase, currently version 0.1.0.

There is no npm package.

There is no PyPI package, nothing on crates.io, and no Go module either. This SDK has never been published to any registry. If you were told to run npm install @nomevex/verify or pip install nomevex-verify, that instruction was wrong and the package does not exist.

Earlier revisions of this page also showed a JavaScript API — verifySession(), verifyProof(), getProof() — over objects like VerifySDK.create(). No such functions exist in this codebase, in this crate or in any other. Every function name on this page is transcribed from the source; the list below is the full public surface.

To use it from a browser you build the crate yourself and load the generated bindings:

wasm-pack build microservices/verification-sdk --target web

Pin the revision. The crate is license = "proprietary" and is at 0.1.0, so its signatures and its JSON shapes can change without a major-version bump. Depend on a specific commit, not on a branch:

verification-sdk = { git = "https://github.com/Nomevex/verification-sdk", rev = "<commit-sha>" }

The licence is the reason the crate is not on crates.io, and it is worth knowing before you plan an integration: ask us for a licence grant rather than assuming the code is open. The in-product consumer is our verification portal, whose Dioxus/WASM client portal-client compiles the crate straight into its browser bundle — so the portal you can point an auditor at is running exactly the code described here.

What it checks

Three independent levels, each of which can pass while another fails — a bundle that clears level 1 has not thereby cleared level 3.

  1. Chain continuity — batch sequence numbers are contiguous and each batch's prev_batch_hash matches its predecessor's payload_hash. Catches a deleted or reordered batch.
  2. Merkle checkpoint consistency — every checkpoint's root really is the Merkle root of the batches in the range it claims, and its batch_sequence_start/ batch_sequence_end cover the session with no gaps. Catches a summary that does not describe the data.
  3. Anchor receipts — the receipt's body hash matches its decoded body, its checkpoint_payload_hash matches the checkpoint, the Merkle roots agree, and — if you supply a verifier — the receipt exists on the chain you say it does. This is the only level that proves the institution could not rewrite history after the fact.

Ed25519 signature authenticity is checked separately, against a key set you already trust (see Public keys below). Levels 1 and 2 do not verify signatures, so a bundle can be internally consistent and still unsigned garbage.

Using it from JavaScript

Every export takes JSON strings and returns a JSON string, so there is no marshalling layer to learn. The result is always one of:

{"valid":true}
{"valid":false,"error":{"field":"<name>","message":"<reason>"}}

field tells you which input was at fault (json, batches, checkpoints, receipt, on_chain or verification) and message says why. A field of json or batches is a malformed export, not tampering. Read the field before you report tampering.

import init, {
  verify_signed_batch_continuity_wasm,
  verify_merkle_checkpoint_consistency_wasm,
  verify_anchor_receipt_wasm,
} from './verification_sdk/pkg/verification_sdk.js';

// WASM must be initialised before any export is called.
await init();

const { session_id, batches, checkpoints, receipts } = bundle;

const level1 = JSON.parse(verify_signed_batch_continuity_wasm(JSON.stringify(batches)));
const level2 = JSON.parse(
  verify_merkle_checkpoint_consistency_wasm(
    JSON.stringify(batches),
    JSON.stringify(checkpoints),
  ),
);

let level3 = { valid: false, error: { field: 'on_chain', message: 'no receipts in bundle' } };
if (receipts?.length) {
  // Async: returns a Promise<string>. Awaiting it is not optional.
  //
  // The third argument is a SYNCHRONOUS JS function that receives the receipt
  // JSON and returns a boolean. An async callback is not supported — a
  // function returning a Promise has no boolean to read, and the call fails
  // with "on_chain_verifier must return a boolean". Do your RPC read first and
  // close over the answer.
  const verifiedOnChain = receiptJson => rpcHasReceipt(JSON.parse(receiptJson));
  level3 = JSON.parse(
    await verify_anchor_receipt_wasm(
      JSON.stringify(receipts[0]),
      JSON.stringify(checkpoints[0]),
      verifiedOnChain,
    ),
  );
}

console.log(level1.valid, level2.valid, level3.valid);

Merkle helpers

These two are conveniences, not part of the three verification levels, and their input shapes are different from each other. That is how the crate is written today (lib.rs:218 and :249), so it is worth stating rather than letting you discover it from a wrong root.

import init, {
  compute_merkle_root_wasm,
  compute_merkle_proof_wasm,
} from './verification_sdk/pkg/verification_sdk.js';

await init();

// NOTE the two different input shapes. They are not interchangeable and the
// crate does not normalise between them.

// compute_merkle_root_wasm takes an ARRAY OF OBJECTS, each with a `leaf` key.
const root = compute_merkle_root_wasm(
  JSON.stringify([
    { leaf: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
    { leaf: 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' },
  ]),
);
// -> a base64url string, 43 chars (32 bytes, unpadded base64url)

// compute_merkle_proof_wasm takes an OBJECT with a camelCase `leafHashes` key.
const proof = JSON.parse(
  compute_merkle_proof_wasm(
    JSON.stringify({
      leafHashes: [
        'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
        'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE',
      ],
    }),
    1,
  ),
);
// -> { leaf: string, proof: string[], root: string }
//    or { error: 'invalid index' } / { error: 'parse error' }

Two behaviours to know before you rely on either. A leaf that is not a 32-byte base64url digest is skipped silently — it is not an error, and dropping one leaf changes the root, so validate your input if the root has to match a published value. And compute_merkle_root_wasm returns the all-zero digest, not an error, when no leaf is usable.

Using it from Rust

The native API is a better fit than the wasm bindings if you are verifying server-side: it takes typed structs instead of JSON strings, returns Result<(), VerificationError> with a typed VerificationErrorKind, and takes your own key provider instead of a JavaScript callback.

use verification_sdk::{
    verify_signed_batch_continuity, verify_merkle_checkpoint_consistency,
    verify_batch_signature, verify_checkpoint_signature,
    verify_anchor_receipt, KeyLookup, AnchorProvider,
    SignedBatchV1, MerkleCheckpointV1, AnchorReceiptV1,
};

// Level 1 — chain continuity and payload hashes.
verify_signed_batch_continuity(&batches)?;

// Level 2 — each checkpoint really summarises the batches it claims.
verify_merkle_checkpoint_consistency(&batches, &checkpoints)?;

// Ed25519 authenticity, against a key set the caller already trusts.
verify_batch_signature(&batches[0], &keys)?;
verify_checkpoint_signature(&checkpoints[0], &keys)?;

// Level 3 — async: needs an AnchorProvider to answer "is this on-chain?"
verify_anchor_receipt(&receipt, &checkpoint, &provider).await?;

verify_anchor_receipt is async because your AnchorProvider does the chain read. The in-wasm build supplies a provider that trusts the JavaScript callback's answer, which is why the wasm binding takes a function instead: there is no async provider to pass across the boundary.

Public keys

The SDK does not decide which keys are trustworthy — you do, because only you know which institution you are verifying. Load the key set you were given and hand it to KeyLookup, or implement PublicKeyProvider yourself. A verification portal serves its set at /.well-known/verification-keys.json in the shape:

{
  "keys": [
    { "kid": "nae-2026-01", "public_key": "<32 bytes, base64url no padding>" }
  ]
}

The kid is the value that appears in each batch's and checkpoint's signature.kid, so a signature from an unknown key is a verification failure rather than a crash.

What a pass does not mean

Verification proves the record is internally consistent and, at level 3, anchored where the receipt says it is. It does not prove the candidate behaved honestly, that no collusion occurred, or that the events in the bundle are the events that happened — a party that never wrote an event down cannot be contradicted by a hash chain. Automated flags are always subject to human review before any adverse action; see our AI Disclosure Notice and Privacy Policy.

Not sure you need to write code?

You probably do not. The verification portal runs the same three levels against the same crate in your browser, with no account, and prints a plain-language result. Upload the exported bundle and read that first; come back here when you need the result in your own system.

White-label colours, marks and domains.

Plans, usage metering and invoices.

Audit exports for ABET, NBA, NAAC and AACSB.

What the platform is made of, and how the parts fit.

Read it, then check it.

These pages describe a system designed so its claims can be checked. The verification SDK and the public portal let you confirm a session's integrity yourself, without trusting the platform.

  • Session integrity verifiable from the audit bundle alone
  • Public verification portal — no account required
  • Schemas published alongside the API reference
See cryptographic verification