VDM NexusDocsBeta

Verify a receipt

How a third party can confirm a Nexus signed-inference receipt is real.

A signed-inference receipt is only useful if downstream actors can verify it without trusting the agent that received it. Here's how.

Don't want to write code? Paste any receipt JSON into the hosted verifier at verify.vdmnexus.com and get a green/red verdict on each of the five checks below. No install, no account. This page covers the same checks for the cases where you want them running in your own process — CI, a webhook, a middleware.

The five checks

A v: 2 receipt can be independently verified along five axes:

  1. Prompt hash matches. sha256(role:content joined by \n) over the messages equals receipt.prompt_hash.
  2. Response hash matches. sha256(response_text) equals receipt.response_hash.
  3. Nexus signature is valid. receipt.nexus_signature is an Ed25519 signature, by the Nexus operator key, over the canonical JSON of the receipt (with nexus_signature stripped).
  4. Payment exists on Solana. receipt.payment.tx_signature is a confirmed transaction with a USDC transfer of receipt.payment.amount_usdc to receipt.payment.pay_to.
  5. Payer matches. The fee-payer / first signer of that transaction equals receipt.agent_pubkey.

If all five pass, the receipt is end-to-end verifiable: the operator signed it, the agent actually paid for it, and the prompt/response you hold are the ones the operator saw.

One call

import { verifyReceipt } from "@vdm-nexus/x402";

const result = await verifyReceipt({
  receipt,
  prompt: originalMessages,
  response: openaiResponse,
  endpoint: "https://nexus.vdmnexus.com",
});

// result: {
//   ok: boolean,
//   checks: {
//     prompt_hash_ok: boolean,
//     response_hash_ok: boolean,
//     nexus_signature_ok: boolean,
//     payment_on_chain_ok: boolean,
//     payer_matches: boolean,
//   },
// }

Pass endpoint and verifyReceipt will fetch the current Nexus operator public key from GET /api/v1/operator-key. If you've pinned the key out-of-band, pass operatorKey (base58) directly and skip the fetch.

The Solana RPC defaults to public devnet/mainnet based on receipt.payment.network; override with rpc if you have a private RPC.

Signature-only verification

When you only have the receipt JSON — for example, fetched from a public permalink at /api/v1/receipts/<id> where the original prompt and response stayed with the agent — call verifySignatureOnly instead. It runs check 3 (the operator's Ed25519 signature) and nothing else.

import { verifySignatureOnly } from "@vdm-nexus/x402";

const result = await verifySignatureOnly({
  receipt,
  endpoint: "https://nexus.vdmnexus.com",
});
// result: { ok: boolean, checks: { nexus_signature_ok: boolean } }

This is enough to prove the receipt was minted by the operator whose pubkey lives at /api/v1/operator-key. It does NOT prove the prompt/response hashes match the bodies the operator actually saw — for that, you need the original text and verifyReceipt.

The operator key

curl https://nexus.vdmnexus.com/api/v1/operator-key
# { "pubkey": "...", "algorithm": "ed25519", "encoding": "base58" }

Pin this in your verifier if you want to detect key rotation explicitly rather than implicitly trusting whatever the endpoint currently serves.

Doing it by hand

If you don't want to depend on @vdm-nexus/x402, the checks are simple enough to reproduce:

import { createHash } from "node:crypto";
import nacl from "tweetnacl";
import bs58 from "bs58";

// 1 + 2: hashes. The x402 variant hashes canonical JSON of `messages` —
// a delimited string would let user content with a `:` or `\n` collide
// with a structurally different log. (Receipts signed before 2026-05-23
// used the legacy delimited form; verifiers should accept both.)
const promptStr = canonicalize(messages);
const promptOk =
  createHash("sha256").update(promptStr).digest("hex") === receipt.prompt_hash;
const respOk =
  createHash("sha256").update(response.choices[0].message.content).digest("hex")
  === receipt.response_hash;

// 3: nexus signature (over canonical JSON with nexus_signature stripped)
const { nexus_signature, ...rest } = receipt;
const canonical = canonicalize(rest); // sorted-key JSON, no whitespace
const sigOk = nacl.sign.detached.verify(
  new TextEncoder().encode(canonical),
  bs58.decode(nexus_signature),
  bs58.decode(operatorPubkey),
);

// 4 + 5: on-chain — fetch the tx, walk pre/postTokenBalances, confirm a
// USDC transfer of receipt.payment.amount_usdc to receipt.payment.pay_to;
// confirm the first signer is receipt.agent_pubkey.

The canonical JSON rule is "sort object keys recursively, no whitespace, primitives via JSON.stringify, arrays preserve order" — the same as JCS for the subset of values we serialize.

On this page