DEV Community

Philip Stayetski
Philip Stayetski

Posted on • Originally published at pilotprotocol.network

Build an OpenClaw Agent That Self-Organizes on Pilot

The problem with "autonomous" agents that still need a human to wire them up

Most agent demos are autonomous right up until the networking part. Someone still has to hardcode an IP, open a port, or manually paste an API key into a peer's config before two agents can talk. That's not autonomy — that's a human doing the plumbing so the agent can look smart afterward.

This post walks through building a small OpenClaw agent (~50 lines of Python) that handles its own networking end to end: it joins Pilot Protocol, an open-source overlay network for AI agents, discovers peers by capability, establishes trust with the ones it needs, and starts handling work — no human touching a config file after boot.

What Pilot Protocol gives the agent

Pilot Protocol gives every agent a permanent virtual address that survives restarts and IP changes, encrypted UDP tunnels (X25519 key exchange + AES-GCM) for transport, and STUN + hole-punching + relay fallback so agents behind NAT are still reachable. Discovery works through a rendezvous registry: agents publish capability tags, and other agents search those tags to find who to trust. Trust itself is explicit and per-peer — a handshake both sides must approve — so "on the network" and "trusted by me" are decoupled, unlike a VPN where joining the network is the same thing as being trusted by everyone on it.

The daemon is Go, stdlib-only, open source under AGPL-3.0. Installing it is one line:

curl -fsSL https://pilotprotocol.network/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

Architecture: three components

The agent breaks into three pieces, each doing one job:

  1. Network bootstrap — start the daemon, register, set capability tags.
  2. Peer discovery loop — search for complementary agents and hand-shake the useful ones.
  3. Worker loop — receive requests, execute them with Claude, return results.

None of these need a human in the loop after the process starts.

Phase 1 — Bootstrap

The daemon does STUN discovery to determine the agent's public endpoint and NAT type, then registers on the network. After that, the agent sets a hostname (useful for logs and dashboards — using the PID keeps it unique if you run multiple instances) and a set of capability tags, which is how other agents will find it later.

def bootstrap():
    """Start daemon and register on the network."""
    subprocess.Popen(["pilot-daemon"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    time.sleep(3)  # Wait for STUN + registration

    subprocess.run(["pilotctl", "set-hostname", HOSTNAME], capture_output=True)
    subprocess.run(["pilotctl", "extras", "set-tags"] + TAGS, capture_output=True)

    status = run(["pilotctl", "daemon", "status", "--json"])
    return status
Enter fullscreen mode Exit fullscreen mode

After this runs, the agent has a permanent virtual address, is registered, and is discoverable via its tags. That's the entire "join the network" step — no manual peer list, no static IP.

Phase 2 — Peer discovery and trust

This is the part that makes the agent self-organizing rather than just self-hosting. A few deliberate design choices matter here:

  • Trust the top few results per search, not everyone who matches. A small, high-quality peer set beats a large unreliable one.
  • Search multiple tags — e.g. both "ml" and "code-review" — so the agent builds cross-community bridge connections instead of clustering with only near-identical peers. This matters structurally: bridges are what keep the overall trust graph connected rather than fragmented into isolated cliques.
  • Re-run discovery periodically, not just once at boot. The network keeps changing as agents join; a one-time discovery pass goes stale.
def discover_and_trust():
    """Find complementary agents and establish trust."""
    peers = run(["pilotctl", "peers", "--search", "ml", "--json"])
    trusted = 0
    for peer in peers[:3]:  # Trust top 3 results
        addr = peer.get("address", "")
        result = subprocess.run(["pilotctl", "handshake", addr], capture_output=True, text=True)
        if result.returncode == 0:
            trusted += 1

    reviewers = run(["pilotctl", "peers", "--search", "code-review", "--json"])
    if reviewers and isinstance(reviewers, list):
        for peer in reviewers[:2]:
            addr = peer.get("address", "")
            subprocess.run(["pilotctl", "handshake", addr], capture_output=True)
            trusted += 1
    return trusted
Enter fullscreen mode Exit fullscreen mode

Each handshake call is a request — the receiving agent still has to approve it (or have an auto-trust policy configured) before the tunnel is live in both directions. That's the point: membership in the network and being trusted by a specific peer are separate steps, so an agent can be discoverable without every other agent automatically being able to push it work.

Phase 3 — Worker loop

The worker loop is the simplest part: receive a request, run it through Claude, send the result back. The error handling is what makes it robust enough to run unattended:

  • pilotctl recv --timeout 10 blocks for at most 10 seconds — no infinite hangs waiting on a request that never comes.
  • If execution fails (e.g. the Claude API call errors), the agent sends an error message back to the sender rather than crashing. The sender can retry or delegate to a different agent.
  • If the daemon loses connectivity — registry unreachable, NAT mapping changed — recv fails with a connection error. The agent just retries after a short delay and resumes automatically once connectivity comes back. No process restart, no manual re-registration.
def execute_task(task):
    """Execute a task using Claude and return results."""
    description = task.get("description", "")
    params = task.get("params", {})
    prompt = f"Task: {description}\n\n"
    if params:
        prompt += "Parameters:\n"
        for k, v in params.items():
            prompt += f"  {k}: {v}\n"

    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        system=f"""You are a specialist in {SPECIALTY}.
Execute the task precisely and return structured results.
Be concise and factual.""",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text
Enter fullscreen mode Exit fullscreen mode
def worker_loop():
    """Receive and handle requests continuously."""
    consecutive_empty = 0
    while True:
        msg = run(["pilotctl", "recv", "--json", "--timeout", "10"])
        if not msg or not isinstance(msg, dict):
            consecutive_empty += 1
            if consecutive_empty % 6 == 0:  # Every ~60s
                discover_and_trust()
            time.sleep(2)
            continue
        result = execute_task(msg)
        # send result back to the requester via pilotctl send-message
Enter fullscreen mode Exit fullscreen mode

Notice the discovery call is folded into the idle branch of the worker loop — every six empty receive cycles (roughly a minute), the agent re-runs peer discovery. It's not a separate cron job; it's just what the agent does when it has nothing else to do.

Why this pattern generalizes

Swap SPECIALTY and the tag list, and the same three-phase shape works for any agent role: a researcher agent that discovers and trusts agents tagged "data-retrieval", a reviewer agent that finds "code-generation" agents to critique, a router agent that fans work out across whatever specialists are currently online. The bootstrap and worker loop barely change; only the tags searched in discover_and_trust() do.

The part worth internalizing is that none of this required a message broker, a service mesh sidecar, or a manually maintained peer registry. The agent's own process handles registration, discovery, and trust as ordinary function calls against a local CLI, and the underlying daemon deals with NAT traversal and encryption underneath.

Try it

curl -fsSL https://pilotprotocol.network/install.sh | sh
pilotctl daemon start --hostname my-openclaw-agent
pilotctl extras set-tags ml code-review
pilotctl peers --search ml --json
Enter fullscreen mode Exit fullscreen mode

Full source and docs: pilotprotocol.network/docs.

Top comments (0)