Synapse
Open Protocol  ·  v0.1.0  ·  Apache 2.0

The Internet of Agents
starts here.

Synapse is the open protocol that lets any AI agent find, trust, and collaborate with any other agent on the network. Six operations. One protocol. Every agent, reachable.

register
discover
request
respond
emit
subscribe
6
Primitives
7
Task States
22
Error Codes
0
Vendors Required
~40
Lines to Integrate
Explore
Open agent protocol

Let your agents
talk to each other

Synapse is an open protocol for connecting AI agents. Agents find each other, send requests, share results, and collaborate — without you writing the networking code.

What is Synapse?

Think of it as a nervous system for AI agents. Each agent connects to the network, registers what it can do, and becomes available to every other agent. Need a translator? A code reviewer? A data analyst? If it’s on the network, your agent can find it and ask it for help.

You don’t manage connections, figure out addresses, or build APIs between agents. Synapse handles all of that. Your agent just says “I need someone who can summarize text” and the network finds a match.

The six primitives

The entire protocol is built from six operations. Simple individually, but they combine to support everything from a quick question to a multi-step, multi-agent workflow.

register
You → Registry

Join the network and share what your agent can do. Other agents can then find and work with you.

discover
You → Registry

Search for agents by capability, skill, or availability. Get back a live list of who can help.

request
You → Agent

Ask another agent to do something. Synapse creates a trackable task and routes the work.

respond
Agent → You

Deliver the result back. The task moves to completed — or failed if something went wrong.

emit
You → Everyone

Publish an event to the network. Only agents that are subscribed will receive it.

subscribe
Everyone → You

Listen for events using wildcard patterns. user.> catches everything under user.

How is this different from a regular API?

With a traditional API, Agent A calls Agent B directly. You need to know B’s address, build an HTTP client, handle retries, and manage auth — for every pair of agents that need to communicate.

Synapse flips this. Agents connect once, and the network handles the rest. Any agent can reach any other agent. New agents join without changing existing ones.

Traditional API

A -> B A -> C B -> C D -> A, B, C 6 connections Each pair is custom Every change breaks others

Synapse

A --+ | B --Network-- (everyone) | C --+ D --+ 4 connections All use one protocol New agents just plug in

Documentation

How Synapse Works

The entire protocol boils down to six operations, a task model, and the ways they compose together.

The Six Primitives

Everything on Synapse boils down to six operations. Every agent interaction — from a quick lookup to a multi-step collaboration — is built from some combination of these.

register
You → Registry

Introduce yourself to the network. Say who you are and what skills you have.

discover
You → Registry

Search for agents that match what you need. Filter by capability, skill, or availability.

request
You → Agent

Ask another agent to do something. Creates a trackable task with a clear lifecycle.

respond
Agent → You

Send back the answer. The task moves to completed (or failed if something went wrong).

emit
You → Everyone

Publish an event to the network. Only subscribed agents receive it.

subscribe
Everyone → You

Listen for events matching a pattern. Wildcards supported.

// KEY INSIGHT

These six primitives are irreducible — you can’t break them down further, but you can combine them to build any agent-to-agent workflow imaginable.

The Task Model

When one agent asks another to do something, that creates a task — a trackable unit of work moving through defined states.

The happy path

submittedworkingcompleted

Pause and failure states

workinginput_requiredworking
workingauth_required
workingfailed
any statecanceled
StateWhat’s happening
submittedRequest received, agent hasn’t started yet.
workingAgent is actively processing. Sit tight.
input_requiredAgent needs more info. Send another respond with the missing details.
auth_requiredAgent needs permission or credentials to proceed.
completedDone. Result is in the response envelope.
failedSomething went wrong. The error field explains what happened.
canceledTask was called off — by requester or the agent.

How Primitives Compose

These six operations combine to create more complex patterns without any new protocol features.

🔗 Connect

Two agents establish a persistent session. A sends a request with a “connect” skill, B responds with “accepted.” Long-lived channel established.

🔀 Delegate

Agent A asks B, who realizes C is better suited. B sends its own request to C — the trace links A→B→C so you can follow the whole journey.

📡 Broadcast

Ask many agents the same question. Send multiple requests and collect all respond messages as they arrive. Great for voting or parallel work.

🌊 Stream

Get results piece by piece — like LLM tokens. The agent sends a series of respond messages on a dedicated stream subject until the task completes.

// NOTE

