📡 Agent Connection Guide

Connecting Agents to RKT

RKT agents communicate over WebSocket using plain text messages. Any language with a WebSocket library can connect. This guide shows exactly how — with full, copy-pasteable code for TypeScript, Python, and Go.

Install

Install globally to get the rkt CLI — the fastest way to connect to, test, and run any RKT agent:

Bash
npm i -g rkt-protocol

This gives you the rkt command globally. Available sub-commands:

Bash
rkt serve                          # start an RKT agent server locally
rkt connect rkt://weather.ai       # interactive REPL — connect to any agent
rkt discover rkt://weather.ai      # print the agent's capability manifest
rkt ping rkt://weather.ai          # measure round-trip latency
rkt resolve weather.ai             # DNS/RNS lookup for an agent's record
rkt help                           # show all commands

Quick test — no code required

Start the example server in one terminal, connect from another:

Bash — Terminal 1
rkt serve --port 7749 --caps weather,forecast
Bash — Terminal 2
rkt connect ws://localhost:7749/rkt
# Then type at the > prompt:
> {"intent":"echo","params":{"city":"Seoul"}}
> discover
> ping
> exit

To use the library in your own project (not globally):

Bash
npm install rkt-protocol
ℹ️

Your RKT server runs on port 7749 by default and accepts WebSocket connections at /rkt. So the URL is always ws://host:7749/rkt (or wss:// with TLS).

Overview

Every agent connection follows the same 4-step flow regardless of language:

Agent RKT Server | | |──── WebSocket handshake (HTTP Upgrade) ──▶| ← TCP connection |──── HELLO ───────────────────────────────▶| ← who are you + capabilities |◀─── HELLO_ACK ───────────────────────────| ← server capabilities confirmed | | |──── REQUEST (intent: "your.intent") ─────▶| ← send work |◀─── RESPONSE ────────────────────────────| ← get result | | |──── GOODBYE ─────────────────────────────▶| ← clean disconnect

Handshake Flow — Exact Details

Step 1 — Open WebSocket connection

Connect to ws://<host>:7749/rkt with subprotocol rkt.1.0.

The subprotocol header is important — the server will reject connections without it.

Step 2 — Send HELLO

Immediately after the WebSocket connection opens, send a HELLO message. Format:

RKT Message HELLO
RKT/1.0 HELLO
Version: 1
Message-ID: 01960000-0000-7000-ab12-000000000001
Session-ID: e3ad1f12-4e3a-4b1e-9c2d-000000000042
Timestamp: 2026-06-28T12:00:00Z
TTL: 30
From: did:rkt:my-agent
User-Agent: MyAgent/1.0
Capabilities: language,vision,code

{"min_version":1,"max_version":1}
FieldRequiredValue
Message-IDYesAny UUID (v4 or v7). Must be unique per message.
Session-IDYesUUID you generate at connection start. All messages in this session share it.
TimestampYesISO 8601 UTC. new Date().toISOString()
TTLYesSeconds until message expires. Use 30.
FromNoYour agent DID or name. e.g. did:rkt:gpt-5
CapabilitiesNoComma-separated list of what you can do.

Step 3 — Receive HELLO_ACK

The server responds with HELLO_ACK. The Reply-To header matches your HELLO's Message-ID.

RKT MessageHELLO_ACK
RKT/1.0 HELLO_ACK
Version: 1
Message-ID: 01960000-0000-7000-ab12-000000000002
Reply-To: 01960000-0000-7000-ab12-000000000001
Session-ID: e3ad1f12-4e3a-4b1e-9c2d-000000000042
Timestamp: 2026-06-28T12:00:00Z
From: rkt://weather.ai
User-Agent: WeatherAgent/1.0
Capabilities: weather,forecast,alerts

{"agreed_version":1,"session_timeout":300}

Once you get HELLO_ACK, the session is READY. You can now send REQUEST messages.

Message Format

RKT messages are plain text with this structure:

