What it is
A2A is the protocol external agents use to talk to AGNT. It's mounted at /a2a/v1, requires a Bearer token from an issued ApiKey, and exchanges a single pydantic envelope shape called AGPEnvelope. NemoClaw, Hermes-Kai, OpenClaw and other developer agents all speak this dialect.
The envelope
Every request body is an AGPEnvelope. Note the from_agent field is aliased to the wire name from because from is a Python keyword.
class AGPEnvelope(BaseModel):
"""Agent Gateway Protocol envelope — the single message shape on /a2a/v1."""
model_config = ConfigDict(populate_by_name=True)
protocol: str = "openclaw/a2a/v1"
from_agent: str = Field(..., alias="from")
to: str = "network:agnt"
intent: str
payload: dict[str, Any] = Field(default_factory=dict)
agent_context: dict[str, Any] = Field(default_factory=dict)
ttl_ms: int = 5000
timestamp: str = ""Fields
| Field | Type | Default | Purpose |
|---|---|---|---|
protocol | string | openclaw/a2a/v1 | Protocol version discriminator. |
from | string | required | Sender agent ID. Aliased from from_agent in Python. |
to | string | network:agnt | Destination. The network handles routing from here. |
intent | string | required | One of network.ping, booking.search, venue.list, booking.confirm. |
payload | object | {} | Intent-specific arguments. |
agent_context | object | {} | Free-form metadata the sender wants echoed. |
ttl_ms | integer | 5000 | Soft TTL hint for queues downstream. |
timestamp | ISO-8601 string | "" | Wall-clock timestamp at send. Optional on the wire. |
Supported intents
Four intents are currently dispatched:
| Intent | Tier | Handler | Purpose |
|---|---|---|---|
network.ping | free+ | handle_ping | Health + active venue count + tier echo. |
booking.search | free+ | handle_search | Full-text search over venues with optional location filter. Results cached 5 min. |
venue.list | free+ | handle_venue_list | List venues by category + location, up to 100. |
booking.confirm | pro / enterprise | handle_confirm | Create a VenueBooking row and a metered ApiKeyUsageEvent. Priced at $0.25 per call. |
Examples
network.ping
POST /a2a/v1 HTTP/1.1
Authorization: Bearer agnt_live_...
Content-Type: application/json
{
"protocol": "openclaw/a2a/v1",
"from": "nemoclaw:hello",
"to": "network:agnt",
"intent": "network.ping",
"payload": {},
"ttl_ms": 5000,
"timestamp": "2026-04-11T12:00:00Z"
}
HTTP/1.1 200 OK
{
"status": "connected",
"venues": 134,
"tier": "free",
"agent_id": "nemoclaw",
"network_version": "1.0"
}booking.search
POST /a2a/v1 HTTP/1.1
Authorization: Bearer agnt_live_...
Content-Type: application/json
{
"protocol": "openclaw/a2a/v1",
"from": "nemoclaw:hello",
"intent": "booking.search",
"payload": {
"query": "sunset dinner",
"location": "canggu",
"limit": 5
},
"ttl_ms": 5000,
"timestamp": "2026-04-11T12:00:00Z"
}
HTTP/1.1 200 OK
{
"venues": [
{
"id": "b9...",
"name": "La Brisa",
"slug": "la-brisa",
"category": "restaurant",
"location": "echo-beach",
"agent_id": "venue:la-brisa",
"is_live": true,
"referral_fee_pct": 0.07
}
],
"fee_pct": 7,
"expires_at": "2026-04-11T12:05:00Z",
"query": "sunset dinner"
}Envelope state machine
Internal AGNT bookings that route through ClawPulse carry an EnvelopeStatus that transitions through a strict state machine. Illegal transitions raise ValueError at the application boundary before they hit the DB check constraint.
class EnvelopeStatus(StrEnum):
PENDING = "pending"
DISPATCHED = "dispatched"
DISPATCH_FAILED = "dispatch_failed"
ACCEPTED = "accepted"
REJECTED = "rejected"
COMPLETED = "completed"
NO_SHOW = "no_show"
EXPIRED = "expired"Legal transitions
ENVELOPE_TRANSITIONS: dict[EnvelopeStatus, frozenset[EnvelopeStatus]] = {
EnvelopeStatus.PENDING: frozenset({EnvelopeStatus.DISPATCHED, EnvelopeStatus.EXPIRED}),
EnvelopeStatus.DISPATCHED: frozenset({
EnvelopeStatus.ACCEPTED,
EnvelopeStatus.REJECTED,
EnvelopeStatus.DISPATCH_FAILED,
}),
EnvelopeStatus.DISPATCH_FAILED: frozenset({EnvelopeStatus.DISPATCHED}), # allow retry
EnvelopeStatus.ACCEPTED: frozenset({EnvelopeStatus.COMPLETED, EnvelopeStatus.NO_SHOW}),
# Terminal — no outgoing transitions
EnvelopeStatus.REJECTED: frozenset(),
EnvelopeStatus.COMPLETED: frozenset(),
EnvelopeStatus.NO_SHOW: frozenset(),
EnvelopeStatus.EXPIRED: frozenset(),
}REJECTED, COMPLETED, NO_SHOW, and EXPIREDare terminal — once an envelope lands in any of those states it never moves again. DISPATCH_FAILEDis special: it's the only error state that allows a retry back to DISPATCHED.
Authentication
Every request must present a Bearer token scoped to an ApiKey row. middleware/agent_auth.py (verify_agent_key) resolves the token, records last_used_at, and returns the key. Tier enforcement happens in the handler: booking.confirm raises 403 if the key is not pro or enterprise.
Metrics
Every dispatch increments the a2a_requests_total Prometheus counter with labels {intent, tier, status}. Monitor the status=error slice and the a2a_metered_events_total counter to detect billing drift.
Public status endpoint
GET /a2a/v1/status is unauthenticated and returns the current venue count, the number of agents active in the last 5 minutes, and the network version. External agents use it as a cheap health check before opening a session.