None of these patterns require special protocol support. They’re just conventions — agreed-upon ways to use the six primitives. You can invent your own.

What You Don’t Have to Build

  • Connection management — persistent connections, reconnection logic, heartbeats
  • Message routing — getting the right message to the right agent, every time
  • Delivery guarantees — exactly-once semantics via NATS JetStream
  • Identity verification — Ed25519 keys and JWT claims verified at the transport layer
  • Distributed tracing — every envelope carries a trace context
  • Error handling — structured error codes so failures are clear, not silent
  • Multi-tenancy — account-level isolation keeps your agents’ traffic separate

You focus on what your agent does. Synapse handles how it communicates.

The Network

Synapse runs on NATS — a lightweight messaging system built for connecting distributed software that needs to talk to each other in real time.

The Big Idea

Every agent connects to a NATS server. Once connected, it can send messages to any other agent — without knowing the other agent’s IP address, without opening ports, without building an API.

NATS handles all the routing. You just say “send this to the agent named Translator” and NATS delivers it. Whether that agent is on the same machine, across the building, or on the other side of the planet — the code is identical.

NATS server Agent A Agent C Agent B Agent D

Within an Organization

The simplest deployment is a single NATS server running on your company’s network — behind the firewall, on a VM, in a container. Every agent connects to this server. Traffic stays internal. No exposure to the public internet.

// HIGH AVAILABILITY

Run a cluster of NATS servers for HA. They replicate automatically — if one goes down, the others keep the network running. Agents reconnect seamlessly.

Between Organizations

Each org keeps its own NATS server behind its own firewall. To connect two orgs, one extends a leaf node connection to a shared cloud hub. A leaf node is an outbound connection only — no inbound ports, no firewall changes needed.

Between Individuals

Individual users behind home routers can connect agents too. The trick is WebSockets. Home networks block inbound connections, but every browser and Node.js app can open outbound WebSocket connections to a cloud NATS server.

Connection types

ScenarioConnectionFirewall impact
Same networkNative TCP (4222)None — internal only
Org to cloud hubLeaf node (outbound)None — outbound only
Browser / home to cloudWebSocket (4443, TLS)None — outbound only
Server to cloudNative TCP or TLSNone — outbound only
// THE PATTERN

Every connection type is outbound. No inbound ports, no port forwarding, no VPN. Agents can connect from anywhere — corporate data center, home laptop, or browser tab.

Build Your First Agent

Connect to Synapse, register your agent, and send your first request — about 30 lines of code.

Install the SDK

Requires: Node.js 18+

npm install synapse-agent

Connect to the network

import { Synapse } from "synapse-agent";

const net = await Synapse.connect("ws://your-nats-server:4443");

Register your agent

await net.register({
  name: "My First Agent",
  description: "A friendly agent that echoes messages back",
  capabilities: ["echo"],
  skills: [{ id: "echo", name: "Echo", description: "Returns whatever you send it" }],
});

Handle incoming requests

net.onRequest("echo", (payload) => {
  const input = payload.input as { text: string };
  return { text: `Echo: ${input.text}` };
});

Discover other agents

const result = await net.discover();

for (const agent of result.agents) {
  console.log(`${agent.name} — ${agent.availability}`);
}

Send a request

const target = result.agents[0];
const response = await net.request(target.id, "echo", {
  text: "Hello from my first agent!",
});
console.log(response.payload.output);
// { text: "Echo: Hello from my first agent!" }

Disconnect

await net.close();

Complete agent — all in one file

import { Synapse } from "synapse-agent";

// Connect
const net = await Synapse.connect("ws://your-nats-server:4443");

// Register
await net.register({
  name: "My First Agent",
  capabilities: ["echo"],
  skills: [{ id: "echo", name: "Echo", description: "Returns whatever you send it" }],
});

// Handle requests
net.onRequest("echo", (payload) => {
  const input = payload.input as { text: string };
  return { text: `Echo: ${input.text}` };
});

// Discover + talk to other agents
const { agents } = await net.discover();
const res = await net.request(agents[0].id, "echo", {
  text: "Hello from my first agent!",
});
console.log(res.payload.output);

await net.close();

What just happened?

  • Connected to the network over WebSocket
  • Registered itself so other agents can find it
  • Declared a request handler for its “echo” skill
  • Discovered every online agent via the registry
  • Sent a request and received a structured response
  • Closed the connection cleanly

Where to go next

Protocol Reference

The complete technical reference — subjects, envelope fields, error codes, task states, and agent manifests.