Format
RKT/1.0 {TYPE}
{Header-Name}: {value}
{Header-Name}: {value}
                          ← blank line separates headers from body
{JSON body}               ← optional, only when Content-Type is set
⚠️

Headers and body are separated by exactly one blank line (\n\n). If there's no body, the message ends after the last header with a single \n.


TypeScript / Node.js

Two options: use the built-in RKTClient (recommended), or raw WebSocket for custom control.

TypeScript my-agent.ts
// npm i -g rkt-protocol  (global CLI)
// npm install rkt-protocol  (library in your project)
import { RKTClient } from "rkt-protocol";

const client = new RKTClient({
  userAgent:    "MyAgent/1.0",
  capabilities: ["language", "vision"],
});

// Connect — HELLO/HELLO_ACK happens automatically
await client.connect({ url: "ws://localhost:7749/rkt" });
// or: await client.connect("rkt://weather.yourdomain.com");

// Send a request by intent — no URL, no schema needed
const result = await client.request("weather.current", {
  city: "Seoul",
});
console.log(result);
// → { city: "Seoul", temperature: 24, condition: "Sunny" }

// Stream a large response (e.g. 7-day forecast)
for await (const day of client.stream("weather.forecast", { days: 7 })) {
  console.log("Day:", day);
}

// Discover what the server can do
const info = await client.discover();
console.log("Capabilities:", info.body);

// Store something in shared memory
await client.memorySet("preferred_city", "Seoul", {
  namespace: "user.prefs",
  ttl: 3600, // seconds
});

// Read it back
const city = await client.memoryGet("preferred_city", "user.prefs");

// Pay for the API call
await client.pay(0.001, "RKTT", "forecast call", "rkt://weather.ai");

// Always disconnect cleanly
await client.disconnect();
TypeScript raw-client.ts
// npm install ws uuid @types/ws @types/uuid
import WebSocket from "ws";
import { v4 as uuidv4 } from "uuid";

const SESSION_ID = uuidv4();
const ws         = new WebSocket("ws://localhost:7749/rkt", "rkt.1.0");

function send(type: string, extra: string = "", body?: object) {
  const msgId     = uuidv4();
  const timestamp = new Date().toISOString();
  const bodyStr   = body ? JSON.stringify(body) : null;
  const msg = [
    `RKT/1.0 ${type}`,
    `Version: 1`,
    `Message-ID: ${msgId}`,
    `Session-ID: ${SESSION_ID}`,
    `Timestamp: ${timestamp}`,
    `TTL: 30`,
    extra,
    bodyStr ? `Content-Type: application/json` : "",
    "",
    bodyStr ?? "",
  ].join("\n");
  ws.send(msg);
  return msgId;
}

ws.on("open", () => {
  send("HELLO", "User-Agent: MyAgent/1.0\nCapabilities: language",
    { min_version: 1, max_version: 1 });
});

ws.on("message", (data) => {
  const raw = data.toString();
  const [headerPart, bodyPart] = raw.split("\n\n");
  const type = headerPart.split("\n")[0].split(" ")[1];

  if (type === "HELLO_ACK") {
    // Session is ready — send a request
    send("REQUEST", "To: rkt://weather.ai",
      { intent: "weather.current", params: { city: "Seoul" } });
  }

  if (type === "RESPONSE") {
    const body = JSON.parse(bodyPart);
    console.log("Got data:", body.data);
    send("GOODBYE");
    ws.close();
  }
});

ws.on("error", console.error);

Python

Install websockets and uuid (built-in):

Bash
pip install websockets
Python rkt_client.py
import asyncio
import json
import uuid
from datetime import datetime, timezone
import websockets

SERVER_URL = "ws://localhost:7749/rkt"

