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:
npm i -g rkt-protocol
This gives you the rkt command globally. Available sub-commands:
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:
rkt serve --port 7749 --caps weather,forecast
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):
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:
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/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}
| Field | Required | Value |
|---|---|---|
Message-ID | Yes | Any UUID (v4 or v7). Must be unique per message. |
Session-ID | Yes | UUID you generate at connection start. All messages in this session share it. |
Timestamp | Yes | ISO 8601 UTC. new Date().toISOString() |
TTL | Yes | Seconds until message expires. Use 30. |
From | No | Your agent DID or name. e.g. did:rkt:gpt-5 |
Capabilities | No | Comma-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/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:
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.
// 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();
// 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):
pip install websockets
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
go get github.com/gorilla/websocket
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):
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/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.
// 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); }
Shared Memory
Any agent in any session can read/write the same memory namespace. Perfect for storing user preferences or session context.
// 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.
// 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.
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.
// 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.
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.
{
"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"
}
| Code | Meaning | Action |
|---|---|---|
401 | Unauthorized | Send AUTH message first |
429 | Rate limited | Wait Retry-After seconds |
602 | Capability missing | Use a different intent or agent |
603 | Version unsupported | Check min/max version in HELLO |
605 | Payment required | Send PAY message first |
Header Reference
| Header | Required | Example |
|---|---|---|
Version | Yes | 1 |
Message-ID | Yes | uuid-v4-or-v7 |
Session-ID | Yes | uuid-v4 |
Timestamp | Yes | 2026-06-28T12:00:00Z |
TTL | Yes | 30 |
From | No | did:rkt:my-agent |
To | No | rkt://weather.ai |
Authorization | No | Bearer TOKEN |
Stream | No | true |
Priority | No | LOW / NORMAL / HIGH / URGENT |
Capabilities | No | language,vision,code |
Accept-Language | No | ko, en |