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:
- Prompt hash matches.
sha256(role:content joined by \n)over the messages equalsreceipt.prompt_hash. - Response hash matches.
sha256(response_text)equalsreceipt.response_hash. - Nexus signature is valid.
receipt.nexus_signatureis an Ed25519 signature, by the Nexus operator key, over the canonical JSON of the receipt (withnexus_signaturestripped). - Payment exists on Solana.
receipt.payment.tx_signatureis a confirmed transaction with a USDC transfer ofreceipt.payment.amount_usdctoreceipt.payment.pay_to. - 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.