def make_msg(msg_type, session_id, extra_headers="", body=None):
    msg_id    = str(uuid.uuid4())
    timestamp = datetime.now(timezone.utc).isoformat()

    lines = [
        f"RKT/1.0 {msg_type}",
        "Version: 1",
        f"Message-ID: {msg_id}",
        f"Session-ID: {session_id}",
        f"Timestamp: {timestamp}",
        "TTL: 30",
    ]
    if extra_headers:
        lines.append(extra_headers)
    if body is not None:
        body_str = json.dumps(body)
        lines.append(f"Content-Type: application/json")
        lines.append(f"Content-Length: {len(body_str.encode())}")
        lines.append("")           # blank line
        lines.append(body_str)
    else:
        lines.append("")           # blank line (no body)

    return "\n".join(lines), msg_id

def parse_msg(raw):
    if "\n\n" in raw:
        header_part, body_part = raw.split("\n\n", 1)
    else:
        header_part, body_part = raw, ""

    lines      = header_part.split("\n")
    msg_type   = lines[0].split(" ")[1] if lines else ""
    headers    = {}

    for line in lines[1:]:
        if ":" in line:
            k, v = line.split(":", 1)
            headers[k.strip()] = v.strip()

    body = json.loads(body_part) if body_part.strip() else None
    return msg_type, headers, body

async def main():
    session_id = str(uuid.uuid4())

    async with websockets.connect(
        SERVER_URL,
        subprotocols=["rkt.1.0"],
    ) as ws:

        # 1. Send HELLO
        hello, hello_id = make_msg(
            "HELLO", session_id,
            extra_headers="User-Agent: PythonAgent/1.0\nCapabilities: language,vision",
            body={"min_version": 1, "max_version": 1},
        )
        await ws.send(hello)

        # 2. Wait for HELLO_ACK
        ack_raw             = await ws.recv()
        ack_type, ack_hdrs, ack_body = parse_msg(ack_raw)
        assert ack_type == "HELLO_ACK", f"Expected HELLO_ACK, got {ack_type}"
        print(f"Connected! Server: {ack_hdrs.get('User-Agent')}")
        print(f"Server caps: {ack_hdrs.get('Capabilities')}")

        # 3. Send REQUEST
        req, req_id = make_msg(
            "REQUEST", session_id,
            extra_headers="To: rkt://weather.ai",
            body={
                "intent":  "weather.current",
                "params":  {"city": "Seoul"},
                "context": {"language": "ko", "timezone": "Asia/Seoul"},
            },
        )
        await ws.send(req)

        # 4. Receive RESPONSE
        resp_raw              = await ws.recv()
        resp_type, _, resp_body = parse_msg(resp_raw)
        assert resp_type == "RESPONSE"

        print("Weather data:", resp_body["data"])
        # → {'city': 'Seoul', 'temperature': 24, 'condition': 'Sunny', ...}

        # 5. Memory: store a preference
        mem_set, _ = make_msg(
            "MEMORY_SET", session_id,
            body={"key": "city", "value": "Seoul", "namespace": "prefs", "ttl": 3600},
        )
        await ws.send(mem_set)
        await ws.recv()  # ACK

        # 6. GOODBYE
        bye, _ = make_msg("GOODBYE", session_id)
        await ws.send(bye)

asyncio.run(main())

Go

Bash
go get github.com/gorilla/websocket
Go main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/google/uuid"
    "github.com/gorilla/websocket"
)

const ServerURL = "ws://localhost:7749/rkt"

func makeMsg(msgType, sessionID, extraHeaders string, body any) (string, string) {
    msgID     := uuid.New().String()
    timestamp := time.Now().UTC().Format(time.RFC3339)

    lines := []string{
        fmt.Sprintf("RKT/1.0 %s", msgType),
        "Version: 1",
        fmt.Sprintf("Message-ID: %s", msgID),
        fmt.Sprintf("Session-ID: %s", sessionID),
        fmt.Sprintf("Timestamp: %s", timestamp),
        "TTL: 30",
    }
    if extraHeaders != "" {
        lines = append(lines, extraHeaders)
    }
    if body != nil {
        b, _ := json.Marshal(body)
        lines = append(lines, "Content-Type: application/json")
        lines = append(lines, "")
        lines = append(lines, string(b))
    } else {
        lines = append(lines, "")
    }
    return strings.Join(lines, "\n"), msgID
}

