← All skills
actionFree · MIT

Verify a Trust Card

Action skill. Given a wallet, fetch the Trust Card, verify the EIP-712 signature, recompute the canonical hash, optionally cross-check the ERC-8004 NFT on Base. Zero AR-side trust required.

View raw SKILL.mdDownloadar skills get verify-trust-card

verify-trust-card — verify any agent's identity claim

Action skill. Given a wallet address, fetch the agent's Trust Card, verify the EIP-712 signature, recompute the canonical card hash, optionally cross-check the on-chain ERC-8004 Identity NFT, and return a structured result. Zero AR-side trust required — the verifier package contains all the cryptography it needs.

Principle

If you cannot verify a claim without calling the issuer's database, the claim is not trustworthy. AR Trust Cards are designed so that any client (browser, CLI, MCP server, embedded device) can verify a card with:

  1. The card body itself.
  2. AR's public issuer address (anchored on-chain in the Identity Registry).
  3. The canonical-JSON serializer that produced the hash.

That's it. No bearer token, no API key, no "AR is up." This skill walks you through doing it correctly.

Quick start (TypeScript)

import { fetchTrustCard, verifyCard } from "@agentresources/verify";

const wallet = "0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC"; // example: AR Treasury
const card = await fetchTrustCard("https://api.agentresources.xyz", wallet);
const result = await verifyCard(card);

console.log({
  valid: result.valid, // signature recovers the claimed signer
  signerMatchesIssuer: result.signerMatchesIssuer, // signer is AR's known issuer address
  cardHashMatches: result.cardHashMatches, // canonical hash recomputes byte-for-byte
  notExpired: result.notExpired, // validUntil is in the future
  agentWallet: result.agentWallet, // recovered subject wallet
  latestAnchorTxHash: result.latestAnchorTxHash, // most recent Merkle anchor on Base
});

A card is trusted iff valid && signerMatchesIssuer && cardHashMatches && notExpired.

Quick start (Python)

from agentresources.verify import fetch_trust_card, verify_card

wallet = "0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC"
card   = fetch_trust_card("https://api.agentresources.xyz", wallet)
result = verify_card(card)

assert result.valid and result.signer_matches_issuer and result.card_hash_matches

Quick start (CLI)

npx @agentresources/mcp verify --wallet 0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC
# or, if you have ar installed:
ar verify trust-card 0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC --on-chain

What "valid" actually means

A Trust Card is a JSON-LD verifiable credential with a proof block. The verifier checks four independent things:

  1. EIP-712 signature recovery. The proof.proofValue is a 65-byte secp256k1 signature over the EIP-712 typed-data hash of (agentWallet, issuanceDate, validUntil, cardHash). We recover the signer address; it must match proof.verificationMethod.
  2. Issuer match. The recovered signer must equal AR's known issuer address (currently the operator wallet derived from ERC8004_SIGNER_KEY). Hard-coded in @agentresources/verify.
  3. Canonical hash match. We strip the proof block, run the canonical-JSON serializer (sorted keys, undefined dropped, no whitespace), keccak256 the result, and compare to credentialSubject.cardHash. Must be byte-identical.
  4. Validity window. issuanceDate <= now < validUntil.

If any one of these fails, the card is not trustworthy. Treat it like an unsigned email.

Optional: on-chain cross-check

When you set onChain: true, the verifier additionally:

  1. Reads the agent's tokenId from the Trust Card body (credentialSubject.identity.tokenId).
  2. Calls IdentityRegistry.tokenURI(tokenId) on Base mainnet (Identity Registry 0x8004A169...A432).
  3. Fetches that URI, extracts its agentWallet field, and confirms it equals the card's agentWallet.
  4. Reads the latest entry from merkle_anchors and confirms the anchor transaction is mined.

This requires a public RPC (we default to https://mainnet.base.org). If you don't have one, leave onChain: false — the off-chain cryptographic checks are already cryptographically conclusive; the on-chain check is just defense-in-depth against a hypothetical issuer-key compromise.

Common mistakes

  • Trusting valid: true alone. A signature can be valid but issued by an attacker. Always check signerMatchesIssuer too. The verifier hard-codes AR's issuer address; do not pass it in dynamically.
  • Re-canonicalising with a different JSON serializer. JSON.stringify will not work — key order is not guaranteed. Use the verifier's canonicaliseJSON (or call verifyCard directly), do not roll your own.
  • Caching card.proof.signedAt and assuming the card is still fresh. Always re-fetch when the card expires (validUntil field).
  • Verifying the card without checking notExpired. A genuinely-signed card from 2025 is still cryptographically valid; it's just expired. Reject expired cards in production.
  • Calling fetchTrustCard with a wallet that is not lowercased. The endpoint normalises but some intermediate caches don't. Always pass wallet.toLowerCase().

Error contract

Outcome What it means What to do
404 from fetchTrustCard No card has been issued for this wallet Treat the agent as unverified
valid: false Signature does not recover anyone — body was modified Reject
signerMatchesIssuer: false Card is signed by someone other than AR Reject; possible forgery
cardHashMatches: false Body has been tampered with after signing Reject
notExpired: false Card has expired (validUntil past) Re-fetch; if 404 the agent is no longer active
onChainMatches: false tokenURI on-chain does not match card body Reject; possible chain compromise

Quality gates

This skill is "loaded correctly" iff the runtime can:

  • Verify the AR Treasury Trust Card (0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC) end-to-end.
  • Detect a tampered body (mutate one byte; verifier must return cardHashMatches: false).
  • Detect a forged signer (replace proof.proofValue with random 65 bytes; verifier must return valid: false).
  • Reject an expired card.
  • Optionally complete the on-chain cross-check when given a Base RPC.

Any runtime that cannot do these four things has not actually loaded the skill.

Self-test snippet

Drop this into the runtime to confirm load:

import { fetchTrustCard, verifyCard } from "@agentresources/verify";

const wallet = "0x4da73F7B725abC5565ce87DE3d51CFFBb71D59aC";
const card = await fetchTrustCard("https://api.agentresources.xyz", wallet);
const r = await verifyCard(card);

console.assert(r.valid, "Treasury card signature should recover");
console.assert(r.signerMatchesIssuer, "Signer should be AR issuer");
console.assert(r.cardHashMatches, "Canonical hash should match");
console.assert(r.notExpired, "Treasury card should be in-window");
console.log("✓ verify-trust-card skill loaded");

Where to read more

  • Documentation/06-Specifications/TRUST_CARD_PROTOCOL.md — the canonical protocol spec.
  • packages/verify/src/index.ts — the entire verifier in ~200 lines of TypeScript.
  • packages/verify/README.md — usage and the canonicalisation algorithm.
  • agent-resources orientation skill — context on the wider trust surface.

License

MIT. The verifier package is also MIT. Fork it; we want as many independent verifiers in the wild as possible.