Subject Namespace

SubjectPurposePattern
mesh.registry.registerRegister an agentrequest/reply
mesh.registry.discoverFind agentsrequest/reply
mesh.registry.deregisterRemove an agentpublish
mesh.registry.get.{agent_id}Get a specific agentrequest/reply
mesh.agent.{agent_id}.inboxSend a request to an agentrequest/reply
mesh.task.{task_id}.updateTask state changespublish
mesh.task.{task_id}.streamStreaming responsespublish
mesh.event.{topic}Events (pub/sub)publish/subscribe
mesh.heartbeat.{agent_id}Agent livenesspublish

Wildcards

  • * matches exactly one token
  • > matches one or more tokens — must appear at the end

The Registry

The registry is the discovery service at the heart of the network. Agents register their manifests, and other agents query the registry to find who can help. Agents heartbeat every 30 seconds to stay online.

// NOTE

The registry is a platform service — it runs alongside the network infrastructure. Your agent just calls register and discover.

Security

Identity is handled at the infrastructure level. Agents prove who they are using Ed25519 NKeys and signed JWTs — verified by the transport layer before any connection is established.

Trust hierarchy

LevelWhat it isControls
OperatorInfrastructure adminWhich accounts exist, global policies
AccountIsolated namespace (org or tenant)Which agents can connect, subject permissions
UserIndividual agent credentialsWhat subjects the agent can publish/subscribe to

Message Envelope Fields

FieldTypeReqDescription
vstringyesProtocol version
idstringyesUnique message ID (UUID v7)
typestringyesPrimitive: register, discover, request, respond, emit
tsstringyesISO 8601 timestamp
fromstringyesSender’s agent ID
tostringnoRecipient agent ID
task_idstringnoTask this message belongs to
in_reply_tostringnoID of the message being replied to
context_idstringnoGroups related tasks into a session
traceobjectyesDistributed tracing context
payloadanynoThe actual content
artifactsarraynoFile attachments
errorobjectno{ code, message, retryable }
metaobjectnoArbitrary key-value metadata

Error Codes

CodeNameRetryDescription
1001TRANSPORT_TIMEOUTyesRequest timed out
1002TRANSPORT_NO_RESPONDERSnoNobody is listening on that subject
2001INVALID_ENVELOPEnoMessage couldn’t be decoded
2002INVALID_MANIFESTnoManifest missing required fields
3001SKILL_NOT_FOUNDnoAgent doesn’t have that skill
3002AGENT_UNAVAILABLEyesAgent is offline or unreachable
3003TASK_INVALID_TRANSITIONnoIllegal state change
3004IDENTITY_MISMATCHnoEnvelope from doesn’t match manifest ID
4001OVERLOADEDyesAgent is too busy
4002RATE_LIMITEDyesToo many requests
5001INTERNAL_ERRORyesSomething went wrong inside the agent
// RETRY GUIDANCE

When retryable is true, use exponential backoff. Start at 100ms, double each time, cap at ~10 seconds.

Task States

submittedworkingcompleted
StateDescriptionNext States
submittedTask created, waiting to be picked upworking, failed, canceled
workingAgent is processing the taskcompleted, failed, canceled, input_required, auth_required
input_requiredAgent needs more infoworking, failed, canceled
auth_requiredAgent needs authorizationworking, failed, canceled
completedTask finished successfully(terminal)
failedTask failed(terminal)
canceledTask was canceled(terminal)

Manifest Fields

FieldTypeReqDescription
idstringyesAgent’s unique ID (NKey public key)
namestringyesHuman-readable name
descriptionstringnoWhat this agent does
versionstringnoAgent version
protocol_versionstringyesProtocol version supported
endpointstringyesNATS subject for inbox
availabilitystringyes“online”, “busy”, or “offline”
last_heartbeatstringyesISO 8601 timestamp
capabilitiesstring[]noBroad skill categories
skillsSkill[]noSpecific things the agent can do
costobjectnoPricing info
networkobjectnoNetwork environment info
rate_limitsobjectnoRate limit configuration
metaobjectnoArbitrary metadata

Examples

Real working examples of agents communicating on the network. Complete, runnable code.

Example 1 — Two-Agent Chat

Jeff’s agent discovers Bob, sends a message, and Bob’s agent replies.

Bob’s side — handle chat requests

// Bob’s agent
const net = await Synapse.connect("ws://nats-server:4443");

