@trustbench/verify-receipt
v0.1.2
Published
Standalone third-party verifier for TrustBench Ed25519-signed receipts. No TrustBench API calls in the verify path beyond fetching the published public key.
Maintainers
Readme
@trustbench/verify-receipt
Standalone third-party verifier for TrustBench Ed25519-signed receipts.
No TrustBench API calls in the verify path beyond fetching the published public key. Verifies offline once the public key is cached. Optional on-chain verification via viem peer dependency.
Install
npm install @trustbench/verify-receipt
# Optional, for on-chain verification:
npm install viemSupported receipt prefixes
Two prefixes are accepted; both route to the same /receipts/:id endpoint:
| Prefix | Issued by | Envelope shape |
|---|---|---|
| rcpt_… | Phase 3 settlement receipts | receipt.settlement.{chain, tx_hash, block_number, payer_address, payee_address, amount_atomic, …} |
| rrcpt_… | Phase 4 paywall routing receipts | receipt.paid.{chain, tx_hash, payer_address, payee_address, amount_atomic, …} (no block_number; verifier confirms the block via the tx_hash lookup) |
The verifier handles both shapes transparently. Code that already worked with rcpt_ requires no changes.
Usage (programmatic)
import { verifyReceipt } from '@trustbench/verify-receipt';
// By receipt id (Phase 3 or Phase 4)
const result = await verifyReceipt('rcpt_01KQY7C44GAPSXZPFQYRZ1D10C');
const result2 = await verifyReceipt('rrcpt_01KRN8HYPPRD1MS9JE7045S77Q');
console.log(result.signatureValid); // true | false
console.log(result.ok); // signature valid + chain verified (if checkChain)
// From an already-fetched envelope
const result = await verifyReceipt(envelope);
// From a full URL
const result = await verifyReceipt('https://trustbench.io/receipts/rrcpt_...');
// With on-chain verification
const result = await verifyReceipt('rrcpt_...', { checkChain: true });
if (result.chain && result.chain.ok) {
console.log('Block:', result.chain.block_number);
console.log('Payer:', result.chain.payer);
console.log('Amount:', result.chain.amount, '(atomic USDC)');
}Usage (CLI)
# Signature-only verification
npx trustbench-verify-receipt rcpt_01KQY7C44GAPSXZPFQYRZ1D10C
npx trustbench-verify-receipt rrcpt_01KRN8HYPPRD1MS9JE7045S77Q
# Signature + on-chain (requires viem)
npx trustbench-verify-receipt rrcpt_01KRN8HYPPRD1MS9JE7045S77Q --check-chain
# From a local JSON file
npx trustbench-verify-receipt ./my-receipt.json
# Override the public key URL (useful for local-dev verification)
npx trustbench-verify-receipt ./my-receipt.json --pubkey-url http://localhost:3000/.well-known/trustbench-pubkeyExit codes: 0 valid, 1 bad args, 2 signature invalid (tamper signal), 3 on-chain mismatch, 4 chain check error, 5 verification unavailable (couldn't fetch the receipt or public key — connectivity, not tampering).
What gets verified
The signature step:
- JCS-canonicalize the
receiptobject (RFC 8785-style: sorted keys at every depth, JSON.stringify for primitives, no whitespace). - Encode to UTF-8 bytes.
- Verify the Ed25519 signature against those bytes using the public key fetched from
signature.public_key_url.
The optional on-chain step (when --check-chain is used):
- Fetch the transaction by
settlement.tx_hashfrom a Base RPC. - Confirm
tx.tois the USDC contract (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913). - Decode the calldata as
transferWithAuthorization(from, to, value, ...). - Confirm
from,to, andvaluematch the receipt'spayer_address,payee_address, andamount_atomic. - Confirm the tx was mined successfully and (if present in the receipt) at the claimed
block_number.
A signature alone tells you "TrustBench claims this happened with these parameters." --check-chain tells you "the chain agrees with TrustBench's claim." Both together is the strongest assurance an external party can get without trusting either side.
API
verifyReceipt(input, options?)
| Param | Type | |
|---|---|---|
| input | Object \| string | Receipt envelope, id (rcpt_...), full URL, or .json path |
| options.baseUrl | string | Override base URL for id-based input. Default https://trustbench.io |
| options.pubkeyUrl | string | Override the public_key_url from the envelope |
| options.checkChain | boolean | Also verify on-chain settlement (requires viem) |
| options.rpcUrl | string | RPC URL for chain check. Default https://mainnet.base.org |
Returns Promise<VerifyResult> — see index.d.ts for the full shape.
verifyOnChain(envelope, rpcUrl?)
Lower-level helper. Same chain check as verifyReceipt({ checkChain: true }) but skips the signature step. Returns { ok, reason?, chain?, tx_hash?, block_number?, payer?, payee?, amount? }.
jcsCanonicalize(obj)
The exact canonicalization function used internally. Useful for callers that want to reconstruct the bytes that were signed.
Compatibility
- Node.js >= 18 (for built-in
fetch). viem >= 2.0.0peer dependency, optional, only needed for--check-chain.- Mirrors the in-repo reference verifier (
scripts/verify-receipt.js) byte-for-byte for the JCS + Ed25519 logic. If they disagree, that's a bug — please open an issue.
Changelog
0.1.2
- Added
verificationStatus: 'valid' | 'invalid' | 'unavailable'on theVerifyResult. Previously, a fetch failure for the receipt URL or the public key URL was indistinguishable from a tampered signature in the headline output. CLI now prints⚠️ VERIFICATION UNAVAILABLE(exit code5) when the verifier could not reach the URLs needed to do the math, vs.❌ SIGNATURE INVALID(exit code2) only when bytes were checked and do not match. CI policies that retry on flakiness but alert on tamper should branch on this distinction. - The
signatureValidboolean and existing exit codes 0-4 are preserved. v0.1.0/v0.1.1 callers usingresult.signatureValidorresult.okcontinue working unchanged. - Refreshed example IDs in the README and CLI HELP block to point at the post-Strata-PR-24 reference receipt (
rrcpt_01KRN8HYPPRD1MS9JE7045S77Q). - No change to the JCS canonicalization or Ed25519 verify logic. If v0.1.1 said VALID, v0.1.2 says VALID.
0.1.1
- Recognize the
rrcpt_prefix in addition torcpt_. Both route to the same/receipts/:idendpoint on the issuer host. - Verifier reads on-chain settlement data from either
receipt.settlement(Phase 3) orreceipt.paid(Phase 4 paywall routing receipts). Whenblock_numberis absent on the paywall envelope, the verifier still confirms the on-chain transaction via thetx_hashlookup and reportsblock_check: "block_number not in receipt"in the chain result. - No breaking changes. v0.1.0 callers continue working unchanged.
0.1.0
- Initial release. Ed25519 signature verification over RFC 8785 JCS-canonical bytes, optional on-chain verification via
viem.
License
MIT. See LICENSE.
Links
- TrustBench — public registry + non-custodial router
- Methodology — what the probe measures
- Receipt spec v1.0.0
- Public key
- GitHub
