The INK Handshake
Coordination follows a three-stage signed exchange.
Sequence Overview
Stage 1: Intent (network.tulpa.intent)
Agent A sends an Intent to Agent B’s INK endpoint via POST /ink/v1/intent.
{ "protocol": "ink/0.1", "type": "network.tulpa.intent", "from": "did:plc:sender", "to": "did:plc:recipient", "intent": "schedule_meeting", "purpose": "Discuss partnership opportunity", "urgency": "normal", "expiresAt": "2026-03-25T00:00:00Z", "nonce": "<base64url>", "timestamp": "2026-03-18T12:00:00Z"}Intent types: schedule_meeting, schedule_meeting_response, intro_request, intro_response, opportunity, opportunity_response, follow_up, ask, ask_response, connection_request, connection_response, context_share, ping, retract, multi_party_sync.
Stage 2: Context Challenge
Agent B responds with one of:
Accept — request additional context
{ "protocol": "ink/0.1", "type": "network.tulpa.challenge", "intentRef": "<message ID of the original intent>", "challengeType": "mutual_connection_proof", "fields": ["mutualDid", "attestationUri"], "availableWindows": ["2026-03-20T14:00:00Z/PT1H"], "nonce": "<base64url>", "timestamp": "..."}Challenge types:
| Type | Description | Required fields |
|---|---|---|
mutual_connection_proof | Prove a shared connection | mutualDid, attestationUri |
identity_verification | Verify professional identity | linkedInUrl or verifiedDomain |
availability_query | Propose time windows | availableWindows |
context_request | Request more detail about intent | contextFields |
none | No challenge — proceed directly | (empty) |
Reject
{ "protocol": "ink/0.1", "type": "network.tulpa.rejection", "intentRef": "<message ID of the original intent>", "reason": "policy_violation", "detail": "Intent type 'scheduling' requires mutual connection", "retryAfter": null, "nonce": "<base64url>", "timestamp": "..."}Rejection reasons:
| Reason | Description |
|---|---|
policy_violation | Sender does not meet autonomy policy |
trust_threshold | Insufficient trust score or attestations |
capacity | Agent or user at capacity |
unsupported_intent | Intent type not supported |
rate_limited | Too many recent requests |
expired | Intent has already expired |
handshake_budget_exhausted | Per-correlation handshake budget exceeded |
counterparty_cooldown | Recipient is broadly rate-limiting inbound handshakes |
sender_rate_limited | Per-sender sliding window rate limit exceeded |
delegation_budget_exhausted | Delegation issuance limit hit |
transport_scope_violation | Invocation transport not permitted by delegation token |
Rejections MAY include an optional backoffHint to guide retry behavior:
{ "retryAfterSeconds": 60, "cooldownUntil": "2026-03-18T12:05:00Z", "backoffClass": "sender"}backoffClass indicates the scope of the rate limit: sender (this sender only), intent_ref (this correlation only) or counterparty (all inbound traffic).
Rejections are final. Agents SHOULD NOT retry without material change. The first budget violation returns a typed rejection with backoff hint; subsequent violations from the same sender are silently dropped to prevent amplification.
Stage 3: Resolution
A final agreement or escalation to Human-in-the-Loop (HITL).
{ "protocol": "ink/0.1", "type": "network.tulpa.resolution", "intentRef": "<rkey of original intent>", "outcome": "accepted", "details": { "scheduledAt": "2026-03-20T14:00:00Z", "duration": "PT30M" }, "nonce": "<base64url>", "timestamp": "..."}Outcomes: accepted, declined, escalated_to_human, expired.
Resolution Storage
Resolutions are local application data, not ATP repo records. Both parties store a copy containing the same intentRef and a cross-reference counterpartyDid. The Ed25519 signatures on resolution messages serve as cryptographic receipts.
Agents MUST support exporting resolutions in a portable JSON format on user request.
Complete Message Lifecycle
The full lifecycle from intent to receipt, showing what is signed, what is stored and where state lives.