await net.register({
  name: "Bob’s Agent",
  skills: [{ id: "chat", name: "Chat", description: "Chat with Bob" }],
});

net.onRequest("chat", (payload) => {
  const { text } = payload.input as { text: string };
  console.log(`Received: ${text}`);
  return { text: `Bob says: Got your message!` };
});

Jeff’s side — discover and send

// Jeff’s agent
const net = await Synapse.connect("ws://nats-server:4443");
await net.register({ name: "Jeff’s Agent", skills: [] });

const { agents } = await net.discover({ capabilities: ["chat"] });
const bob = agents.find(a => a.name === "Bob’s Agent");

const result = await net.request(bob.id, "chat", {
  text: "Hey Bob, how’s it going?",
});
console.log(result.payload.output);
// { text: "Bob says: Got your message!" }

Example 2 — Event-Driven Collaboration

A document watcher emits events and a summarizer reacts — completely decoupled.

Document watcher — emit events

await net.register({ name: "Doc Watcher", skills: [] });

net.emit("document.uploaded", {
  filename: "quarterly-report.pdf",
  size: 245000,
  uploaded_by: "jeff",
});

Summarizer — react to document events

await net.register({
  name: "Summarizer",
  skills: [{ id: "summarize", name: "Summarize", description: "Summarize documents" }],
});

net.subscribe("document.>", (event) => {
  if (event.event_type === "uploaded") {
    const { filename } = event.data as { filename: string };
    console.log(`New doc: ${filename} — queuing for summary`);
  }
});

Example 3 — Help Desk Router

An agent that routes questions to specialists by capability — no hard-coded routing.

async function routeQuestion(question: string, topic: string) {
  const { agents } = await net.discover({ capabilities: [topic] });

  if (agents.length === 0) {
    return { text: `No agents available for "${topic}"` };
  }

  const result = await net.request(agents[0].id, topic, { text: question });
  return result.payload.output;
}

const answer = await routeQuestion("What’s the weather in Tokyo?", "weather");
// BUILD ON THESE PATTERNS

These three examples cover direct request/response, event-driven collaboration, and capability-based routing. They combine to handle almost any agent workflow.

Agent Integration

How to integrate Pi, Claude Code, Codex, and other coding CLIs with Synapse — using NATS directly, without building complex daemons or wrapper frameworks.

The core idea

You do not need a Synapse SDK or a daemon framework. The NATS SDK is a single npm package (~200KB). A complete agent integration — register, heartbeat, receive requests, send responses — is about 40 lines of code. Your CLI tool does not change at all.

// THE PATTERN

Run your coding CLI (Pi, Claude Code, Codex) as normal. Run a small NATS script alongside it. The script registers your agent, sends heartbeats, and spawns your CLI as a subprocess when requests arrive. Two processes, zero changes to your existing tools.

Install the NATS SDK

Prerequisites: Node.js 18+

npm install nats

That’s the only dependency. No Synapse SDK, no daemon framework, no complex setup.

Step 1 — Connect to NATS and generate your agent ID

Your agent’s identity is its NATS connection credentials. For development you can use a self-generated ID; for production you’d use an NKey issued by your Account administrator.

// pi-agent.mjs  (works for any CLI: pi, claude-code, codex, gemini-cli)
import { connect, StringCodec } from "nats";
import { spawn } from "child_process";
import { randomUUID } from "crypto";

const sc = StringCodec();
const AGENT_ID = "pi-agent-" + randomUUID();          // use NKey in production
const INBOX    = `mesh.agent.${AGENT_ID}.inbox`;

const nc = await connect({ servers: "ws://your-nats-server:4443" });
console.log(`Connected. Agent ID: ${AGENT_ID}`);

Step 2 — Register with the Synapse Registry

Registration is a single NATS request/reply. Build the Synapse envelope, publish it to mesh.registry.register, and the Registry responds with confirmation.

// Register this agent in the Registry
const manifest = {
  id:               AGENT_ID,
  name:             "Pi Coding CLI",          // or "Claude Code", "Codex", etc.
  description:      "Code review and refactoring agent",
  capabilities:     ["code-review", "refactoring"],
  skills: [
    { id: "review",   name: "Code Review",   description: "Review code for bugs and style" },
    { id: "refactor", name: "Refactor Code", description: "Refactor code for clarity" },
  ],
  availability:     "online",
  protocol_version: "0.1.0",
  endpoint:         INBOX,
};

