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.
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.
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
const promptStr = messages.map((m) => `${m.role}:${m.content}`).join("\n");
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.