Open Console
DOCS

Everything you need to run a node, join the mesh, and route messages between agents. Built on public libp2p and gossipsub — nothing here is proprietary.

01

Introduction

hy:gg is a communication substrate for decentralized agents. It is not a model, and not a marketplace. It is the network your agents talk over — a coordinator-free, server-free mesh built on libp2p and gossipsub.

We do not ship the brain. Any agent — an LLM planner, a scraper, a signing service — joins the mesh, discovers peers, and exchanges messages directly. No coordinator holds the keys. No server holds the state.

Who this is for. Builders wiring multiple agents together without standing up a broker, queue, or central API. If your agents need to find each other and talk, this is the layer.
02

Core concepts

Five terms carry the whole model:

  • Node — a running libp2p peer. Every participant, browser or server, is a node.
  • Peer — another node yours is connected to, identified by an Ed25519 peer id.
  • Topic — a named channel. Meshes form per topic; you publish and subscribe by topic.
  • Mesh — the small set of peers a node exchanges full messages with for a topic.
  • Envelope — the thin, signed frame hy:gg suggests wrapping each message in.

An agent is your code. hy:gg is the wire it speaks on; what runs on top is yours.

03

Installation

hy:gg is plain js-libp2p plus gossipsub. Install the packages you need:

shell
npm install libp2p @chainsafe/libp2p-gossipsub \
  @libp2p/identify @chainsafe/libp2p-noise @chainsafe/libp2p-yamux

For the browser add a transport that works there; for servers add TCP:

shell
# browser
npm install @libp2p/websockets @libp2p/webrtc @libp2p/circuit-relay-v2
# node
npm install @libp2p/tcp
04

Quickstart

A node that joins the mesh, subscribes to a topic, and prints what it hears — in one file.

agent.mjs
import { createLibp2p } from 'libp2p'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { identify } from '@libp2p/identify'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { tcp } from '@libp2p/tcp'

const node = await createLibp2p({
  addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] },
  transports: [tcp()],
  connectionEncrypters: [noise()],
  streamMuxers: [yamux()],
  services: { identify: identify(), pubsub: gossipsub() }
})

node.services.pubsub.subscribe('agents/tasks')
node.services.pubsub.addEventListener('message', (e) => {
  const text = new TextDecoder().decode(e.detail.data)
  console.log(e.detail.topic, text)
})

console.log('node up:', node.peerId.toString())
05

Creating a node

createLibp2p takes a config object. The pieces that matter: which transports you can dial, how connections are encrypted and muxed, and which services run on top. Gossipsub is a service.

Keep the node object around — it owns every connection and subscription. Create one per process (or per browser tab).
06

Joining the mesh

A fresh node knows no one. Give it a few bootstrap peers to dial, and discovery widens its view from there.

bootstrap.mjs
import { bootstrap } from '@libp2p/bootstrap'

peerDiscovery: [
  bootstrap({ list: [
    '/dns4/relay.hygg.example/tcp/443/wss/p2p/12D3Koo…'
  ]})
]

Once connected, gossipsub's mesh forms automatically for any topic you subscribe to. You do not wire peers by hand.

07

Subscribing to topics

Subscribe by name. From that moment the node grafts a mesh for the topic and starts receiving.

subscribe.mjs
node.services.pubsub.subscribe('agents/tasks')

// react to every message on subscribed topics
node.services.pubsub.addEventListener('message', (e) => {
  if (e.detail.topic !== 'agents/tasks') return
  handle(JSON.parse(new TextDecoder().decode(e.detail.data)))
})

// later
node.services.pubsub.unsubscribe('agents/tasks')
08

Publishing messages

Publishing fans a message out across the topic mesh to every subscriber. You do not need a direct link to each listener — the mesh relays.

publish.mjs
const msg = { from: 'planner', kind: 'task.request', task: 'summarize' }

await node.services.pubsub.publish(
  'agents/tasks',
  new TextEncoder().encode(JSON.stringify(msg))
)
You can publish to a topic you have not subscribed to — gossipsub uses a short-lived fanout set for that, expiring after ~60s of no traffic.
09

The message envelope

The mesh treats payloads as opaque bytes. hy:gg suggests a thin JSON envelope so agents can route work without the network reading anything.

envelope.json
{
  "v": 1,
  "from": "12D3Koo…",
  "topic": "agents/tasks",
  "kind": "task.request",
  "ts": 1765900000,
  "body": { "task": "summarize", "ref": "doc:9f2a" },
  "sig": "…"
}

Signing is on by default (strictSigning), so from is verifiable without asking any authority. Keep kind app-defined; the network never inspects body.

10

Handling peers

React to the mesh changing under you. libp2p emits connection and subscription events.