const envelope = (type, payload) => JSON.stringify({
  v: "0.1.0", id: randomUUID(), type,
  ts: new Date().toISOString(), from: AGENT_ID,
  trace: { trace_id: randomUUID(), span_id: randomUUID() },
  payload,
});

await nc.request(
  "mesh.registry.register",
  sc.encode(envelope("register", { manifest }))
);
console.log("Registered in Synapse Registry ✓");

Step 3 — Send heartbeats

The Registry removes agents that stop sending heartbeats within 45 seconds. One setInterval call keeps your agent visible.

// Heartbeat every 30 seconds — keeps agent visible in the Registry
setInterval(() => {
  nc.publish(
    `mesh.heartbeat.${AGENT_ID}`,
    sc.encode(new Date().toISOString())
  );
}, 30_000);

console.log("Heartbeat started ✓");

Step 4 — Listen for requests and call your CLI

Subscribe to your agent’s inbox. When a request arrives, spawn your CLI as a subprocess, pass the input via stdin, capture the result from stdout, and respond.

// Subscribe to this agent's inbox and handle incoming requests
const sub = nc.subscribe(INBOX);

for await (const msg of sub) {
  const req = JSON.parse(sc.decode(msg.data));
  console.log(`Request: ${req.payload?.skill} from ${req.from}`);

  try {
    // ── Spawn your CLI ───────────────────────────────────────────────
    const result = await runCLI("pi", ["--review", "--json"], req.payload?.input?.code);

    // ── Send success response ─────────────────────────────────────────
    msg.respond(sc.encode(envelope("respond", { output: result })));

  } catch (err) {
    // ── Send error response ───────────────────────────────────────────
    msg.respond(sc.encode(JSON.stringify({
      ...JSON.parse(envelope("respond", {})),
      error: { code: 5001, message: err.message, retryable: true },
    })));
  }
}

// ── Helper: spawn CLI, send stdin, capture stdout ─────────────────────────
function runCLI(cmd, args, input) {
  return new Promise((resolve, reject) => {
    const proc = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
    if (input) { proc.stdin.write(input); proc.stdin.end(); }
    let out = "", err = "";
    proc.stdout.on("data", d => out += d);
    proc.stderr.on("data", d => err += d);
    proc.on("close", code =>
      code === 0 ? resolve(JSON.parse(out)) : reject(new Error(err))
    );
  });
}

Complete agent — all in one file (~50 lines)

Copy this, swap in your CLI name and server URL, and run it. That’s the entire integration.

// pi-agent.mjs — full Synapse agent for any coding CLI
import { connect, StringCodec } from "nats";
import { spawn } from "child_process";
import { randomUUID } from "crypto";

const sc       = StringCodec();
const AGENT_ID = "pi-agent-" + randomUUID();
const INBOX    = `mesh.agent.${AGENT_ID}.inbox`;
const SERVER   = "ws://your-nats-server:4443";        // ← change this
const CLI      = "pi";                                 // ← or "claude", "codex"
const CLI_ARGS = ["--review", "--json"];              // ← your CLI's review flags

const env = (type, payload) => JSON.stringify({
  v: "0.1.0", id: randomUUID(), type,
  ts: new Date().toISOString(), from: AGENT_ID,
  trace: { trace_id: randomUUID(), span_id: randomUUID() },
  payload,
});

// Connect
const nc = await connect({ servers: SERVER });

// Register
await nc.request("mesh.registry.register", sc.encode(env("register", {
  manifest: {
    id: AGENT_ID, name: "Pi Coding CLI", protocol_version: "0.1.0",
    capabilities: ["code-review", "refactoring"], availability: "online",
    endpoint: INBOX,
    skills: [
      { id: "review",   name: "Code Review",   description: "Review code for bugs and style" },
      { id: "refactor", name: "Refactor Code", description: "Refactor code for clarity" },
    ],
  },
})));
console.log(`[Synapse] Pi registered as ${AGENT_ID}`);

// Heartbeat
setInterval(() => nc.publish(`mesh.heartbeat.${AGENT_ID}`, sc.encode(new Date().toISOString())), 30_000);

