Skip to content

Add ENSIP: Agent Card Schema (/.well-known/agent.json) - #75

Open
TMerlini wants to merge 7 commits into
ensdomains:masterfrom
TMerlini:ensip-27-agent-card-schema
Open

TMerlini wants to merge 7 commits into
ensdomains:masterfrom
TMerlini:ensip-27-agent-card-schema

Conversation

@TMerlini

Copy link
Copy Markdown

Summary

Completes the agent discovery chain defined in ENSIP-26.

ENSIP-26 defines how to find an agent via ENS text records (agent-endpoint[mcp], agent-endpoint[a2a]). This ENSIP defines what the agent card document at /.well-known/agent.json contains.

  • Standardises the /.well-known/agent.json schema (required + optional fields)
  • Adds erc8004 identity anchor — bridges ENSIP-25 (verification) + ENSIP-26 (discovery) + ERC-8004 (on-chain identity)
  • Adds sanitizationSpec for input provenance (WYRIWE)
  • Adds trustScope for A2A depth limiting
  • Security Considerations: gateway trust, HTTPS requirement, provenance verification

Reference implementation

Live at gateway.ensub.org as of 2026-05-19:

GET https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/.well-known/agent.json

ERC-8004 factory (verified on Etherscan): 0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f
Three live collection registries on Ethereum mainnet.

Relationship to existing ENSIPs

ENSIP Role
ENSIP-25 Verifies ENS name ↔ agent registry association
ENSIP-26 Discovers agent via agent-endpoint[*] text records
ENSIP-27 Defines what the agent card document contains

ENSIP-26 explicitly states: "Future ENSIPs may define more specific formats." This is that future.

— dinamic.eth

Defines the schema for the agent card document that completes the
discovery chain established in ENSIP-26.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move related spec links inline into Rationale section.
The CI linter expects Security Considerations → Copyright with no intervening headings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nxt3d

nxt3d commented May 19, 2026

Copy link
Copy Markdown
Contributor

Could the well-known endpoint be on an ENS name itself using the contenthash record?

I think the question is how we have experimentation around new agent protocols, while at the same time not reserving a bunch of keywords that might conflict.

Can you give your protocol a more unique name? For example, endpoint[ens-acs].

Also, if you can open a thread on the ENS forum, it can be helpful to get better visibility on your ENSIP.

@TMerlini

Copy link
Copy Markdown
Author

Could the well-known endpoint be on an ENS name itself using the contenthash record?

I think the question is how we have experimentation around new agent protocols, while at the same time not reserving a bunch of keywords that might conflict.

Can you give your protocol a more unique name? For example, endpoint[ens-acs].

Also, if you can open a thread on the ENS forum, it can be helpful to get better visibility on your ENSIP.

On the contenthash question: good direction for static agent cards, but our implementation relies on dynamic content, the card includes EIP-712 signed attestations and live endpoint URLs that change per execution. Contenthash (IPFS) is immutable once set; we use CCIP-Read precisely so the card can be served dynamically and signed by the gateway on each fetch. That said, a static variant pointing to a pinned card would be a valid subset of the same schema, the two aren't mutually exclusive.
On namespace: fair point, card is too generic. Renamed to agent-endpoint[ens-acs] (ENS Agent Card Schema) in both PRs.
Opening one on discuss.ens.domains now.

@jmacwhyte

Copy link
Copy Markdown
Contributor

I would also recommend taking a look at #64, an in-progress ENSIP for attaching schemas for any type of entity (and especially agents). Here is an explanation of how it would work to attach an agent's metadata (including links to ERC-8004 and all other details that could be in an agent manifest) directly to their ENS name: 0xLighthouse/ens-metadata#46

@TMerlini

TMerlini commented May 20, 2026

Copy link
Copy Markdown
Author

Thanks @jmacwhyte — this is directly relevant and I should have caught #64 earlier.

Reading through the Agent use case in 0xLighthouse/ens-metadata#46, there's significant overlap in intent and some differences in approach worth aligning on.

Where we converge:

  • Both use parameterized text record keys for service endpoints
  • Both reference ERC-8004 as the on-chain agent identity anchor
  • Both treat the ENS name as the canonical discovery point for the agent

Where we differ:

Concern ENSIP-64 / ens-metadata ENSIP-27
Service endpoint keys services[mcp], services[a2a] agent-endpoint[mcp], agent-endpoint[a2a]
Node classification class = "Agent" + schema = "ipfs://..." implicit via /.well-known/agent.json
Cross-registry refs registrations[0] (CAIP-19) not specified
Resolution Static text records CCIP-Read (EIP-3668) — dynamic, off-chain computed

The key naming divergence (services[*] vs agent-endpoint[*]) is the most important one to resolve — clients can't support both without knowing which to look for.