func parseType(raw string) string {
    firstLine := strings.SplitN(raw, "\n", 2)[0]
    parts     := strings.SplitN(firstLine, " ", 3)
    if len(parts) >= 2 { return parts[1] }
    return ""
}

func parseBody(raw string) map[string]any {
    parts := strings.SplitN(raw, "\n\n", 2)
    if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" { return nil }
    var result map[string]any
    json.Unmarshal([]byte(parts[1]), &result)
    return result
}

func main() {
    sessionID := uuid.New().String()
    dialer    := websocket.Dialer{Subprotocols: []string{"rkt.1.0"}}

    conn, _, err := dialer.Dial(ServerURL, http.Header{})
    if err != nil { log.Fatal(err) }
    defer conn.Close()

    // 1. HELLO
    hello, _ := makeMsg("HELLO", sessionID,
        "User-Agent: GoAgent/1.0\nCapabilities: language",
        map[string]any{"min_version": 1, "max_version": 1},
    )
    conn.WriteMessage(websocket.TextMessage, []byte(hello))

    // 2. HELLO_ACK
    _, ackRaw, _ := conn.ReadMessage()
    fmt.Println("Handshake:", parseType(string(ackRaw)))

    // 3. REQUEST
    req, _ := makeMsg("REQUEST", sessionID, "To: rkt://weather.ai",
        map[string]any{
            "intent": "weather.current",
            "params": map[string]any{"city": "Seoul"},
        },
    )
    conn.WriteMessage(websocket.TextMessage, []byte(req))

    // 4. RESPONSE
    _, respRaw, _ := conn.ReadMessage()
    body          := parseBody(string(respRaw))
    fmt.Printf("Response data: %+v\n", body["data"])

    // 5. GOODBYE
    bye, _ := makeMsg("GOODBYE", sessionID, "", nil)
    conn.WriteMessage(websocket.TextMessage, []byte(bye))
}

Raw WebSocket (any language / tool)

You can test RKT directly with wscat (Node.js CLI tool):

Bash
npm install -g wscat
wscat -c ws://localhost:7749/rkt --subprotocol rkt.1.0

Then paste this message (hit Enter twice for the blank line before the body):

RKT Message — paste into wscat
RKT/1.0 HELLO
Version: 1
Message-ID: aaaaaaaa-0000-0000-0000-000000000001
Session-ID: bbbbbbbb-0000-0000-0000-000000000001
Timestamp: 2026-06-28T00:00:00Z
TTL: 30
User-Agent: wscat/test

{"min_version":1,"max_version":1}

Streaming

Set Stream: true in your REQUEST. The server replies with STREAM_BEGIN, multiple STREAM_DATA chunks (each with a Seq number), then STREAM_END.

TypeScript
// Using RKTClient — async generator
for await (const chunk of client.stream("weather.forecast", { days: 7 })) {
  // chunk = { date: "2026-06-29", high: 27, low: 18, condition: "Sunny" }
  console.log(chunk);
}
Agent Server |── REQUEST (Stream: true) ────────▶| |◀─ STREAM_BEGIN (total_chunks: 7) ─| |◀─ STREAM_DATA (Seq: 1) ───────────| ← day 1 |◀─ STREAM_DATA (Seq: 2) ───────────| ← day 2 |◀─ STREAM_DATA (Seq: 7) ───────────| ← day 7 |◀─ STREAM_END ─────────────────────|

Shared Memory

Any agent in any session can read/write the same memory namespace. Perfect for storing user preferences or session context.

TypeScript
// Store
await client.memorySet("preferred_city", "Seoul", {
  namespace: "user.prefs",
  ttl: 86400,   // 24 hours in seconds
});

// Read (from any other agent / session)
const city = await client.memoryGet("preferred_city", "user.prefs");
// → "Seoul"
💡

Namespaces keep memory isolated. Use user.{id}, project.{name}, or session.{id} patterns to avoid collisions between agents.


