Yesterday I wrote about 8 security layers for my MCP marketplace. Today I'm shipping the 9th: Agent Trust Cards (ATC) — and this time the cryptography is real, not a mock.
What is ATC?
ATC = SSL certificates for AI agents. MarketNow acts as the Certificate Authority (CA). Every agent gets a cryptographically signed trust card that other agents can verify.
Agent A wants to call Agent B's tools
→ Agent A fetches Agent B's ATC: GET /api/atc?action=verify&card_id=X
→ Verifies the Ed25519 signature with the CA public key
→ Checks: not expired, not revoked, sentinel_score > threshold
→ If valid: trusts Agent B and proceeds
→ If invalid: refuses to interact
This is exactly how SSL works for websites, but for agents.
What's real now (vs what was mock before)
| Component | Before (mock) | Now (real) |
|---|---|---|
| CA keypair | 'marketnow-ca-public-key-placeholder' |
Ed25519 generated with crypto.generateKeyPairSync
|
| Signature |
'signed_by_marketnow_ca' (literal string) |
crypto.sign(null, data, privateKey) → 128 hex chars |
| Verify | Always returned valid: true
|
Fetches record from GitHub, verifies signature with crypto.verify, checks expiry + revocation |
| Revoke | Returned "revoked" but didn't persist | Updates the persisted record; future verify returns valid: false, reason: 'revoked'
|
| Persistence | None (1 hardcoded card) | GitHub Contents API → _data/atc/{card_id}.json (public ledger) |
| Sentinel score | Hardcoded 10
|
Fetched from real Sentinel certificate in _data/sentinel_certificates/
|
How it works
1. The CA keypair
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
// Private key → Vercel env var MARKETNOW_ATC_CA_PRIVATE_KEY
// Public key → committed to _data/atc/ca-public-key.json
Anyone can fetch the CA public key:
GET https://marketnow.site/api/atc?action=ca-key
2. Issuing an ATC
POST https://marketnow.site/api/atc
Content-Type: application/json
{
"action": "issue",
"agent_id": "agent.example.myagent",
"public_key": "<Ed25519 public key>",
"capabilities": ["search", "recommend"],
"protocol_language": "mcp",
"skill_id": "mn-mcp-filesystem"
}
Response:
{
"status": "issued",
"card_id": "ATC-2026-1115271",
"agent_id": "agent.example.myagent",
"trust": {
"sentinel_score": 4,
"risk_level": "high",
"certificate_id": "MN-SC-2026-8393995"
},
"signature": {
"algorithm": "Ed25519 (RFC 8032)",
"value": "bc79ee606339460a382f3ecc324c629e...",
"signed_by": "MarketNow Sentinel CA"
}
}
The signature is over the canonical JSON of the payload:
JSON.stringify(payload, Object.keys(payload).sort())
3. Verifying (independently)
Anyone can verify an ATC without trusting MarketNow's API:
import crypto from 'crypto';
// 1. Fetch the CA public key
const caKeyRes = await fetch('https://marketnow.site/api/atc?action=ca-key');
const { public_key_pem } = await caKeyRes.json();
const publicKey = crypto.createPublicKey(public_key_pem);
// 2. Fetch the ATC record
const atcRes = await fetch('https://marketnow.site/api/atc?action=verify&card_id=ATC-2026-1115271');
const atc = await atcRes.json();
// 3. Verify the signature yourself
const data = Buffer.from(JSON.stringify(atc.payload, Object.keys(atc.payload).sort()));
const signature = Buffer.from(atc.signature.value, 'hex');
const valid = crypto.verify(null, data, publicKey, signature);
// → true
4. Revoking
POST https://marketnow.site/api/atc
{
"action": "revoke",
"card_id": "ATC-2026-1115271",
"reason": "Key compromised"
}
The persisted record is updated: status: 'revoked'. Future verify calls return:
{
"valid": false,
"reason": "revoked",
"revoked_at": "2026-07-16T02:58:57.604Z",
"revocation_reason": "Key compromised"
}
5. Framework translation (bonus)
ATC also translates between agent frameworks:
POST https://marketnow.site/api/atc
{
"action": "translate",
"from": "langchain",
"to": "mcp",
"message": {
"name": "search",
"description": "Search skills",
"args_schema": { "type": "object", "properties": { "q": {"type":"string"} } }
}
}
Returns the MCP-formatted tool schema (inputSchema instead of args_schema). Real schema mapping, not passthrough.
The public ledger
Every ATC is persisted to _data/atc/ in the public GitHub repo. Anyone can:
- Audit the full ledger:
GET /api/atc - Verify any card:
GET /api/atc?action=verify&card_id=X - Check the CA key:
GET /api/atc?action=ca-key - Clone the repo and verify signatures offline
No database, no proprietary backend. Just signed JSON files in a git repo. That's the transparency layer.
Why this matters
Agents are starting to call each other autonomously. A2A (Google) and MCP (Anthropic) handle the communication. But nobody handles trust.
Without trust:
- Agent A can't know if Agent B is malicious
- Agent A can't know if Agent B's tools have been audited
- Agent A can't know if Agent B has been revoked (key compromised, ToS violation, etc.)
ATC fills that gap. It's the SSL layer for agent commerce.
Try it
# Get the CA public key
curl https://marketnow.site/api/atc?action=ca-key
# Issue a trust card for your agent
curl -X POST https://marketnow.site/api/atc \
-H "Content-Type: application/json" \
-d '{"action":"issue","agent_id":"your.agent","public_key":"your-ed25519-pubkey"}'
# Verify it
curl "https://marketnow.site/api/atc?action=verify&card_id=YOUR_CARD_ID"
— Edison Flores, AliceLabs LLC — marketnow.site
Top comments (2)
The canonicalization line has a coverage bug:
I ran this in Node against an Ed25519 keypair. The signing input came out as:
Then I changed only nested fields, sentinel_score 4 -> 10 and risk_level "high" -> "low". The recomputed signing input was byte-identical and crypto.verify returned true. As a control, changing agent_id made verify return false, so the top level is genuinely protected.
The cause is subtle. The second parameter of JSON.stringify is a replacer allowlist, not a key sorter. Passing Object.keys(payload).sort() does order the top-level output, which is why it looks like canonicalization and why top-level tampering gets caught. That same allowlist is then applied recursively at every nesting depth. A nested key survives only if its name also appears in the top-level list. sentinel_score and risk_level exist only inside trust, so they get filtered out before signing, and what actually gets signed is trust:{}. The whole trust block sits outside the signature.
How much that costs depends on where verify reads the score from. If it re-fetches from _data/sentinel_certificates/ the blast radius is smaller. If it reads trust.sentinel_score off the card, then the one number the policy gates on is the one number the signature does not cover, and a mirrored copy of the JSON with a raised score still verifies against the real CA key.
Arrays of scalars like capabilities are unaffected, since the replacer does not filter array elements. This is specifically nested object key loss.
The fix is small. Use a canonical form that recursively sorts keys (JCS / RFC 8785), or keep the signed payload flat so the allowlist can never drop a policy field. Either way this one fails open, so a test that only asserts "a good signature verifies" will never catch it. Sign a card, mutate trust.sentinel_score, assert verify returns false.
The revocation persistence via GitHub Contents API is a clever choice — using a public ledger that anyone can audit. Have you considered what happens if the CA key itself is compromised? Would love to see a follow-up on key rotation and multi-sig for high-value agents.