On CCIP-Read: ENSIP-27's primary use case is CCIP-Read resolution — the agent's ENS name resolves dynamically via an off-chain gateway, which enables private endpoints, runtime-computed metadata, and records that don't require on-chain writes. This is complementary to the static text record path rather than competing with it. An agent could publish stable fields as static records and serve dynamic ones via CCIP-Read.

The registrations[*] CAIP-19 pattern is a genuine gap in ENSIP-27 — cross-chain registry references should be in scope. Already shipped this in the reference implementation: pixel-goblins.dinamic.eth now returns registrations[0] = eip155:1/erc721:0xe61f.../5 via CCIP-Read, verifiable with any ENS resolver.

The key naming (services[*] vs agent-endpoint[*]) and node classification (class = "Agent") are the remaining open questions — happy to align once we agree on the right convention. No point shipping a rename twice.

Would it make sense to treat ENSIP-64 as the classification and metadata layer, and ENSIP-27 as the agent-card content spec that sits on top of it? The /.well-known/agent.json format and the CCIP-Read serving mechanism feel like a different layer from node classification.

Open to that conversation here or wherever makes sense to the #64 working group.

@jmacwhyte

Copy link
Copy Markdown
Contributor

I have responded to your comment here: https://discuss.ens.domains/t/ensip-27-agent-card-schema-well-known-agent-json/22130/4

@babyblueviper1

Copy link
Copy Markdown

Reading this as someone who serves /.well-known/agent.json in production and co-authored the WYRIWE ERC (ERC-8299, ethereum/ERCs#1810) whose input-provenance commitment sanitizationSpec cites — three concrete notes, one of which is an interop risk worth settling before this freezes.

1. Same path, near-identical shape, silently different semantics vs the A2A agent card.

/.well-known/agent.json is already the serving path for A2A protocol agent cards, and ENSIP-27's required fields (name, provider{organization,url}, version, capabilities{streaming,pushNotifications,stateTransitionHistory}, skills[{id,name,tags,examples}]) are byte-compatible with that schema — which is good. But two fields diverge silently:

  • In A2A, top-level url is the A2A JSON-RPC endpoint. Here it's defined as the MCP endpoint. Same key, same type, different transport on the other end.
  • A2A discriminates via protocolVersion (and optionally $schema); ENSIP-27 via schema_version. A client fetching an arbitrary agent.json can't tell which document class it holds without checking for both.

The failure mode is the bad kind: everything parses, then the client speaks JSON-RPC to an MCP endpoint (or vice versa) and the error surfaces two layers away from its cause. Two cheap fixes, either works:

  • (a) Keep the divergence but make it detectable: state in the spec that schema_version is the ENSIP-27 discriminator, that documents carrying protocolVersion are A2A cards, and that a conforming client MUST check before reading url.
  • (b) Align: keep A2A's url semantics and carry transports in an array. We hit exactly this one-card-many-transports problem and ship supported_interfaces: [{protocol: "a2a"|"mcp"|"rest", url, protocol_binding, protocol_version}] — live example: https://api.babyblueviper.com/.well-known/agent.json. One document, no key collision, MCP and A2A both first-class.

2. sanitizationSpec — the identity-sentinel MUST is the load-bearing half; offer of conformance vectors.

The design decision that matters here is the one already made: when there is no sanitization, the value MUST be the identity-sentinel URI rather than the field being absent. Absence claims nothing; identity claims identity — that asymmetry is what makes the commitment verifiable rather than vibes. As the 8299 side of this: happy to contribute a small conformance set for the "clients MUST fetch and verify" path — a valid pipeline spec, the identity sentinel, and a tampered pipeline that must fail — so independent implementations converge on bytes, not prose. Say the word and I'll PR them wherever fits (assets here, or a companion repo).

3. The card anchors who (erc8004) and what went in (sanitizationSpec); consider an optional pointer for checking what comes out.

Right now a card can prove the agent's identity and its input handling, but a client acting on the agent's output still has nothing to check. An optional trustEndpoints object closes that: {"verifyProof": url, "independentNodes": [urls], "selfHostVerifier": pkg} — where a client can independently verify signed claims the agent attaches to its output, without trusting the gateway or the agent. Prior art: we've served exactly this block in our live card since June (same URL as above), and third parties do hit the verify endpoint. Optional, zero cost when absent, and it makes the discovery chain end at something checkable instead of something asserted.

None of this blocks the ENSIP as written — (1) is the only one I'd call a must-settle, and fix (a) is a two-sentence edit. Good design overall; the ENSIP-25/26/27 + ERC-8004 chain is the right decomposition.

… trustEndpoints; add babyblueviper1 as co-author

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TMerlini

TMerlini commented Jul 1, 2026

Copy link
Copy Markdown
Author

Updated this PR with a substantial revision, co-authored with @babyblueviper1, whose review (above) was spec-level, not feedback, and folded in directly:

A2A interoperability, made normative. /.well-known/agent.json is shared with A2A by design (the required fields are byte-compatible). The revision adds an explicit discrimination rule: schema_version is the ENSIP-27 discriminator, and a conforming client MUST check it before dereferencing url, which here is the MCP endpoint, vs A2A’s JSON-RPC. This closes the silent-protocol-mismatch failure mode.
supported_interfaces (optional), an explicit multi-transport array so a card can carry MCP + A2A + REST without the top-level url key collision. Prior art: @babyblueviper1’s production card.
trustEndpoints (optional), {verifyProof, independentNodes, selfHostVerifier}, so the discovery chain ends at something a client can independently recompute rather than something the gateway asserts. erc8004 anchors who the agent is, sanitizationSpec commits what went in, trustEndpoints lets you check what comes out.
sanitizationSpec sharpened, absence ≠ the identity sentinel; the sentinel is a positive, verifiable claim. Cross-linked to ERC-8299 (WYRIWE), where the input-provenance commitment is being standardised on the ERC track.
Conformance vectors, @babyblueviper1 is contributing a reference set (valid pipeline / identity sentinel / tampered-must-fail) so independent implementations converge on bytes, not prose. Happy to land them wherever the editors prefer (assets here or a companion repo).
On the number: I understand “27” may be going to the node-metadata standard, no attachment here, happy to take whatever number the editors assign. The goal is a correct, interoperable schema for verifiable agent discovery; the slot is the editors’ call.

Grateful for the earlier input from @jmacwhyte and @nxt3d too, the ENSIP-25/26 + ERC-8004 decomposition this composes into is the right one.

…c) — fix ensip linter

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@babyblueviper1