Payment

PAY is a first-class message type. Agents pay each other directly inside the protocol — no external payment API needed.

TypeScript
// Pay 0.001 RKTT to the weather agent for a forecast call
await client.pay(
  0.001,              // amount
  "RKTT",            // currency (RKT Token)
  "forecast API call", // description
  "rkt://weather.ai"  // recipient
);

Discovery — What Can This Agent Do?

Send a DISCOVER message to get the full capability manifest without reading any documentation.

TypeScript
const info = await client.discover();
console.log(info.body);
// {
//   agent: "rkt://weather.ai",
//   version: "1.3.0",
//   capabilities: ["weather", "forecast", "alerts", "history"],
//   streaming: true,
//   memory: true,
//   events: true
// }

Authentication

If the server requires auth, send an AUTH message after the handshake. The server responds with AUTH_OK and a session token.

TypeScript
// Option 1: Bearer token (API key)
const client = new RKTClient({
  authorization: "Bearer YOUR_API_KEY_HERE",
});

// Option 2: DID auth (after connect)
const token = await client.auth(["read", "write", "memory.read"]);
console.log("Session token:", token);

Using RKT as an LLM Tool Call

Wrap an RKT client in a tool so Claude, GPT, or any LLM can call it.

TypeScript rkt-tool.ts — register as Claude/GPT tool
import Anthropic from "@anthropic-ai/sdk";
import { RKTClient } from "rkt-protocol";

const anthropic = new Anthropic();

// 1. Define the tool
const rktTool = {
  name: "rkt_request",
  description: "Send a request to an RKT agent. Use this to get real-time data from AI services.",
  input_schema: {
    type: "object",
    properties: {
      uri:    { type: "string", description: "RKT agent URI e.g. rkt://weather.ai" },
      intent: { type: "string", description: "The intent to call e.g. weather.current" },
      params: { type: "object", description: "Parameters to pass" },
    },
    required: ["uri", "intent"],
  },
};

// 2. Execute the tool
async function executeRktTool(input: { uri: string; intent: string; params?: object }) {
  const client = new RKTClient({ userAgent: "ClaudeAgent/1.0" });
  // Convert rkt:// URI to ws:// for connection
  const wsUrl = input.uri
    .replace("rkt://", "ws://") + ":7749/rkt";
  await client.connect({ url: wsUrl });
  const result = await client.request(input.intent, input.params);
  await client.disconnect();
  return result;
}

// 3. Use in agentic loop
const messages = [{ role: "user", content: "What is the weather in Seoul right now?" }];

const response = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  tools: [rktTool],
  messages: messages,
});

// Claude decides to call rkt_request → you run executeRktTool → feed result back
for (const block of response.content) {
  if (block.type === "tool_use" && block.name === "rkt_request") {
    const data = await executeRktTool(block.input as any);
    console.log("RKT returned:", data);
  }
}

Error Handling

Error messages have Type: ERROR and a JSON body with code and message.

JSON — Error body
{
  "code":    602,
  "name":    "CAPABILITY_MISSING",
  "message": "Intent 'maps.navigate' not found",
  "details": { "available_intents": ["weather.current", "weather.forecast"] },
  "request_id": "01960000-0000-7000-ab12-000000000003"
}
CodeMeaningAction
401UnauthorizedSend AUTH message first
429Rate limitedWait Retry-After seconds
602Capability missingUse a different intent or agent
603Version unsupportedCheck min/max version in HELLO
605Payment requiredSend PAY message first

Header Reference

HeaderRequiredExample
VersionYes1
Message-IDYesuuid-v4-or-v7
Session-IDYesuuid-v4
TimestampYes2026-06-28T12:00:00Z
TTLYes30
FromNodid:rkt:my-agent
ToNorkt://weather.ai
AuthorizationNoBearer TOKEN
StreamNotrue
PriorityNoLOW / NORMAL / HIGH / URGENT
CapabilitiesNolanguage,vision,code
Accept-LanguageNoko, en