Elo Node — P2P Message Mesh for AI Agents
Zero infrastructure. One process. One TCP port. One ed25519 key.
Elo is a decentralized P2P message mesh for communication between AI agents. No central server, no Kafka, no Redis, no NATS. Just direct TCP between nodes.
pip install elo-node
import asyncio
from elo import Node
async def main():
node = Node("my-agent", port=7878, peers=["TRACKER_IP:7878"])
await node.connect()
await node.register(agents=["analyst"], tools=["web-search"])
@node.on_task
async def handle(task):
return {"result": f"processed by {node.node_id}"}
await node.run()
asyncio.run(main())
Features
- Decentralized P2P — discovery via public tracker or Kademlia DHT
- ed25519 signatures — cryptographic identity, authenticated messages
- Capabilities — publish/subscribe of agent skills across the mesh
- Relay via tracker — nodes behind NAT/Docker communicate through a tracker
- Zero infra — no Kafka, Redis, NATS, or central server
- Native CLI —
python -m elo serve,status,init,id
CLI
python -m elo status # Node ID, hash, keys
python -m elo id # Just the node_id
python -m elo pubkey # Public key (hex + b64)
python -m elo init # Generate persistent identity
python -m elo serve # Start an interactive node
Quick Start
1. Connect to a tracker
⚠️ peers= is required for outbound connections. Without it the node only listens.
node = Node("agent-1", port=7878, peers=["TRACKER_IP:7878"])
await node.connect()
On HELLO handshake with a tracker (v0.4.4+), known peers are automatically exchanged — new nodes are discovered on connect.
2. Register capabilities
await node.register(agents=["ping", "analyst"], tools=["web-search"])
3. Send tasks
# Auto-fallback: direct → InterestTable → QUERY → tracker relay → NO_PEER
result = await node.send_task("", "ping", {"msg": "hello"})
# → Result(status="success", payload={...})
4. Discover peers
# Peers with full handshake
known = node.get_known_peers()
# Broadcast QUERY across the mesh (returns node_id + addresses)
peers = await node.discover_peers_network(timeout=5.0)
# Legacy (local-only, deprecated)
local = await node.discover_peers()
5. Relay through tracker (NAT/Docker)
# Explicit relay for NAT'd nodes
result = await node.send_task_via_tracker(
tracker_node="",
target="sauron-elo",
capability="ping",
payload={"msg": "hello"},
)
Architecture
┌──────────────────┐ TCP/JSON ┌──────────────────┐
│ Node A │◄──────────────►│ Node B │
│ ed25519 key │ │ ed25519 key │
│ Capabilities │ │ Capabilities │
│ Interests │ │ Interests │
└──────────────────┘ └──────────────────┘
│ │
│ Tracker (optional) │
└────────────── DHT ───────────────┘
Each node:
- Generates an ed25519 identity on first run
- Listens on a TCP port
- Announces capabilities (e.g. "analyst", "web-search")
- Discovers other nodes via shared tracker or manual peers
- Exchanges signed messages (tasks, results, events)
Private Tracker
For 3+ nodes or nodes behind NAT/Docker (no public port), use a private tracker:
- Pick an always-online node as tracker (e.g. SAM at
TRACKER_IP:7878) - Run
examples/python/tracker.pyon it - All other nodes connect via
peers=["<tracker-ip>:7878"] - The tracker relays tasks between nodes automatically
# Client node
node = Node("my-node", port=0, peers=["TRACKER_IP:7878"])
Stale peer prevention (v0.4.10)
When a node restarts (Docker, VM reboot) with the same identity but a different port, the tracker previously kept the stale entry — tasks could fail with "Peer not connected".
Fixed in v0.4.10: the tracker now removes any existing peer entry matching the same node_id prefix on HELLO, before registering the new connection. Identity stays persistent (ed25519 key), port stays dynamic — both preserved.
Changelog
| Version | Highlights |
|---|---|
| 0.4.10 | Stale peer fix — tracker removes old entry on reconnection with same identity, different port |
| 0.4.9 | node_id included in discover_peers_network() response |
| 0.4.8 | send_task() routing fix — correctly falls back to tracker relay when target is offline |
| 0.4.7 | Rate limiting, result signing, broadcast loop fix |
| 0.4.6 | Documentation update, __version__ fix |
| 0.4.5 | Multi-response discover, tracker returns all peers on query |
| 0.4.4 | HELLO_ACK with known_peers, send_task auto-fallback tracker |
| 0.4.3 | Relay via tracker, send_task_via_tracker() |
| 0.4.2 | discover_peers() fix, get_known_peers() API |
| 0.4.0 | Initial release |
Compatibility
- Python 3.11+
- Linux, macOS, Windows
Development
git clone https://github.com/andreocc/elo
cd elo/py
pip install -e ".[dev]"
pytest
Related Projects
- Hermes Agent — autonomous agent runtime
- Honcho — persistent memory for agents
Metadata
Release files for elo-node 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| elo_node-0.6.0.tar.gz | 33.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elo_node-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.7 kB
Release files / elo_node-0.6.0.tar.gz
| Download URL | elo_node-0.6.0.tar.gz |
|---|---|
| Size | 33.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
39a04dfb7ab60d2c1501d9a77be257b62cb2e4ba5499e1679b472e2aa5e8f383
|
|
BLAKE2b-256 checksum How to use checksums |
d7660495d2cd67bee742ebef6d16118bc9394b8c721914c8f7f43ed1373c1590
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|
Release files / elo_node-0.6.0-py3-none-any.whl
| Download URL | elo_node-0.6.0-py3-none-any.whl |
|---|---|
| Size | 30.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e847cb57d5c93cb5e9721e0165d8d0487690068b892166a3947090f44792f656
|
|
BLAKE2b-256 checksum How to use checksums |
589fdb0d148a6ed8c53a9a0e8b190a7828ec8ce28b48fba59c6811e2b8248ce7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|