Copy link
Copy Markdown

Yes to co-authoring — thank you, @TMerlini. Recomputed 272fc77 against what I raised rather than take the commit message's word for it; all three landed as normative spec, not a courtesy line:

  • A2A discriminatorschema_version is the discriminator, a client MUST confirm document class before dereferencing url, and the MCP-vs-JSON-RPC url divergence is spelled out. That's the must-settle made normative, which is what it needed to be.
  • supported_interfaces — the one-card-many-transports array is in, with the live card credited as prior art.
  • trustEndpoints{verifyProof, independentNodes, selfHostVerifier} closes the output-verification leg so the discovery chain ends at something recomputable. Using your gateway's endpoints in the example is the right call (they're yours to attest to; the field is generic).

And I'm listed under contributors. Good — accurate.

Delivering the conformance vectors I offered, so the sanitizationSpec MUST converges on bytes instead of prose:

https://github.com/babyblueviper1/ensip27-sanitizationspec-vectors

Three vectors, exactly the set the spec now names:

  • valid_pipelinepasses (recomputed CID == claimed)
  • identity_sentinelpasses — encoding the asymmetry you kept: the sentinel is a positive, verifiable "no transformation" claim, distinct from absence
  • tamperedmust fail — bytes altered under the original CID, so recompute rejects it

It's CIDv1-raw + sha2-256 on purpose: the CID is a pure sha256 of the exact bytes, so the checker is stdlib-only (no IPFS node, no network) and anyone re-derives every CID from the files. CI recomputes all three on every push. Wire it wherever the editors want it — vendored into assets/ here, or referenced as a companion repo; I'll follow the number reassignment either way.

Between your production gateway, the 8299 side, and a byte-level conformance set, this is the reference for verifiable agent discovery — the better outcome regardless of which ENSIP number it lands under. Let's build.

…ec (valid/sentinel/tampered, CIDv1-raw+sha256)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jmacwhyte

Copy link
Copy Markdown
Contributor

Some outstanding questions:

  1. The A2A agent card endpoint is .well-known/agent-card.json. You've used .well-known/agent.json which won't be discoverable by A2A. Is that intentional?

  2. If this spec is about agent cards, which is an AI concept, what is the relation to ENS? Why does this need to be an ENSIP?

@babyblueviper1

Copy link
Copy Markdown

On (1): checked rather than assumed — you're right, and thanks for catching it. A2A's well-known path moved from /.well-known/agent.json (spec v0.2.5) to /.well-known/agent-card.json (v0.3.0), per IANA feedback: https://a2a-protocol.org/v0.3.0/specification/. agent.json is the old A2A path, not current.

Concretely: our own production agent (api.babyblueviper.com) serves an A2A-conformant card, and we serve it at both paths for exactly this reason — agent-card.json as canonical, agent.json kept as a compat alias for older A2A clients still on v0.2.x. Worth the ENSIP doing the same rather than picking one: canonical at /.well-known/agent-card.json, and if /.well-known/agent.json is kept at all, it should be documented explicitly as a legacy alias, not "the" A2A path. Happy to open a PR with that fix if useful — it's a path rename plus one clarifying sentence, not a schema change.

