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.
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
Ed25519peer 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.
Installation
hy:gg is plain js-libp2p plus gossipsub. Install the packages you need:
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:
# browser npm install @libp2p/websockets @libp2p/webrtc @libp2p/circuit-relay-v2 # node npm install @libp2p/tcp
Quickstart
A node that joins the mesh, subscribes to a topic, and prints what it hears — in one file.
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())
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.
Joining the mesh
A fresh node knows no one. Give it a few bootstrap peers to dial, and discovery widens its view from there.
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.
Subscribing to topics
Subscribe by name. From that moment the node grafts a mesh for the topic and starts receiving.
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')
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.
const msg = { from: 'planner', kind: 'task.request', task: 'summarize' } await node.services.pubsub.publish( 'agents/tasks', new TextEncoder().encode(JSON.stringify(msg)) )
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.
{
"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.
Handling peers
React to the mesh changing under you. libp2p emits connection and subscription events.
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).
Browser vs Node
Same protocol, different transports. Servers dial over TCP; browsers can't, so they use WebSockets and WebRTC.
| Concern | Node | Browser |
|---|---|---|
| Transport | TCP, WebSockets | WebSockets, WebRTC |
| Listen addr | Yes | No (dial-only) |
| Discovery | Bootstrap, DHT, mDNS | Bootstrap + pubsub relay |
| Needs a relay | No | Usually yes |
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.
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 } })
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.
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.
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.
strictSigning) is on by default. Leave it on unless every peer is already trusted at a lower layer.Network parameters
Gossipsub defaults hy:gg ships with. Tune per deployment.
| Parameter | Symbol | Default | Meaning |
|---|---|---|---|
| Mesh degree | D | 6 | Target peers per topic mesh |
| Mesh low | D_lo | 4 | Graft when below |
| Mesh high | D_hi | 12 | Prune when above |
| Lazy gossip | D_lazy | 6 | Peers gossiped per heartbeat |
| Heartbeat | — | 1 s | Maintenance interval |
| Fanout TTL | — | 60 s | Lifetime of fanout peers |
| History | — | 5 | Heartbeats of message cache |
| Max RPC | — | 65,536 B | Max payload per RPC |
| Protocol | — | /meshsub/1.1.0 | Falls back to 1.0.0 |
API reference
The surface you will touch most. Full API in the js-libp2p docs.
| Method | Returns | Description |
|---|---|---|
| createLibp2p(config) | Promise<Node> | Build and start a node |
| pubsub.subscribe(topic) | void | Join a topic mesh |
| pubsub.unsubscribe(topic) | void | Leave a topic |
| pubsub.publish(topic, bytes) | Promise | Send to all subscribers |
| pubsub.getSubscribers(topic) | PeerId[] | Peers in a topic mesh |
| pubsub.getTopics() | string[] | Topics this node is on |
| node.dial(addr) | Promise | Connect to a peer |
| node.stop() | Promise | Shut the node down |
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
subscribeto 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.