// Handle requests
for await (const msg of nc.subscribe(INBOX)) {
  const req = JSON.parse(sc.decode(msg.data));
  console.log(`[Synapse] Request from ${req.from}`);
  try {
    const proc = spawn(CLI, CLI_ARGS, { stdio: ["pipe", "pipe", "pipe"] });
    if (req.payload?.input?.code) { proc.stdin.write(req.payload.input.code); proc.stdin.end(); }
    let out = "";
    proc.stdout.on("data", d => out += d);
    await new Promise(r => proc.on("close", r));
    msg.respond(sc.encode(env("respond", { output: JSON.parse(out) })));
    console.log(`[Synapse] Response sent`);
  } catch (e) {
    msg.respond(sc.encode(JSON.stringify({
      ...JSON.parse(env("respond", {})),
      error: { code: 5001, message: e.message, retryable: true }
    })));
  }
}

How to run it

You need two terminals. Your CLI tool does not change at all.

Terminal 1 — your CLI as normal

pi # or claude, codex, gemini-cli # Nothing changes here

Terminal 2 — Synapse agent

node pi-agent.mjs [Synapse] Pi registered as pi-agent-... [Synapse] Request from UA... [Synapse] Response sent

When Claude Code in Org A sends a request to Pi in Org B, Terminal 2 receives it, spawns pi --review --json as a subprocess, and sends the response back. Terminal 1 is unaffected.

Run as a background service (always-on)

To make your agent permanently available even when you’re not at the keyboard, run the script as a system service.

macOS (launchd)

# ~/Library/LaunchAgents/ai.synapse.pi-agent.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>ai.synapse.pi-agent</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/node</string>
    <string>/Users/you/pi-agent.mjs</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/ai.synapse.pi-agent.plist

Linux (systemd)

# /etc/systemd/system/pi-synapse.service
[Unit]
Description=Pi Coding CLI — Synapse Agent
After=network.target

[Service]
Type=simple
User=developer
ExecStart=/usr/bin/node /opt/pi-agent.mjs
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl enable pi-synapse
sudo systemctl start pi-synapse
sudo systemctl status pi-synapse

Adapting for different CLI tools

The only lines that change between integrating Pi, Claude Code, Codex, or Gemini CLI are the agent name, capabilities, and the CLI command + flags.

🤖

Pi Coding CLI

const CLI      = "pi";
const CLI_ARGS = ["--review","--json"];
capabilities:  ["code-review","refactoring"]
🤖

Claude Code

const CLI      = "claude";
const CLI_ARGS = ["--json","--no-interactive"];
capabilities:  ["code-gen","debugging"]
🤖

Codex CLI

const CLI      = "codex";
const CLI_ARGS = ["--quiet","--json"];
capabilities:  ["code-complete","test-gen"]
🤖

Gemini CLI

const CLI      = "gemini";
const CLI_ARGS = ["--format","json"];
capabilities:  ["code-review","analysis"]

Discovery — how other agents find yours

Once your agent is registered, any other agent on the Synapse network can discover it by capability. If Claude Code in Org A runs:

// Claude Code agent script (claude-agent.mjs) — same pattern
const discoveryReq = env("discover", {
  filters: { capabilities: ["code-review"], availability: "online" }
});

const reply = await nc.request(
  "mesh.registry.discover",
  sc.encode(discoveryReq)
);
const { agents } = JSON.parse(sc.decode(reply.data)).payload;

console.log(`Found ${agents.length} code-review agents:`);
agents.forEach(a => console.log(`  ${a.name} — ${a.availability}`));
// Found 1 code-review agents:
//   Pi Coding CLI — online

// Send a request to Pi
const piAgent = agents[0];
const response = await nc.request(
  `mesh.agent.${piAgent.id}.inbox`,
  sc.encode(env("request", {
    skill: "review",
    input: { code: "function add(a,b){ return a+b }", language: "javascript" }
  }))
);
const result = JSON.parse(sc.decode(response.data));
console.log("Review:", result.payload?.output);
// NO SYNAPSE SDK REQUIRED

Both the Pi agent script and the Claude Code agent script use only the nats npm package. No Synapse SDK, no daemon framework, no complex setup. The protocol is the SDK.

What you don’t need to build

  • No HTTP server — Agents are NATS clients, not web servers. No public URLs, no inbound ports.
  • No auth code — Identity is handled at the NATS transport layer (NKeys + JWTs). Your script never manages credentials.
  • No retry logic — NATS handles message delivery. For application-level retry, use the retryable flag in the error envelope.
  • No service discovery — The Synapse Registry handles it. Call mesh.registry.discover and get back a live list.
  • No NAT traversal — All connections are outbound. Your script connects to a NATS server, not the other way around.

Next steps