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:
- The card body itself.
- AR's public issuer address (anchored on-chain in the Identity Registry).
- 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:
- EIP-712 signature recovery. The
proof.proofValueis a 65-byte secp256k1 signature over the EIP-712 typed-data hash of(agentWallet, issuanceDate, validUntil, cardHash). We recover the signer address; it must matchproof.verificationMethod. - 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. - Canonical hash match. We strip the
proofblock, run the canonical-JSON serializer (sorted keys, undefined dropped, no whitespace), keccak256 the result, and compare tocredentialSubject.cardHash. Must be byte-identical. - 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:
- Reads the agent's
tokenIdfrom the Trust Card body (credentialSubject.identity.tokenId). - Calls
IdentityRegistry.tokenURI(tokenId)on Base mainnet (Identity Registry0x8004A169...A432). - Fetches that URI, extracts its
agentWalletfield, and confirms it equals the card'sagentWallet. - Reads the latest entry from
merkle_anchorsand 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: truealone. A signature can be valid but issued by an attacker. Always checksignerMatchesIssuertoo. The verifier hard-codes AR's issuer address; do not pass it in dynamically. - Re-canonicalising with a different JSON serializer.
JSON.stringifywill not work — key order is not guaranteed. Use the verifier'scanonicaliseJSON(or callverifyCarddirectly), do not roll your own. - Caching
card.proof.signedAtand assuming the card is still fresh. Always re-fetch when the card expires (validUntilfield). - 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
fetchTrustCardwith awalletthat is not lowercased. The endpoint normalises but some intermediate caches don't. Always passwallet.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.proofValuewith random 65 bytes; verifier must returnvalid: 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-resourcesorientation 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.