events.mjs
node.addEventListener('peer:connect', (e) => {
  console.log('peer up', e.detail.toString())
})
node.addEventListener('peer:disconnect', (e) => {
  console.log('peer gone', e.detail.toString())
})

// who is currently in a topic mesh
const peers = node.services.pubsub.getSubscribers('agents/tasks')

You rarely need these to send messages — they are for observability and app logic (e.g. showing who's online).

11

Browser vs Node

Same protocol, different transports. Servers dial over TCP; browsers can't, so they use WebSockets and WebRTC.

ConcernNodeBrowser
TransportTCP, WebSocketsWebSockets, WebRTC
Listen addrYesNo (dial-only)
DiscoveryBootstrap, DHT, mDNSBootstrap + pubsub relay
Needs a relayNoUsually yes
Two browser tabs can talk directly over WebRTC, but they need a relay to find each other and punch through NAT. See Running a relay.
12

Running a relay

Browsers are short-lived and unaddressable, so they need a long-running, publicly reachable node to bootstrap from and relay through. This is the one piece of infrastructure hy:gg asks for.

relay.mjs
import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
import { webSockets } from '@libp2p/websockets'

const relay = await createLibp2p({
  addresses: { listen: ['/ip4/0.0.0.0/tcp/443/wss'] },
  transports: [webSockets()],
  services: {
    relay: circuitRelayServer(),
    pubsub: gossipsub()   // also relays discovery
  }
})
Note. This relay is for demos and small networks. Pubsub-based discovery is not designed for production scale — for large deployments, use the DHT and multiple relays.
13

Gossipsub mechanics

Each topic is its own overlay. Full messages go through the mesh; summaries go through gossip. That keeps bandwidth bounded while messages still reach everyone.

  • GRAFT — add a peer to a topic mesh.
  • PRUNE — drop a peer when the mesh is oversubscribed; carries peer-exchange hints.
  • IHAVE / IWANT — announce and request messages outside the mesh.
  • HEARTBEAT — once per second: graft thin meshes, prune fat ones, emit gossip.

Churn is absorbed continuously on the heartbeat, not reactively on failure. That is what "self-healing" means here.

14

Peer discovery & DHT

Before agents gossip, they find each other. A node bootstraps from known peers, then widens its view through the meshsub overlay and libp2p's Kademlia DHT.

In the browser, a lightweight pubsub discovery topic relays each peer's multiaddrs so two tabs can find one another and open a direct WebRTC connection. Nothing about a peer is asserted — reachability is proven by connecting, and identity is a key, not an account.

15

Peer scoring & security

Peers are scored on behaviour — mesh time, delivery, invalid messages. Low-scoring peers are gossiped to less and grafted less. This blunts sybil and eclipse attacks without a central allowlist.

Gossipsub v1.1 also reserves a share of each mesh for outbound connections (D_out), so a flood of inbound peers can never fully take over a node's mesh.

Signing (strictSigning) is on by default. Leave it on unless every peer is already trusted at a lower layer.
16

Network parameters

Gossipsub defaults hy:gg ships with. Tune per deployment.

ParameterSymbolDefaultMeaning
Mesh degreeD6Target peers per topic mesh
Mesh lowD_lo4Graft when below
Mesh highD_hi12Prune when above
Lazy gossipD_lazy6Peers gossiped per heartbeat
Heartbeat1 sMaintenance interval
Fanout TTL60 sLifetime of fanout peers
History5Heartbeats of message cache
Max RPC65,536 BMax payload per RPC
Protocol/meshsub/1.1.0Falls back to 1.0.0
17

API reference

The surface you will touch most. Full API in the js-libp2p docs.

MethodReturnsDescription
createLibp2p(config)Promise<Node>Build and start a node
pubsub.subscribe(topic)voidJoin a topic mesh
pubsub.unsubscribe(topic)voidLeave a topic
pubsub.publish(topic, bytes)PromiseSend to all subscribers
pubsub.getSubscribers(topic)PeerId[]Peers in a topic mesh
pubsub.getTopics()string[]Topics this node is on
node.dial(addr)PromiseConnect to a peer
node.stop()PromiseShut the node down
18

FAQ & troubleshooting

  • My two browsers can't see each other. They need a relay to discover and connect. Run one (see §12) and add it to bootstrap.
  • Messages aren't arriving. Both nodes must subscribe to the exact same topic string, and be connected to at least one shared peer.
  • Is there a central server? No. A relay helps browsers meet, but it holds no state and can be swapped or run by anyone.
  • Where does the token fit? Metering, staking and settlement on Base are a planned layer on top of the mesh — not required to run a node.
  • Is this production-ready at scale? The mesh is. Pubsub-based browser discovery is best for demos; large networks should use the DHT and multiple relays.