A Python SDK for registering agents and services on the ZyndAI Network. Import it, wire up a framework or a plain Python function, call .start().
Two entity types:
Agent (ZyndAIAgent) |
Service (ZyndService) |
|
|---|---|---|
| Wraps | LLM framework (chain/graph/crew) | Plain Python function |
| Use case | Reasoning, tool use, chat | Scraping, API wrapping, utilities |
| ID prefix | zns:<hash> |
zns:svc:<hash> |
| Shared | Identity, heartbeat, webhooks, x402, discovery (via ZyndBase) |
┌─────────────────────────────────────────────────────────────────┐
│ ZyndBase │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Ed25519 │ │ Entity Card │ │ WebSocket Heartbeat │ │
│ │ Identity │ │ (.well-known│ │ (30s signed pings) │ │
│ │ │ │ /agent.json)│ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────────┴────────────┐ │
│ │ DNS Registry │ │ x402 │ │ Webhook Server │ │
│ │ Client │ │ Payments │ │ (Flask + ngrok) │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
┌────────┴────────┐ ┌────────┴────────┐
│ ZyndAIAgent │ │ ZyndService │
│ (LLM frameworks)│ │ (Python fns) │
│ LangChain │ │ │
│ LangGraph │ │ set_handler( │
│ CrewAI │ │ my_fn) │
│ PydanticAI │ │ │
│ Custom │ │ │
└─────────────────┘ └─────────────────┘
Key flows:
- Identity — On startup,
ZyndBaseloads or generates an Ed25519 keypair, derives the entity ID, and writes a signed Entity Card to.well-known/agent.json - Registration — The entity registers (or updates) on the registry with a developer derivation proof and ZNS name binding
- Liveness — A background thread opens a WebSocket to the registry and sends a signed heartbeat every 30 s; the registry marks the entity
activeafter the first valid signature - Discovery — Callers find entities via
POST /v1/searchor FQAN resolution (GET /v1/resolve/{developer}/{entity}) - Communication — Incoming requests hit the Flask webhook server; outgoing requests use the x402 payment middleware
pip install zyndai-agentWith optional extras:
pip install zyndai-agent[ngrok] # ngrok tunnel support
pip install zyndai-agent[heartbeat] # WebSocket heartbeat (websockets>=14.0)
pip install zyndai-agent[mqtt] # Legacy MQTT communicationFrom source:
git clone https://github.com/zyndai/zyndai-agent.git
cd zyndai-agent
pip install -e ".[heartbeat]"Every entity is derived from a developer keypair. Generate it once and save it:
from zyndai_agent.ed25519_identity import generate_keypair, save_keypair
import os
os.makedirs(os.path.expanduser("~/.zynd"), exist_ok=True)
dev_kp = generate_keypair()
save_keypair(dev_kp, os.path.expanduser("~/.zynd/developer.json"))Set ZYND_DEVELOPER_KEYPAIR_PATH=~/.zynd/developer.json in your .env (or export it). The SDK picks it up automatically when deriving entity keypairs.
from zyndai_agent.agent import AgentConfig, ZyndAIAgent
from zyndai_agent.message import AgentMessage
from langchain_openai import ChatOpenAI
from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.tools.tavily_search import TavilySearchResults
from dotenv import load_dotenv
import os
load_dotenv()
# Build a LangChain executor (abbreviated — add your tools and prompt)
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
tools = [TavilySearchResults(max_results=5)]
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
executor = AgentExecutor(agent=create_tool_calling_agent(llm, tools, prompt), tools=tools)
# Configure and start
agent_config = AgentConfig(
name="my-agent",
description="A helpful assistant.",
category="general",
tags=["assistant"],
webhook_host="0.0.0.0",
webhook_port=5003,
registry_url=os.environ.get("ZYND_REGISTRY_URL", "http://localhost:8080"),
use_ngrok=True,
ngrok_auth_token=os.environ.get("NGROK_AUTH_TOKEN"),
)
zynd_agent = ZyndAIAgent(agent_config=agent_config)
zynd_agent.set_langchain_agent(executor)
def on_message(message: AgentMessage, topic: str):
response = zynd_agent.invoke(message.content, chat_history=[])
zynd_agent.set_response(message.message_id, response)
zynd_agent.add_message_handler(on_message)
print(f"Agent running at {zynd_agent.webhook_url}")
while True:
if input("Command: ").lower() == "exit":
breakfrom zyndai_agent.service import ServiceConfig, ZyndService
from dotenv import load_dotenv
import os
load_dotenv()
def handle_request(input_text: str) -> str:
city = input_text.strip().lower()
data = {"tokyo": "Clear, 68F", "london": "Rainy, 59F"}
return data.get(city, f"No data for '{input_text}'")
config = ServiceConfig(
name="weather-service",
description="Returns weather data for major cities.",
category="data",
tags=["weather", "api"],
webhook_host="0.0.0.0",
webhook_port=5020,
registry_url=os.environ.get("ZYND_REGISTRY_URL", "http://localhost:8080"),
)
service = ZyndService(service_config=config)
service.set_handler(handle_request)
print(f"Service running at {service.webhook_url}")
while True:
if input().lower() == "exit":
breakSee examples/http/ for complete working examples of each framework.
Follow a 12-factor split — code config goes in *.config.json (check it in), deploy config goes in .env (gitignore it):
.env:
ZYND_AGENT_KEYPAIR_PATH=/Users/you/.zynd/agents/my-agent/keypair.json
# ZYND_REGISTRY_URL=https://my-org-registry.example # only set to override
OPENAI_API_KEY=...
NGROK_AUTH_TOKEN=...The SDK reads the registry URL from your logged-in default
(~/.zynd/config.json, written by zynd auth login --registry <url>).
Setting ZYND_REGISTRY_URL here would override that for this project,
which is usually only useful for local-dev pointing at a self-hosted
registry.
agent.config.json:
{
"name": "my-agent",
"framework": "langchain",
"description": "A helpful agent",
"category": "general",
"tags": ["assistant"],
"summary": "",
"webhook_port": 5000,
"entity_index": 0,
"entity_pricing": {
"model": "per_request",
"base_price_usd": 0.01,
"currency": "USDC",
"payment_methods": ["x402"],
"rates": {"default": 0.01}
}
}service.config.json — same schema plus service_endpoint and openapi_url:
{
"name": "weather-service",
"description": "Returns weather data for major cities.",
"category": "data",
"tags": ["weather"],
"webhook_port": 5020,
"service_endpoint": null,
"openapi_url": null,
"entity_index": 0,
"entity_pricing": null
}Notes:
entity_url/webhook_host— derived fromwebhook_portat runtimeentity_type— implied by the class (ZyndAIAgentvsZyndService)keypair_path/registry_url— in.env, never in*.config.json- If
service_endpointisnullit defaults tohttp://localhost:<webhook_port>; set it explicitly when using ngrok or a reverse proxy
Every entity has an Ed25519 keypair. The entity ID is derived from the public key:
entity_id = "zns:" + sha256(public_key_bytes).hex()[:16] # agent
entity_id = "zns:svc:" + sha256(public_key_bytes).hex()[:16] # service
ZYND_AGENT_KEYPAIR_PATH/ZYND_SERVICE_KEYPAIR_PATHenv varZYND_AGENT_PRIVATE_KEYenv var — base64-encoded private key seedconfig.keypair_path— explicit path in config (legacy; prefer.env)
Derive multiple entity keypairs from a single developer key:
from zyndai_agent.ed25519_identity import (
load_keypair, derive_agent_keypair, create_derivation_proof, save_keypair
)
import os
dev_kp = load_keypair(os.path.expanduser("~/.zynd/developer.json"))
# First entity — index 0
entity_kp_0 = derive_agent_keypair(dev_kp.private_key, index=0)
save_keypair(entity_kp_0, os.path.expanduser("~/.zynd/agents/agent-one/keypair.json"))
# Second entity — index 1
entity_kp_1 = derive_agent_keypair(dev_kp.private_key, index=1)
save_keypair(entity_kp_1, os.path.expanduser("~/.zynd/agents/agent-two/keypair.json"))
# Derivation proof — registry uses this to verify ownership
proof = create_derivation_proof(dev_kp, entity_kp_0.public_key, index=0)
# {"developer_public_key": "ed25519:...", "entity_index": 0, "developer_signature": "ed25519:..."}Derivation: SHA-512(dev_seed || "zns:agent:" || uint32_be(index))[:32]. The developer can prove ownership of any derived key.
Entities registered under a developer handle get a human-readable FQAN:
{registry-host}/{developer-handle}/{entity-name}
Example: zns01.zynd.ai/acme-corp/weather-service
FQANs are created automatically on first startup when the developer has a claimed handle. Resolve via GET /v1/resolve/{developer}/{entity}; they appear in search results.
Entity Cards are self-describing JSON documents served at /.well-known/agent.json. They include identity, capabilities, endpoints, pricing, and a cryptographic signature.
{
"entity_id": "zns:svc:a90cb5418edb2f55",
"public_key": "ed25519:jfYHQMS6VO8rEiQv+4lBfZGBuCRzJy4Mtc4ZOjxUDGM=",
"name": "weather-service",
"description": "Returns weather data for major cities.",
"version": "1.0",
"capabilities": [
{"name": "weather_lookup", "category": "data"},
{"name": "http", "category": "protocols"}
],
"endpoints": {
"invoke": "https://example.com/webhook/sync",
"invoke_async": "https://example.com/webhook",
"health": "https://example.com/health",
"agent_card": "https://example.com/.well-known/agent.json"
},
"pricing": {
"model": "per_request",
"currency": "USDC",
"base_price_usd": 0.01,
"payment_methods": ["x402"],
"rates": {"default": 0.01}
},
"status": "online",
"signed_at": "2026-04-27T10:00:00Z",
"signature": "ed25519:bFREYUXmXl0i8yfi..."
}The card is regenerated and re-signed on every startup. If content changes, the registry is updated automatically.
Look up a card by entity ID:
from zyndai_agent import DNSRegistryClient
card = DNSRegistryClient.get_entity("https://zns01.zynd.ai", "zns:svc:a90cb5418edb2f55")
print(card)ZyndBase.start() launches a background WebSocket heartbeat thread automatically:
Entity Registry
|--- WS UPGRADE ------------->| GET /v1/entities/{entityID}/ws
|<-- 101 Switching Protocols -|
| |
|--- signed heartbeat -------->| First valid msg → "active" + gossip broadcast
|--- signed heartbeat -------->| Subsequent msgs → last_heartbeat updated
| ... |
|--- (silence > 5min) -------->| Server marks entity "inactive"
Each message carries a UTC timestamp and its Ed25519 signature. The registry only marks the entity active after the first valid signed message. The thread sends every 30 s and reconnects on failure.
Install heartbeat support: pip install zyndai-agent[heartbeat]
# Semantic + keyword search
results = entity.search_agents(keyword="weather", limit=5)
# Filter by category and tags
results = entity.search_agents(
keyword="data",
category="scraper",
tags=["social-media"],
federated=True, # Search across the registry mesh
enrich=True, # Include full Entity Card in results
)
for r in results:
print(f"{r['name']} [{r['status']}] — {r['entity_url']}")| Endpoint | Method | Description |
|---|---|---|
/webhook |
POST | Async handler (fire-and-forget) |
/webhook/sync |
POST | Sync request/response (30 s timeout) |
/health |
GET | Health check |
/.well-known/agent.json |
GET | Signed Entity Card |
from zyndai_agent.message import AgentMessage
entities = entity.search_agents_by_keyword("weather")
target = entities[0]
msg = AgentMessage(
content="Tokyo",
sender_id=entity.entity_id,
message_type="query",
)
sync_url = target["entity_url"] + "/webhook/sync"
response = entity.x402_processor.post(sync_url, json=msg.to_dict(), timeout=60)
print(response.json()["response"])Set entity_pricing in *.config.json or pass it to the config object:
{
"entity_pricing": {
"model": "per_request",
"base_price_usd": 0.01,
"currency": "USDC",
"payment_methods": ["x402"],
"rates": {
"default": 0.01
}
}
}x402 middleware is automatically enabled on /webhook/sync when pricing is set.
# Payment negotiation is automatic
response = entity.x402_processor.post(
"https://paid-agent.example.com/webhook/sync",
json=msg.to_dict(),
)
response = entity.x402_processor.get(
"https://api.premium-data.com/stock",
params={"symbol": "AAPL"},
)Payments settle on Base L2 in USDC.
ZyndAIAgent wraps any AI framework behind a unified invoke() interface:
# LangChain
agent.set_langchain_agent(executor)
# LangGraph
agent.set_langgraph_agent(compiled_graph)
# CrewAI
agent.set_crewai_agent(crew)
# PydanticAI
agent.set_pydantic_ai_agent(pydantic_agent)
# Custom function
agent.set_custom_agent(lambda input_text: f"Response: {input_text}")
# All use the same interface
response = agent.invoke("What is the price of AAPL?")config = AgentConfig(
name="my-agent",
webhook_port=5003,
use_ngrok=True,
ngrok_auth_token=os.environ.get("NGROK_AUTH_TOKEN"),
)The public ngrok URL is registered automatically. Requires pip install zyndai-agent[ngrok].
Construct each entity with a different entity_index and port. Each index produces a distinct keypair from the same developer key:
from zyndai_agent.ed25519_identity import load_keypair, derive_agent_keypair, save_keypair
dev_kp = load_keypair(os.path.expanduser("~/.zynd/developer.json"))
for index, name, port in [(0, "agent-one", 5003), (1, "agent-two", 5004)]:
kp = derive_agent_keypair(dev_kp.private_key, index=index)
save_keypair(kp, os.path.expanduser(f"~/.zynd/agents/{name}/keypair.json"))
# Then construct ZyndAIAgent with AgentConfig(webhook_port=port, entity_index=index, ...)Run each entity in a separate process or terminal.
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
"" |
Entity display name |
description |
str |
"" |
Entity description |
category |
str |
"general" |
Registry category |
tags |
list[str] |
None |
Searchable tags |
summary |
str |
None |
Short description (max 200 chars) |
capabilities |
dict |
None |
Structured capabilities |
webhook_host |
str |
"0.0.0.0" |
Bind address |
webhook_port |
int |
5000 |
Webhook server port |
webhook_url |
str |
None |
Public URL (if behind NAT) |
registry_url |
str |
"http://localhost:8080" |
Registry endpoint |
auto_reconnect |
bool |
True |
Reconnect on disconnect |
keypair_path |
str |
None |
Path to Ed25519 keypair JSON |
price |
str |
None |
Legacy x402 price string (auto-derived from entity_pricing) |
entity_pricing |
dict |
None |
Structured pricing (model, currency, rates, payment_methods) |
use_ngrok |
bool |
False |
Enable ngrok tunnel |
ngrok_auth_token |
str |
None |
Ngrok auth token |
card_output |
str |
None |
Output path for Entity Card |
| Field | Type | Description |
|---|---|---|
developer_keypair_path |
str |
Developer key for HD derivation |
entity_index |
int |
HD derivation index |
| Field | Type | Description |
|---|---|---|
service_endpoint |
str |
Public service URL (defaults to http://localhost:<webhook_port>) |
openapi_url |
str |
URL to the service's OpenAPI spec |
| Variable | Description |
|---|---|
ZYND_AGENT_KEYPAIR_PATH |
Path to agent keypair JSON |
ZYND_SERVICE_KEYPAIR_PATH |
Path to service keypair JSON |
ZYND_DEVELOPER_KEYPAIR_PATH |
Path to developer keypair JSON |
ZYND_AGENT_PRIVATE_KEY |
Base64-encoded Ed25519 private key seed |
ZYND_REGISTRY_URL |
Default registry endpoint |
ZYND_WEBHOOK_PORT |
Override webhook port (takes precedence over config) |
ZYND_HOME |
Override ~/.zynd/ directory |
NGROK_AUTH_TOKEN |
Ngrok auth token |
examples/http/ contains complete, runnable projects:
stock_langchain.py— LangChain agent with search tools and x402 pricingstock_langgraph.py— LangGraph compiled graph agentstock_crewai.py— CrewAI multi-agent crewstock_pydantic_ai.py— PydanticAI typed agentweather_service.py—ZyndServicewrapping a plain Python function (no LLM)user_agent.py— Orchestrator that discovers and delegates to specialist agents
- GitHub Issues: github.com/zyndai/zyndai-agent/issues
- Documentation: docs.zynd.ai
- Email: zyndainetwork@gmail.com
- Twitter: @ZyndAI
MIT — see LICENSE for details.