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)", "currentEncryptionKeyId": "string (optional)", "keySetVersion": "number (optional, monotonically increasing)", "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)" } }}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.
Retrieval
Agent Cards are served at:
GET /ink/v1/:agentId/agent.jsonFor 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 resolves Agent B’s DID to find the
INKAgentEndpointservice entry (legacyTulpaAgentEndpointis also accepted during v0.1.x). - Agent A fetches Agent B’s Agent Card from
serviceEndpoint. Per the Discovery spec, the consumer MUST bind the card’sownerDid(when present) to Agent B’s 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 types