A2A Outbound Adapter Example
Note: Use a unique agent
nameandseedwhen startingSingleA2AAdapterorMultiA2AAdapter. The uAgent address is derived from{name}_seedwhen you omitseed. Reusing a common name collides with other local agents.
Inbound vs outbound: this page is outbound — a uAgent that calls out to A2A HTTP specialists and exposes them on Agentverse via mailbox/chat. For the reverse (A2A clients calling into Agentverse uAgents), see the A2A Inbound Adapter Example.
The A2A Outbound Adapter connects uAgents to A2A HTTP servers. You can:
- Run specialist A2A servers (
a2a_servers) and a coordinating uAgent in one process - Route chat from ASI:One / Agentverse to those specialists
- Discover specialists via A2A Agent Cards (
/.well-known/agent-card.json)
Whether you wrap one specialist (SingleA2AAdapter) or several (MultiA2AAdapter), the adapter handles chat protocol, health checks, and HTTP forwarding.
Prerequisites
- Python 3.10+
- A Brave Search API key for the Brave example
- An Agentverse API key so
mailbox=Truecan register the uAgent - For
MultiA2AAdapterLLM scoring, an ASI1 API key (llm_api_key) - Background: uAgents Adapter guide
Features
Agent management
- Configure specialists with
A2AAgentConfigand start them witha2a_servers - Health-check Agent Cards at runtime (
MultiA2AAdapter) - Chat protocol on the uAgent (mailbox) plus A2A HTTP on separate ports
Routing (MultiA2AAdapter only)
The routing_strategy argument accepts two values. Anything else falls through to keyword matching:
routing_strategy | What the code does |
|---|---|
keyword_match (default) | Score specialties / keywords / skills. priority is a score multiplier, not a separate strategy. If llm_api_key is set, _route_by_keywords tries an LLM pick first, then falls back to keyword scores. |
round_robin | Rotate through healthy agents. |
any other string (including "llm" or "priority_based") | Not a dedicated path — same as keyword_match. |
There is no priority_based strategy string. Setting routing_strategy="llm" does not skip keyword scoring; LLM selection runs inside keyword routing when a key is present.
Reliability
- Health checking and Agent Card discovery
- Optional
fallback_executorwhen no specialist matches - Timeouts on A2A HTTP calls
Installation
Use a virtual environment. Pin both the adapter extra and a2a-sdk below 1.0. Adapter 0.6.2 imports A2AStarletteApplication from a2a.server.apps. That module was removed in a2a-sdk 1.x (v0.3 → v1.0 migration), so an unpinned install that resolves a2a-sdk==1.1.2 fails with ModuleNotFoundError: a2a.server has no attribute apps.
macOS / Linux:
python3 -m venv .venv
source .venv/bin/activate
pip install "uagents==0.25.5" "uagents-adapter[a2a-outbound]==0.6.2" "a2a-sdk[all,sql,sqlite]>=0.2.11,<1.0"
Windows (PowerShell):
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install "uagents==0.25.5" "uagents-adapter[a2a-outbound]==0.6.2" "a2a-sdk[all,sql,sqlite]>=0.2.11,<1.0"
Verified combo: uagents==0.25.5 + uagents-adapter[a2a-outbound]==0.6.2 + a2a-sdk 0.3.x (>=0.2.11,<1.0). After install, from a2a.server.apps import A2AStarletteApplication and from uagents_adapter import SingleA2AAdapter should succeed.
The extra name is a2a-outbound (not a2a-outbond and not a2a). A typo extra does not install a2a-sdk.
Class reference
There is no A2AAdapter class and no asi_api_key constructor argument. Export these from uagents_adapter (or uagents_adapter.a2a_outbound): SingleA2AAdapter, MultiA2AAdapter, A2AAgentConfig, a2a_servers.
A2AAgentConfig
Dataclass fields only (invalid kwargs such as a2a_port or executor_class raise TypeError):
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Specialist name; must match a key in the executors dict passed to a2a_servers |
description | str | required | Human-readable summary |
url | str | required | Base URL used for health checks (for example http://localhost:10020) |
port | int | required | Port for this specialist’s A2A HTTP server |
specialties | list[str] | required | Domains used for tags and keyword generation |
skills | list[str] | None | auto from specialties | Skill ids |
examples | list[str] | None | auto | Sample prompts |
keywords | list[str] | None | auto | Routing keywords |
priority | int | 1 | Keyword-score multiplier (higher prefers this agent) |
Executors are not fields on the config. Pass a separate dict[str, AgentExecutor] to a2a_servers.
from uagents_adapter import A2AAgentConfig
A2AAgentConfig(
name="research_specialist",
description="AI research specialist for research and analysis",
url="http://localhost:10020",
port=10020,
specialties=["research", "analysis", "fact-finding", "summarization"],
priority=2,
)
SingleA2AAdapter
Wraps one AgentExecutor. Starts its own A2A HTTP server on a2a_port (default 9999) and a uAgent on port (default 8000). Chat is forwarded to http://localhost:{a2a_port}.
| Parameter | Type | Default | Description |
|---|---|---|---|
agent_executor | AgentExecutor | required | Executor that handles A2A tasks |
name | str | required | uAgent name; default seed is {name}_seed |
description | str | required | uAgent description |
timeout | int | 90 | HTTP timeout (seconds) |
port | int | 8000 | uAgent HTTP port |
a2a_port | int | 9999 | Adapter’s A2A HTTP port (chat target) |
mailbox | bool | True | Agentverse mailbox |
seed | str | None | {name}_seed | uAgent seed |
agent_ports | list[int] | None | [] | Optional extra ports |
MultiA2AAdapter
Coordinates multiple A2AAgentConfig entries. llm_api_key is keyword-only and required (no default). Pass "" to skip LLM routing. Model default is asi1-mini (not "asi1"). Default base_url is https://api.asi1.ai/v1/chat/completions.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Coordinator uAgent name |
description | str | required | Coordinator description |
llm_api_key | str | required (keyword-only) | ASI1 (or compatible) API key; empty string disables LLM pick |
port | int | 8000 | uAgent port |
timeout | int | 90 | HTTP timeout |
mailbox | bool | True | Agentverse mailbox |
seed | str | None | {name}_seed | uAgent seed |
agent_configs | list[A2AAgentConfig] | None | [] | Specialists to discover |
fallback_executor | AgentExecutor | None | None | Used when routing finds no agent |
routing_strategy | str | "keyword_match" | keyword_match or round_robin only |
model | str | "asi1-mini" | Chat completions model |
base_url | str | ASI1 chat URL | LLM HTTP endpoint |
Methods: add_agent_config(config), run().
import os
from uagents_adapter import MultiA2AAdapter
coordinator = MultiA2AAdapter(
name="multi-specialist-coordinator",
description="Routes queries to A2A specialists",
llm_api_key=os.environ["ASI1_API_KEY"],
base_url="https://api.asi1.ai/v1/chat/completions",
model="asi1-mini",
port=8200,
mailbox=True,
agent_configs=agent_configs, # list[A2AAgentConfig]
routing_strategy="keyword_match",
)
coordinator.run()
a2a_servers(agent_configs, executors)
Starts one uvicorn thread per config. Each server uses config.port and executors[config.name]. This is independent of SingleA2AAdapter.a2a_port.
Why three ports in the Brave demo
A “single agent” Brave sample still starts two A2A HTTP servers plus one uAgent:
| Process | Port (this tutorial) | Role |
|---|---|---|
Specialist from a2a_servers | 10020 | A2A Agent Card for the Brave executor (A2AAgentConfig.port) |
SingleA2AAdapter A2A HTTP | 9999 | Chat target. The uAgent POSTs here (a2a_port default) |
| uAgent | 8200 | Chat protocol + mailbox (SingleA2AAdapter.port) |
Agentverse / ASI:One talk to the uAgent (mailbox / inspector on 8200). That uAgent does not call port 10020 for chat; it calls 9999. Port 10020 is the specialist card you can inspect or call as a raw A2A client.
Agent Card vs Agentverse
- A2A Agent Card is an HTTP discovery document. Prefer
http://localhost:PORT/.well-known/agent-card.json(replacePORTwith10020or9999). The adapter health-check uses that path first, then falls back to legacy/.well-known/agent.json. - Agentverse sees the uAgent after mailbox/Almanac registration. It does not scrape the A2A card to list the agent. Inspector URLs use the uAgent address and port 8200, not the A2A card URL.
File structure (Brave example)
Upstream folder: braveagent
braveagent/
function.py # orchestrator (same logic as main.py below)
brave/
agent.py # BraveSearchAgentExecutor
requirements.txt # see install pins below; do not use extra [a2a]
readme.md
.env # you create this
The GitHub tree currently has no main.py. Save the sample below as main.py, or run python function.py if you use the repo file as-is. Keep the filename you run in sync with these docs.
Recommended requirements.txt (the published example file still lists invalid uagents-adapter[a2a] and a broken a2a-sdk line — do not copy those):
uagents==0.25.5
uagents-adapter[a2a-outbound]==0.6.2
a2a-sdk[all,sql,sqlite]>=0.2.11,<1.0
python-dotenv>=1.0.0
Full system example: main.py
This matches the Brave orchestrator: A2AAgentConfig + a2a_servers + SingleA2AAdapter. Coordinator name is brave-search-coordinator (same as the example repo). Address is unique to your seed; do not copy inspector addresses from old screenshots.
from typing import Any, Dict, List
from uagents_adapter import A2AAgentConfig, SingleA2AAdapter, a2a_servers
from brave.agent import BraveSearchAgentExecutor
COORDINATOR_NAME = "brave-search-coordinator"
SPECIALIST_NAME = "brave_search_specialist"
SPECIALIST_PORT = 10020
UAGENT_PORT = 8200
class BraveSearchAgent:
def __init__(self) -> None:
self.coordinator = None
self.agent_configs: List[A2AAgentConfig] = []
self.executors: Dict[str, Any] = {}
self.running = False
def setup_agents(self) -> None:
print("Setting up Brave Search Agent")
self.agent_configs = [
A2AAgentConfig(
name=SPECIALIST_NAME,
description="AI Agent for web and news search using Brave Search API",
url=f"http://localhost:{SPECIALIST_PORT}",
port=SPECIALIST_PORT,
specialties=[
"web search",
"news",
"information retrieval",
"local business",
"site-specific lookup",
],
priority=3,
)
]
self.executors = {SPECIALIST_NAME: BraveSearchAgentExecutor()}
print("Brave Search Agent configuration created")
def start_individual_a2a_servers(self) -> None:
print("Starting Brave Search specialist server...")
a2a_servers(self.agent_configs, self.executors)
print("Brave Search specialist server started")
def create_coordinator(self) -> SingleA2AAdapter:
print("Creating Brave Coordinator...")
brave_executor = self.executors.get(SPECIALIST_NAME)
if brave_executor is None:
raise ValueError("BraveSearchAgentExecutor not found in executors dictionary.")
self.coordinator = SingleA2AAdapter(
agent_executor=brave_executor,
name=COORDINATOR_NAME,
description="Coordinator for routing Brave Search queries",
port=UAGENT_PORT,
mailbox=True,
)
print("Brave Coordinator created")
return self.coordinator
def start_system(self) -> None:
print("Starting Brave Search System")
try:
self.setup_agents()
self.start_individual_a2a_servers()
coordinator = self.create_coordinator()
self.running = True
print(f"Starting Brave coordinator on port {coordinator.port}...")
print(
f"Specialist Agent Card: http://localhost:{SPECIALIST_PORT}/.well-known/agent-card.json"
)
coordinator.run()
except KeyboardInterrupt:
print("Shutting down Brave Search system...")
self.running = False
def main() -> None:
BraveSearchAgent().start_system()
if __name__ == "__main__":
main()
Getting started
1. Clone the examples repo
git clone https://github.com/fetchai/innovation-lab-examples.git
cd innovation-lab-examples/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent
Copy the main.py listing above into this folder (or run python function.py).
2. Install dependencies
Do not run pip install "uagents-adapter[a2a]" or [a2a-outbond]. Use the Installation commands (or the recommended requirements.txt).
python3 -m venv .venv
source .venv/bin/activate
pip install "uagents==0.25.5" "uagents-adapter[a2a-outbound]==0.6.2" "a2a-sdk[all,sql,sqlite]>=0.2.11,<1.0" python-dotenv
3. Environment variables
Never commit real keys. Create .env in braveagent/:
BRAVE_API_KEY=your_brave_api_key_here
AGENTVERSE_API_KEY=your_agentverse_api_key_here
BraveSearchAgentExecutor needs BRAVE_API_KEY. mailbox=True needs Agentverse credentials (AGENTVERSE_API_KEY) so the uAgent can obtain a mailbox token and register on Almanac. For MultiA2AAdapter, also set ASI1_API_KEY and pass it as llm_api_key.
Load .env if your executor does not already:
from dotenv import load_dotenv
load_dotenv()
4. Run
python main.py
Expected listeners: specialist 10020, adapter A2A 9999, uAgent 8200.
5. Manifests and inspector
- Specialist card:
http://localhost:10020/.well-known/agent-card.json(legacy fallback:.../agent.json) - Adapter A2A card:
http://localhost:9999/.well-known/agent-card.json - Inspector: the process prints an Agentverse inspect URL with
uri=http://127.0.0.1:8200andaddress=set to your uAgent address. That address changes ifnameorseedchanges.
6. Chat

Use Chat with agent in the inspector (for example Find pizza restaurants near Central Park) or ASI:One against the mailbox uAgent.
Architecture

- User sends chat to the uAgent.
SingleA2AAdapterposts to its A2A server (a2a_port, default 9999).MultiA2AAdapterhealth-checks cards and forwards to the chosen specialisturl.- The executor returns text (or payment artifacts); the adapter sends a chat reply and an acknowledgement.
Expected output
When running the example, you should see output similar to:
🚀 Starting Brave Search System
🔧 Setting up Brave Search Agent
✅ Brave Search Agent configuration created
🔄 Starting Brave Search server...
🚀 Starting brave_search_specialist on port 10020
INFO: Started server process [78780]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:10020 (Press CTRL+C to quit)
⏳ Initializing servers...
✅ All A2A servers started!
✅ Brave Search server started!
🤖 Creating Brave Coordinator...
✅ Brave Coordinator created!
🎯 Starting Brave coordinator on port 8200...
AgentCard manifest URL: http://localhost:10020/.well-known/agent.json
🚀 Starting A2A Adapter for 'brave_coordin'
📡 A2A Server will run on port 9999
🤖 uAgent will run on port 8200
INFO: Started server process [78780]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:9999 (Press CTRL+C to quit)
INFO: [brave_coordin]: Starting agent with address: agent1qfs8ujwza9enrq926cncxttt2qglmwa3svp09dgn63wfkulzhjv56lh8tr9
INFO: [brave_coordin]: 🚀 A2A uAgent started at address: agent1qfs8ujwza9enrq926cncxttt2qglmwa3svp09dgn63wfkulzhjv56lh8tr9
INFO: [brave_coordin]: 🔗 A2A Server running on port: 9999
INFO: [brave_coordin]: Agent inspector available at https://agentverse.ai/inspect/?uri=http%3A//127.0.0.1%3A8200&address=agent1qfs8ujwza9enrq926cncxttt2qglmwa3svp09dgn63wfkulzhjv56lh8tr9
INFO: [brave_coordin]: Starting server on http://0.0.0.0:8200 (Press CTRL+C to quit)
INFO: [brave_coordin]: Starting mailbox client for https://agentverse.ai
INFO: [brave_coordin]: Manifest published successfully: AgentChatProtocol
INFO: [uagents.registration]: Registration on Almanac API successful
INFO: [uagents.registration]: Almanac contract registration is up to date!
INFO: [brave_coordin]: Mailbox access token acquired
INFO: [brave_coordin]: 📩 Received message from agent1qvj7rlfmwqq95unelgs2hnfd5w8swwr6endw9664a26lpfk8fftjydlkhyv: Find pizza restaurants near Central Park
INFO:httpx:HTTP Request: POST http://localhost:9999/ "HTTP/1.1 200 OK"
INFO: [brave_coordin]: 🤖 A2A Response: 🌐 Brave Search Agent - General Search
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 Query: Find pizza r...
INFO: [brave_coordin]: 📤 Sent response back to agent1qvj7rlfmwqq95unelgs2hnfd5w8swwr6endw9664a26lpfk8fftjydlkhyv
INFO: [brave_coordin]: ✅ Sent acknowledgment for message accdd830-7726-498b-8df9-4159ac3a17cd

Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
pip extra a2a-outbond / a2a | Extra does not exist | Install [a2a-outbound]==0.6.2 |
ModuleNotFoundError: a2a.server.apps | a2a-sdk 1.x | Pin a2a-sdk[all,sql,sqlite]>=0.2.11,<1.0 |
TypeError on A2AAgentConfig | a2a_port / executor_class | Use only documented fields; pass executors to a2a_servers |
NameError: A2AAdapter / missing asi_api_key | Fictional class | Use SingleA2AAdapter or MultiA2AAdapter + llm_api_key |
python main.py file not found | Repo ships function.py | Save main.py from this page or run python function.py |
| Mailbox / Almanac errors | Missing Agentverse key | Set AGENTVERSE_API_KEY |
| Brave executor errors | Missing search key | Set BRAVE_API_KEY |
| Address not matching a screenshot | Different name/seed | Expected; use the inspector URL from your logs |
| Port already in use | 10020, 9999, or 8200 taken | Stop the other process or change port / a2a_port |
References
- A2A Protocol
- uAgents
- uAgents Adapter source
- a2a-sdk v1 migration (breaking
a2a.server.apps) - ASI1 API
- Agentverse
- uAgents Adapter guide
- A2A Inbound Adapter Example
This pattern shows how outbound adapters expose A2A specialists through uAgent chat and mailbox.
Happy building!