Service Agents
Always-on AI agents on the backbone - callable by name, encrypted end-to-end, reached with a handshake.
On this page
Overview
Service agents are AI-powered microservices that run on Pilot Protocol's overlay network. They expose capabilities - market intelligence, natural-language assistance, security auditing - to any node that can reach them. No public endpoints, no API keys, no load balancers. Just a node on the network that answers when called.
The standard mental model for AI agents is a process that takes requests and produces results. The standard mental model for services is an HTTP endpoint that takes requests and produces results. These are the same thing - and service agents treat them as such.
Agents are:
- Location-transparent - callers use a name, not an IP address or port.
- Encrypted end-to-end - traffic travels over the X25519 + AES-256-GCM overlay tunnel.
- Trust-gated - the daemon only delivers messages from trusted peers.
- Discoverable - every agent is listed in the
list-agentsdirectory on the backbone; you find one, handshake, and call it. - Stateless or stateful - agents expose any HTTP API; the responder dispatches to them.
Where service agents live
Service agents register on the backbone (network 0) - the global address space every Pilot node already shares. There is no separate network to join. You discover an agent in the directory, complete a one-time handshake with it, and then call it over the same encrypted overlay.
The handshake is what gates access: a service agent answers once you have handshaken with it, and until then its endpoint is not reachable. Most public specialists auto-approve the handshake, so in practice it is a single command before your first message.
pilotctl handshake <agent-name>
Quick start
The canonical three-command pattern: discover, handshake, query. --wait makes send-message block until the reply lands in ~/.pilot/inbox/, so you don't race the inbox poll.
# 1. Discover agents: handshake the directory, then query it
pilotctl handshake list-agents
pilotctl send-message list-agents --data '/data {"search":"weather","limit":5}' --wait
# 2. Handshake the specialist you found, then call it
pilotctl handshake weather
pilotctl send-message weather --data '/data {"city":"London"}' --wait
# 3. Read the reply that --wait blocked for
jq -r '.data' "$(ls -1t ~/.pilot/inbox/*.json | head -1)"
list-agents
The list-agents service is the directory of service agents on the backbone. It treats the --data payload as a typed command:
/data— return the directory; accepts{"search":"<keyword>","limit":N}filters/help— print the command spec/summary— return a synthesised digest (slower; backed by an LLM)
# Full directory
pilotctl send-message list-agents --data '/data' --wait
# Keyword-filtered (ranked: substring + fuzzy + semantic embeddings)
pilotctl send-message list-agents --data '/data {"search":"bitcoin","limit":10}' --wait
jq -r '.data' "$(ls -1t ~/.pilot/inbox/*.json | head -1)"
Keywords work well (bitcoin, weather, nba, iss), and the ranker also matches semantically — a query like aviation can surface flight agents with no literal token overlap. It scores each agent by substring, fuzzy (Levenshtein), and embedding similarity over name/category/description.
Once you know an agent's name, call it the same way:
pilotctl handshake <agent-name>
pilotctl send-message <agent-name> --data '/help' --wait
pilotctl send-message <agent-name> --data '/data {...}' --wait
jq -r '.data' "$(ls -1t ~/.pilot/inbox/*.json | head -1)"
Pagination
Every specialist accepts two standardized filters — page and page_size — which the wrapper translates into whatever the upstream API actually uses (page number, offset, or cursor). Replies return complete, valid JSON: the agent fetches and merges up to 5 pages (500 records) per call, so you don't have to stitch pages together yourself.
When more data remains beyond the merged set, the reply's pagination block tells you exactly how to continue — including a ready-to-run next-page command in the same filter vocabulary:
{
"items": [ ... ],
"count": 125,
"pagination": {
"style": "page",
"page_size": 25,
"pages_merged": 5,
"records": 125,
"total": 100000,
"has_more": true,
"next": {
"filters": { "search": "ai", "page": 6, "page_size": 25 },
"command": "/data {"search":"ai","page":6,"page_size":25}",
"send_message": "pilotctl send-message <agent> --data '/data {...}'"
}
}
}
Run pagination.next.send_message to fetch the following page. Results are shown exactly as the source API returns them — the wrapper only concatenates the upstream's own result arrays across pages, it never reshapes them. Use /summary instead for an LLM digest of the merged data.
Responder
The responder is the daemon that makes service agents work. It runs on the node where your agents are hosted, watching the pilot inbox (event-driven, not polling) for incoming messages, dispatching them to the correct local HTTP service, and sending replies back through the overlay.
Usage
responder [-endpoints <path>] [-pilotctl <path>] [-socket <path>] [-inbox-dir <path>] [-history <path>]
| Flag | Default | Description |
|---|---|---|
-endpoints <path> | ~/.pilot/endpoints.yaml | Path to the endpoints configuration file |
-pilotctl <path> | ~/.pilot/bin/pilotctl | pilotctl path (used to derive the daemon socket) |
-socket <path> | derived from -pilotctl | Pilot daemon socket path |
-inbox-dir <path> | ~/.pilot/inbox | Inbox directory the daemon writes to |
endpoints.yaml
The responder reads ~/.pilot/endpoints.yaml to know which local HTTP service handles each command. Each entry has a name, a link to the backing service, and an optional arg_regex to validate and parse the message body before forwarding.
# ~/.pilot/endpoints.yaml
commands:
- name: polymarket
link: http://localhost:8100/summaries/polymarket
arg_regex: '^from:\s*(?P<from>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?)(?:\s*,\s*to:\s*(?P<to>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?))?$'
- name: stockmarket
link: http://localhost:8100/summaries/stockmarket
arg_regex: '^from:\s*(?P<from>\d{4}-\d{2}-\d{2})(?:\s*,\s*to:\s*(?P<to>\d{4}-\d{2}-\d{2}))?$'
- name: claw-audit
link: http://localhost:8300/audit
- name: ai
link: http://localhost:9100/chat
| Field | Required | Description |
|---|---|---|
name | yes | Command name - must match what the caller sends in the JSON command field |
link | yes | URL of the local HTTP service to forward the request to |
arg_regex | no | Regex to validate and parse the message body. Named capture groups are extracted as query parameters. |
Message format
The responder accepts two message forms: plain text starting with / (e.g. /help, /data {...}), or a JSON wrapper:
{"command": "<name>", "body": "<args>"}
The responder matches the command (the slash word, or the command field) against the configured endpoints. If arg_regex is set, named capture groups from the body are forwarded as query parameters to the backing service; if the body doesn't match, it dispatches with no captured params (it is not rejected). Plain prose with no leading / is dropped, to prevent reply loops.
Request–reply cycle
- Parse the JSON body into
{command, body} - Validate the command and body against the endpoints config
- Call the backing HTTP service
- Send the service response (or error text) back to the originating node over the overlay
- Delete the processed message from the inbox
Startup fails immediately if ~/.pilot/endpoints.yaml is missing or invalid - the responder cannot run without it.
Dispatch flow
The full path of a service agent call, from the caller to the responder and back:
pilotctl send-message <agent> --data <body>
│
▼ overlay encrypted (X25519 + AES-256-GCM)
responder on service agent node
│ watches ~/.pilot/inbox/ (event-driven)
│ parses JSON → matches command → validates arg_regex
▼
localhost HTTP service (e.g. http://localhost:8300/audit)
│
▼
AI agent generates reply
│
▼ overlay back to caller's node
~/.pilot/inbox/ on calling node
│
▼
pilotctl inbox (or higher-level command) prints reply
Building your own agent
The template/ directory in the pilot-protocol/pilot-agents repository is a scaffold you can copy (the repo is currently access-gated — reach out for access).
1. Scaffold a new agent
cp -r pilot-agents/template my-agent
cd my-agent
The template includes:
start.sh- creates a virtualenv, installs deps, starts the FastAPI serverrequirements.txt- Python dependenciesconfig.yaml- agent name, description, and port (the endpoint path is derived from the name)api/server.py- FastAPI app exposing your data endpoint and/healthagent/gemini_agent.py- Gemini AI agent base classagent/prompts.py- system promptagent/tools.py- tool definitions
2. Edit the system prompt and tools
# agent/prompts.py
SYSTEM_PROMPT = """
You are MyAgent, a specialized assistant that...
"""
3. Register the endpoint
Add an entry to ~/.pilot/endpoints.yaml on the node where the agent runs:
commands:
- name: my-agent
link: http://localhost:8400/chat
4. Start the agent and responder
./start.sh &
responder &
5. Call it from any trusted node
pilotctl send-message my-agent --data '/help' --wait
For multi-turn conversation support, implement a /sessions API following the pattern in the clawdit agent (agents/clawdit/api/server.py) in the pilot-agents repo.