Agent Card
The Agent Card is the public discovery document that advertises an agent’s identity, capabilities and availability. It is the first thing another agent fetches when initiating coordination.
Schema
{ "protocol": "ink/0.1", "agentId": "string", "ownerDid": "string (optional)", "ownerHandle": "string (optional)", "atprotoRecordUri": "string (optional)", "handle": "string", "displayName": "string (max 200)", "endpoint": "string (INK endpoint URL, see isInkEndpointUrl)", "inboxEndpoint": "string (optional, INK endpoint URL; MUST equal endpoint when both present)", "publicKeyMultibase": "string (z-prefixed, base58btc Ed25519 public key)", "profileSnapshot": "ProfileSnapshot (optional)", "capabilities": { "intentsAccepted": ["IntentType"], "intentsSent": ["IntentType"], "receipts": { "send": "boolean", "dispositions": ["ReceiptDisposition"] }, "auditExchange": "boolean (optional)", "thirdPartyAudit": { "services": ["ThirdPartyAuditService"], "submitPolicy": "all | high_value | none" } }, "keys": { "signing": ["KeyEntry (optional)"], "encryption": ["KeyEntry (optional)"] }, "currentSigningKeyId": "string (optional, max 128)", "currentEncryptionKeyId": "string (optional, max 128)", "keySetVersion": "number (optional, positive integer, monotonically increasing)", "supportedProtocolVersions": ["string (optional, up to 8 entries of max 16 chars)"], "visibility": "public | network_only | capability_gated | private", "availability": { "timezone": "string (IANA timezone)", "meetingHours": "string (optional, e.g. '9am-5pm ET')", "responseSla": "string (optional, e.g. '24h')" }, "governance": { "maxAcceptedDelegationDepth": "number (optional)", "supportedTransports": ["InkTransport (optional)"], "supportsCapabilityGatedDiscovery": "boolean (optional)", "handshakeBudget": { "maxChallengesPerCorrelation": "number (optional)", "maxIntentsPerMinute": "number (optional)" } }, "discovery": { "enabled": "boolean (required when discovery is present)", "scope": "public | network_only | capability_gated | private", "tags": ["string (optional, up to 32 entries of max 64 chars)"], "queryable": "boolean (optional)", "updatedAt": "string (optional, strict RFC 3339)" }, "cardSignature": { "keyId": "string (max 128)", "signature": "string (base64url no padding, exactly 86 chars)" }, "rotationChain": [ { "keySetVersion": "number (positive integer)", "signing": [ { "keyId": "string (max 128, unique within the link)", "publicKeyMultibase": "string (z-prefixed, max 128)", "status": "active | retired | revoked" } ], "prevKeyId": "string (max 128)", "signature": "string (base64url no padding, exactly 86 chars)" } ], "updatedAt": "string (optional, strict RFC 3339)"}discovery, cardSignature, rotationChain and updatedAt are optional top-level members. A consumer that predates them ignores them under the unknown-top-level-field rule, so a card that omits them still validates. String length bounds are measured in UTF-16 code units, except the endpoint byte length below.
Fields
Identity
| Field | Type | Required | Description |
|---|---|---|---|
protocol | string | Yes | Protocol version. Must be "ink/0.1". |
agentId | string | Yes | Unique agent identifier. |
ownerDid | string | No | The DID of the agent’s owner, if bound to an AT Protocol identity. |
ownerHandle | string | No | The AT Protocol handle of the owner. |
atprotoRecordUri | string | No | URI of the agentLink record in the owner’s AT Protocol repo. |
handle | string | Yes | The agent’s handle (e.g., alice.tulpa.network). |
displayName | string | Yes | Human-readable display name. Maximum 200 characters. |
endpoint | string | Yes | The URL where this agent receives INK messages. Validated by the isInkEndpointUrl grammar (see Endpoint URL grammar). |
inboxEndpoint | string | No | Forward-compat alias for endpoint, validated by the same grammar. When present alongside endpoint it MUST equal endpoint. |
publicKeyMultibase | string | Yes | Ed25519 public key in multibase format (base58btc, z prefix). Used to verify signatures on messages from this agent. |
Endpoint URL grammar
The endpoint, inboxEndpoint and each capabilities.thirdPartyAudit.services[].endpoint field are validated by the exported isInkEndpointUrl(value) check, not a generic URL parse. A conforming endpoint:
- uses lowercase
https://only; any other scheme is rejected - has a host (host is required)
- carries no userinfo (no embedded
user:pass@credentials) - carries no fragment (
#...) - is at most 2048 UTF-8 bytes
- contains no control characters, whitespace or backslash
- uses only well-formed
%XXpercent escapes - may include a port in the range 1..65535
Endpoints using another scheme, a fragment, embedded credentials or a malformed percent escape are rejected. This replaced the earlier broad URL check. Agent Card endpoint validation is pinned by the agent-card category in the conformance/v1 corpus (indexed by conformance/v1/manifest.json).
Profile Snapshot
An optional profileSnapshot field provides context for coordination decisions:
headline, professional headlineskills, list of professional skillsinterests, list of interestsavailability, timezone and response SLAopenTo, what the owner is open to (e.g., advisory, collaboration)
Capabilities
The capabilities object advertises what this agent can do:
| Field | Type | Description |
|---|---|---|
intentsAccepted | IntentType[] | Intent types this agent will process. |
intentsSent | IntentType[] | Intent types this agent may send. |
receipts | object | Whether the agent sends delivery receipts and which dispositions it supports. |
auditExchange | boolean | Whether the agent participates in audit log exchange. |
thirdPartyAudit | object | Third-party audit configuration: which services and the submission policy. |
Third-Party Audit Service
{ "endpoint": "string (INK endpoint URL, validated by isInkEndpointUrl)", "did": "string", "publicKey": "string"}Each capabilities.thirdPartyAudit.services[].endpoint is validated by the same endpoint URL grammar as endpoint.
Availability
The availability object helps other agents make scheduling and urgency decisions:
| Field | Type | Required | Description |
|---|---|---|---|
timezone | string | Yes | IANA timezone identifier (e.g., America/New_York). |
meetingHours | string | No | Human-readable meeting hours (e.g., 9am-5pm ET). |
responseSla | string | No | Expected response time (e.g., 24h, 4h). |
Visibility
The visibility field controls how the card is served on unauthenticated requests:
| Visibility | Unauthenticated GET | Authenticated Query |
|---|---|---|
public | Full card | Full card |
network_only | Redacted card | Full card (any authenticated INK peer) |
capability_gated | Redacted card | Full card (relationship-tier filtered) |
private | 404 | Denied unless explicitly connected |
Governance
The optional governance block advertises containment and authorization constraints:
| Field | Type | Description |
|---|---|---|
maxAcceptedDelegationDepth | number | Maximum delegation chain depth this agent will accept |
supportedTransports | InkTransport[] | Transport channels this agent supports |
supportsCapabilityGatedDiscovery | boolean | Whether authenticated card queries are supported |
handshakeBudget | object | Per-correlation handshake budget limits |
Standard transport identifiers: ink_http, ink_ws, extension_api, voice, line_phone, human_review_queue.
Key Management
The optional keys block advertises the agent’s signing and encryption key sets, enabling key rotation without changing the agent’s identity.
| Field | Type | Description |
|---|---|---|
keys.signing | KeyEntry[] | Signing keys (active and retired) |
keys.encryption | KeyEntry[] | Encryption keys (active and retired) |
currentSigningKeyId | string | keyId of the current active signing key |
currentEncryptionKeyId | string | keyId of the current active encryption key |
keySetVersion | number | Monotonically increasing version; incremented on every rotation |
Each KeyEntry includes keyId, algorithm, publicKeyMultibase, status (active / retired / revoked), validFrom and optional validUntil.
Agents without a keys block use the top-level publicKeyMultibase field as the sole signing key. See Key Rotation for full details on key lifecycle and verification rules.
Supported protocol versions
supportedProtocolVersions is an optional array of up to 8 wire-version strings, each at most 16 characters. A sender reads it to decide which wire version to emit. A card that omits it advertises nothing, and a sender treats that as ink/0.1 only. Card-signing capability is deliberately not advertised here: it is a property of the discovery surface, not of the message envelope.
Card Signature
A card MAY carry cardSignature, a self-authenticating proof that binds the whole card under a fixed domain. Without it, key authority and version negotiation rest entirely on TLS plus registry honesty, so a registry compromise can substitute keys or strip a supportedProtocolVersions entry undetected.
The normative rules live in ink-agent-card-signature.md. This section summarizes what an implementer has to build; where the two differ, the spec is right.
The proof
cardSignature is an object with a keyId and an 86-character base64url signature with no padding. The signature is Ed25519 over the UTF-8 bytes:
ink/agent-card\n<JCS(card)>JCS(card) is the RFC 8785 canonicalization of the full card with cardSignature removed and nothing else stripped. Every other member is covered, including endpoint, capabilities, keys, keySetVersion, supportedProtocolVersions and discovery. Partial coverage is rejected by design: it would let an attacker mutate any uncovered member while the signature still verified.
The signer resolves one of two ways. On a card that carries keys.signing, cardSignature.keyId MUST name an active entry of that set and MUST equal currentSigningKeyId; a card signed by a retired or revoked key is invalid. On a legacy card with no keys.signing, cardSignature.keyId MUST be the literal string bootstrap and the verifying key is the multibase decode of the top-level publicKeyMultibase.
An invalid signature rejects the card outright. An invalid-signed card is never demoted to an unsigned card. Only a card carrying no cardSignature at all counts as unsigned.
Rooting the signing key
A valid proof shows the named key signed the card. It does not show the key has authority over the identity. Rooting supplies that, and it differs by principal kind.
For a key-derived agentId (tulpa:zKEY or ink:zKEY), the Ed25519 key embedded in the identifier is the permanent genesis key and the root of card-authentication trust. An agent that has never rotated needs no chain: its cardSignature.keyId resolves to a key byte-equal to the embedded genesis key. An agent that has rotated publishes rotationChain, an array of at most 32 links walked genesis-to-head. Each link commits the complete signing key set at its keySetVersion, never a delta, and is signed under ink/card-rotation\n<JCS(link without signature)> by the key its prevKeyId names. keySetVersion MUST be strictly increasing and contiguous across consecutive links, and the head link MUST match the card’s own keySetVersion and keys.signing set exactly.
For a did:web agentId, the DID document is the root instead. Whenever a did:web card carries cardSignature, the resolved DID document MUST contain a verification method whose decoded public key is byte-equal to the signing key. A did:web card whose signing key is absent from the document is rejected. A did:web card MAY also carry a rotationChain and a receiver MUST verify it when present, but the chain corroborates rotation history and never substitutes for the anchor: link 1’s signer is re-rooted on a verification-method key in the DID document.
All key comparisons in this construction are over raw decoded key bytes with the 0xed01 Ed25519 multicodec prefix required, never over the multibase strings. Two non-canonical encodings of the same 32-byte key are the same key.
Rollout phases
The rollout is staged so unsigned deployments are never broken by a flag day.
| Phase | Who it binds | Obligation |
|---|---|---|
| A | Schema | cardSignature and rotationChain are optional. A receiver verifies a present proof and rejects an invalid one. Unsigned cards still validate. |
| B | Producers | Every implementation that serves a card MUST sign it. keySetVersion and updatedAt become MUST-on-publish. A producer MUST NOT emit a cardSignature it cannot root. |
| C | Receivers | A receiver MUST reject an unsigned card for a key-derived principal, and MUST require anchoring for a did:web card when the DID document resolves. |
Phase B is active. If you serve a card today, sign it, publish keySetVersion and updatedAt, and publish a rotationChain if you have ever rotated. Phase C has a start date fixed by rule rather than by announcement: §10 of the signature spec requires a minimum of 90 days between the Phase B ship and the start of Phase C. Both implementations already carry the Phase C decisions behind a default-off enforcement switch, so a receiver can turn them on early to test.
Phase C changes exactly two decision points and nothing else: the unsigned first-contact outcome, which becomes a reject for every principal kind, and the did:web resolver-unavailable outcome, where a cold verifier fails closed. Everything else a receiver does is already final in Phase A.
Discovery Descriptor
discovery is an optional opt-in descriptor that declares the agent consents to being surfaced by a directory or index. A card with no discovery object, or with enabled: false, is not discoverable, and discovery is never inferred from any other field.
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Opt-in flag. Never inferred. |
scope | visibility enum | Yes | The exposure the agent consents to. |
tags | string[] | No | Up to 32 self-declared hints of at most 64 characters. Hints, not verified claims. |
queryable | boolean | No | Whether the agent accepts directed discovery queries. |
updatedAt | string | No | Strict RFC 3339 timestamp. |
scope MUST NOT exceed the card’s own visibility. The effective exposure is the narrower of the two, so a descriptor can only narrow what the card already exposes and never widen it. A card whose scope ranks above its visibility is rejected. Because the descriptor rides inside the signed card, an indexer cannot strip its constraints without breaking cardSignature.
Retrieval
Agent Cards are served at:
GET <base>/ink/v1/<agentId>/agent.jsonwith the agentId percent-encoded as a single path segment. This is the sole normative discovery surface; see Discovery for the base derivation, the alias rule and the response contract.
For public visibility, this returns the full card with no authentication required.
For network_only and capability_gated visibility, unauthenticated GET returns a redacted card:
{ "type": "ink.agent.card", "version": "1.0", "agentId": "agent:abc123", "displayName": "Alice's Tulpa", "visibility": "capability_gated", "supportsInk": true, "discoveryMode": "authenticate_for_details", "updatedAt": "2026-03-18T12:00:00Z"}The type value ink.agent.card is the protocol-generic name introduced in v0.1.1. The legacy value tulpa.agent.card MUST also be accepted by consumers while the ink/0.x wire line is current; publishers SHOULD emit ink.agent.card. The legacy synonym is not removed by ink/0.2.
The redacted card confirms the agent exists and supports INK but strips capabilities, endpoints, keys, availability and profile data.
For private visibility, unauthenticated GET returns HTTP 404.
Authenticated Card Query
Authenticated peers can request the full card via:
POST /ink/v1/:agentId/agent-card-queryAuthorization: INK-Ed25519 <signature>{ "protocol": "ink/0.1", "type": "network.tulpa.agent_card_query", "from": "did:plc:requester", "nonce": "<base64url>", "timestamp": "2026-03-18T12:00:00Z", "requestedFields": ["capabilities", "availability"]}The response is either a full card (network.tulpa.agent_card_response) or a denial (network.tulpa.agent_card_denied) with reason unknown_requester, insufficient_trust or not_connected.
Usage in Discovery
- Agent A derives a resolution base for Agent B, either from Agent B’s identifier under the Discovery spec derivation rules or from trusted local configuration. A base never comes from the counterparty.
- Agent A fetches Agent B’s Agent Card from
<base>/ink/v1/<agentId>/agent.json. Per the Discovery spec, the consumer MUST bind the card’sownerDid(when present) to Agent B’s owner DID, bind the card’sagentIdto the agent identifier being addressed, refresh onkeyIdmiss orkeySetVersionbump, and apply the SSRF floor on the fetch. - Agent A inspects
capabilities.intentsAcceptedto determine if the desired intent type is supported. - Agent A uses
publicKeyMultibase(orkeys.signing) to verify signatures on future messages from Agent B. - Agent A uses
availabilityto make urgency and scheduling decisions. - Agent A POSTs signed envelopes to the inbound URL returned by
resolveAgentInbox(card), typicallycard.endpoint, withinboxEndpointaccepted as a forward-compat alias that MUST equalendpointwhen both are present (v0.1.1).
Validation
Implementations MUST validate the following:
protocolis exactlyink/0.1publicKeyMultibasestarts withzand decodes to a valid Ed25519 public keyendpoint,inboxEndpoint(when present) and eachcapabilities.thirdPartyAudit.services[].endpoint(when present) pass theisInkEndpointUrlgrammar (see Endpoint URL grammar); when bothendpointandinboxEndpointare present they MUST be equalintentsAcceptedandintentsSentcontain only recognized intent typesdiscovery.scope, when adiscoveryobject is present, does not exceed the card’svisibilitycardSignature, when present, verifies under the Card Signature construction and roots to the identity. An invalid proof rejects the card outright and MUST NOT be demoted to an unsigned card