Home/Tutorials/Verify receipts
Tutorial 05 Advanced ~12 min

Verify R+2 receipts independently — the compliance-audit workflow

By the end of this tutorial you'll have run the full §8 verification flow on real R+2 receipts, walked a chain back to its genesis, deliberately tampered with a receipt to see verification fail, and understood exactly what trust assumptions you're (and aren't) making.

PREREQUISITES Completed Tutorials 01-04. You should have at least 3 receipts in your agent's chain. Node 18+ installed for npx.

Step 01 Install the verifier

Two options:

# Option A: Run with npx (no install — recommended for one-off audits)
npx @trdnetwork/r2-verify --version

# Option B: Global install (best for repeated use)
npm install -g @trdnetwork/r2-verify
r2-verify --version

You should see r2-verify v0.1.1 (R+2 spec r2/v0.1).

Step 02 Fetch a receipt to verify

R+2 receipts are public — no authentication needed. Fetch one:

curl https://api.dcslabs.ai/v1/receipts/r2_a83f12cd > receipt.json
cat receipt.json

You'll see the full signed receipt JSON: spec_version, agent_pubkey, agent_id, action_id, action_type, action_data, occurred_at, prev_receipt_cid, nonce, extensions, signature. Eleven fields per §4 of the spec.

Step 03 Get the expected public key

To verify a signature you need the expected public key — fetched from the identity layer (NOT from the receipt itself, which is what makes verification trustless):

curl https://api.dcslabs.ai/v1/agents/0348 | jq .pubkey
# OR — for fully trustless: read directly from the on-chain SBT contract
cast call 0xbDd1f5fC349D9a8EfCEb07Edbd491233b2540f5F \
  "agentPubkey(string)" "0348" \
  --rpc-url https://mainnet.base.org

Save the public key:

export AGENT_PUBKEY="u4yK_lH8Z6vJ3qZ5tNwQpRz_aBcDeFgH1iJ2kL3mN4o"
TRUST MODEL You can either trust DCS's API to return the right pubkey (acceptable for most cases) or trust only the on-chain SBT contract (the maximally paranoid path — only Coinbase's L2 and Ethereum mainnet are in your trust set). For regulator-grade audits, prefer the on-chain path.

Step 04 Run verification

npx @trdnetwork/r2-verify --receipt receipt.json --pubkey $AGENT_PUBKEY

Expected output:

✓ Schema
✓ Spec version  r2/v0.1
✓ Pubkey match  u4yK_lH8Z6vJ...
✓ Signature
✓ Chain pointer  first receipt (null)
✓ Timestamp     within ±24h window

Receipt verified.

What just happened (per §8 of the spec):

  1. Schema check. All 11 required fields present, correct types, correct lengths.
  2. Spec version. Receipt claims r2/v0.1 — supported.
  3. Pubkey match. Receipt's agent_pubkey field equals the pubkey from the identity layer.
  4. Signature. The signature was removed from the receipt, the remainder canonicalized per RFC 8785, and Ed25519 verified against the pubkey.
  5. Chain pointer. prev_receipt_cid is null (this is the genesis receipt). If non-null, we'd verify it matches the actual previous receipt's CID.
  6. Timestamp. Within ±24h of verification time. Warning only, not failure.

Step 05 Walk the chain

Now verify the chain pointer by fetching the previous receipt:

curl https://api.dcslabs.ai/v1/receipts/r2_b91d34ef > receipt-prev.json
curl https://api.dcslabs.ai/v1/receipts/r2_a83f12cd > receipt-curr.json

npx @trdnetwork/r2-verify \
  --receipt receipt-curr.json \
  --previous receipt-prev.json \
  --pubkey $AGENT_PUBKEY

Now the Chain pointer check actively validates: it computes the SHA-256 hash of the previous receipt's canonical form and confirms it matches the current receipt's prev_receipt_cid. Any tampering anywhere in the chain breaks this check.

Step 06 Deliberately break a receipt to see failure

Make a copy and tamper with it:

cp receipt.json receipt-tampered.json

# Edit receipt-tampered.json — change the action_data content to something fake
# e.g., add a property "additional_info": "I AM FAKE"

npx @trdnetwork/r2-verify --receipt receipt-tampered.json --pubkey $AGENT_PUBKEY

Expected output:

✓ Schema
✓ Spec version  r2/v0.1
✓ Pubkey match
✗ Signature: Ed25519 verification failed (signature does not match canonical bytes)

Receipt invalid: Signature: Ed25519 verification failed

Even a single-character change in the action_data invalidates the signature. This is what makes the system trustworthy: any modification is immediately detectable, and the agent can't claim anything they didn't sign.

Step 07 Verify a full chain end-to-end

For a regulator audit, you want to verify every receipt in the agent's chain. Walk the chain backwards from head to genesis:

#!/bin/bash
# walk-chain.sh — verify every receipt in an agent's chain

AGENT_ID="0348"
PUBKEY=$(curl -s https://api.dcslabs.ai/v1/agents/$AGENT_ID | jq -r .pubkey)

# Get the full chain (paginated for large agents)
curl "https://api.dcslabs.ai/v1/receipts/chain?agent_id=$AGENT_ID&limit=1000" \
  -H "Authorization: Bearer $DCS_API_KEY" > chain.json

# Verify each receipt + chain pointer
jq -c '.receipts[]' chain.json | while read receipt; do
  echo "$receipt" | npx @trdnetwork/r2-verify --pubkey $PUBKEY || break
done

echo "Chain verified end-to-end."

If any receipt fails verification, the script exits and you see which one broke. Otherwise: full chain integrity proven.

Step 08 Audit a receipt without an internet connection (air-gapped)

Sovereign deployments often run in air-gapped environments. r2-verify works offline as long as you have the receipt JSON and the public key:

# On the air-gapped machine — assume r2-verify pre-installed
# Read receipt from a USB stick or printout

r2-verify --receipt /mnt/usb/receipt.json --pubkey $PRINTED_PUBKEY

No network call. No DCS dependency. Pure local cryptography. This is the deployment story for defence, classified data, regulators with strict data-residency requirements.

What you're trusting (and what you aren't)

The R+2 design minimizes the trust surface. After running this tutorial, here's exactly what you trust to verify a receipt:

What you do NOT have to trust:

This is what "trust-minimized" means in practice. Compare to typical SaaS: you trust the vendor for everything, end of story.

Programmatic verification (library use)

If you're embedding verification into your own application, use the library directly:

import { verifyReceipt, computeReceiptCid } from '@trdnetwork/r2-verify';

const result = await verifyReceipt(receipt, expectedPubkey, {
  previousReceipt: prevReceipt,
  skipTimestampCheck: false
});

if (result.ok) {
  console.log("Receipt verified, safe to trust");
} else {
  console.error("Receipt rejected:", result.error);
}

What's next