(2) is @TMerlini's call as the spec's author — I'll let him speak to the ENS-specific rationale. My interest as a co-author is narrower: the sanitizationSpec field this ENSIP cites from WYRIWE (ERC-8299) needs some standard discovery surface for input-provenance verification, and /.well-known/ under an ENS name is a reasonable one — but whether that belongs in an ENSIP specifically vs. a general web standard is a fair question I don't have a strong stake in either way.

A2A moved the agent-card well-known path from /.well-known/agent.json
(v0.2.x) to /.well-known/agent-card.json (v0.3.0) per IANA feedback.
The draft still specified the old path, so ENSIP-27 cards would not be
discoverable by current A2A clients. Make agent-card.json canonical
throughout; keep agent.json documented only as an optional byte-identical
legacy alias (A2A <= v0.2.x). Preserves the deliberate path-sharing +
schema_version discrimination design at the current A2A path.

Caught by @jmacwhyte; fix per @babyblueviper1 review on PR ensdomains#75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TMerlini

TMerlini commented Jul 2, 2026

Copy link
Copy Markdown
Author

Thanks both — two good catches, taking them in order.

(1) The well-known path — you're right, fixed. A2A moved the card path from /.well-known/agent.json (v0.2.5) to /.well-known/agent-card.json (v0.3.0) per IANA feedback; I confirmed it against the v0.3.0 spec §5.3 rather than assume. The draft was still on the old path, so — exactly as you say — it wouldn't have been discoverable by current A2A clients. Pushed 36cdbf0: agent-card.json is canonical throughout, and agent.json is kept only as an optional, explicitly-documented, byte-identical legacy alias for A2A ≤ v0.2.x — which is precisely @babyblueviper1's recommendation (canonical + documented legacy alias, not "the" path). Folded it straight in so no separate PR needed, but thanks for the offer, Fede.

(2) Why an ENSIP, if agent cards are an AI concept? Fair question, and the honest answer is that the card schema isn't the ENS-specific part — you're right that that overlaps A2A, which is deliberate (byte-compatible, path-shared, schema_version-discriminated). What makes this an ENSIP is two things A2A doesn't address:

  1. It's the schema half of a mechanism ENS already accepted. ENSIP-26 standardised agent discovery via ENS text records (agent-endpoint[mcp/a2a]) and explicitly deferred the card format to a future ENSIP. This is that follow-up — it completes ENS name → text record (ENSIP-26) → card (this ENSIP). A2A tells you where the card sits on a domain; it says nothing about resolving a human-readable, user-owned, on-chain ENS name to that card. That name→endpoint binding is the ENS-native part.

  2. The fields that are genuinely ENS/on-chain-anchored, which a bare A2A card doesn't carry: erc8004 (on-chain identity anchor, verifiable from the name via an ERC-8004 registry), sanitizationSpec (WYRIWE / ERC-8299 input-provenance commitment), and trustEndpoints (recompute-don't-trust output verification). These bind the agent to on-chain identity and independently-checkable provenance — the value-add of discovering an agent through ENS rather than through a bare domain.

So: the AI/card-schema layer is A2A's and we align to it, not fork it; the ENS layer is the name→discovery resolution plus the on-chain-anchored extensions. It composes with A2A rather than competing with it.

If the editors would rather sharpen that boundary further, I'm genuinely open to narrowing ENSIP-27 to reference A2A's base card and specify only the ENS-anchored extension fields + the resolution binding, instead of restating the shared required fields. That might be the cleaner shape given your point — happy to take it that direction if it's the preference.

@TMerlini TMerlini changed the title Add ENSIP-27: Agent Card Schema (/.well-known/agent.json) Add ENSIP: Agent Card Schema (/.well-known/agent.json) Jul 11, 2026
@TMerlini

Copy link
Copy Markdown
Author

Renamed to drop the self-assigned "ENSIP-27." Per the repo README, ENSIP numbers are assigned by a maintainer at merge, so I've moved to the Add ENSIP: convention to clear the placeholder collision (a few open drafts are self-labeled 27). Scope is unchanged: this is the Agent Card Schema served under /.well-known/, already deployed and live on the dinamic.eth gateway, ERC-8004-anchored. Happy to let the editors assign the final number when it's picked up.

Per the repo README, ENSIP numbers are assigned by a maintainer at merge. Rename
ensips/27.md -> ensips/agent-card-schema.md and move the heading to the ENSIP-X
placeholder (matching the Add-ENSIP convention, e.g. ensdomains#68) to clear the
self-numbered collision. Content unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants