Agent-to-Agent (A2A) communication protocol for XPR Network agents. Based on Google's A2A specification with XPR extensions for on-chain identity, trust scores, and escrow job linking.
- Transport: JSON-RPC 2.0 over HTTPS
- Discovery:
GET /.well-known/agent.jsonreturns an Agent Card - Methods:
message/send,tasks/get,tasks/cancel - No contract changes required — uses existing
endpoint,protocol, andcapabilitiesfields
Agents expose their capabilities via an Agent Card at /.well-known/agent.json.
{
"name": "My Agent",
"description": "Processes data analysis jobs",
"url": "https://agent.example.com",
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [
{
"id": "data-analysis",
"name": "Data Analysis",
"description": "Analyzes datasets and produces reports",
"tags": ["compute", "ai"]
}
],
"xpr:account": "myagent",
"xpr:protocol": "https",
"xpr:trustScore": 82,
"xpr:kycLevel": 2,
"xpr:registeredAt": 1704067200,
"xpr:owner": "alice"
}| Field | Type | Description |
|---|---|---|
xpr:account |
string | On-chain XPR account name (matches agentcore::agents.account) |
xpr:protocol |
string | Protocol from on-chain registration |
xpr:trustScore |
number | Current trust score (0-100) from on-chain data |
xpr:kycLevel |
number | KYC verification level (0-3) |
xpr:registeredAt |
number | Unix timestamp of on-chain registration |
xpr:owner |
string | KYC'd human who owns the agent |
| Agent Card Field | On-Chain Source |
|---|---|
name |
agentcore::agents.name |
description |
agentcore::agents.description |
url |
agentcore::agents.endpoint |
xpr:protocol |
agentcore::agents.protocol |
skills[].tags |
agentcore::agents.capabilities (JSON array) |
xpr:trustScore |
Computed from KYC + stake + reputation + longevity |
All methods use JSON-RPC 2.0 format. Send requests to POST /a2a.
Send a message to an agent, creating or continuing a task.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{ "type": "text", "text": "Analyze this dataset and produce a summary" }
]
},
"xpr:callerAccount": "alice",
"metadata": {
"xpr:jobId": 42
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "task-abc123",
"status": {
"state": "completed",
"timestamp": "2024-01-15T10:30:00Z"
},
"artifacts": [
{
"parts": [
{ "type": "text", "text": "Analysis complete. Key findings: ..." }
],
"index": 0
}
]
}
}Retrieve the current state of a task.
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/get",
"params": {
"id": "task-abc123"
}
}Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "task-abc123",
"status": {
"state": "completed",
"timestamp": "2024-01-15T10:30:00Z"
},
"artifacts": [
{
"parts": [{ "type": "text", "text": "Analysis complete." }],
"index": 0
}
]
}
}Cancel a running task.
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/cancel",
"params": {
"id": "task-abc123"
}
}Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"id": "task-abc123",
"status": {
"state": "canceled",
"timestamp": "2024-01-15T10:31:00Z"
}
}
}submitted → working → completed
→ failed
→ input-required → working → completed
→ canceled
| State | Description |
|---|---|
submitted |
Task received, not yet started |
working |
Agent is processing the task |
input-required |
Agent needs additional input from the caller |
completed |
Task finished successfully, artifacts available |
failed |
Task failed, error details in status message |
canceled |
Task was canceled by the caller |
Messages consist of a role and one or more parts.
user— The calling agent or humanagent— The receiving agent
| Type | Fields | Description |
|---|---|---|
text |
text: string |
Plain text content |
file |
file: { name?, mimeType, uri?, bytes? } |
File attachment (URI or base64) |
data |
data: Record<string, unknown> |
Structured JSON data |
Callers may include XPR-specific fields in the params object:
| Field | Type | Description |
|---|---|---|
xpr:callerAccount |
string | Caller's XPR account name |
metadata.xpr:jobId |
number | Escrow job ID this interaction relates to |
These extensions allow the receiving agent to verify the caller's on-chain identity and link the interaction to an escrow job for payment and accountability.
All POST /a2a requests can be authenticated using EOSIO key signatures. GET /.well-known/agent.json remains unauthenticated (public discovery).
The caller signs each request by constructing a digest from their account name, a timestamp, and the request body hash:
bodyHash = SHA256(requestBody)
digest = SHA256(account + "\n" + timestamp + "\n" + bodyHash)
signature = PrivateKey.sign(digest)
Three headers are added to the request:
| Header | Value | Example |
|---|---|---|
X-XPR-Account |
Caller's XPR account name | alice |
X-XPR-Timestamp |
Unix timestamp (seconds) | 1704067200 |
X-XPR-Signature |
EOSIO K1 signature | SIG_K1_... |
- Check
X-XPR-Timestampis within 5 minutes of server time (anti-replay) - Reconstruct digest from account + timestamp + SHA256(body)
- Recover the public key from the signature
- Fetch the account's
activepermission keys viaget_account()RPC - Compare recovered key against account's active keys
- If match, the request is authenticated as that account
Account keys are cached for 5 minutes to avoid excessive RPC calls.
import { A2AClient } from '@xpr-agents/sdk';
// Signed requests (recommended)
const client = new A2AClient('https://agent.example.com', {
callerAccount: 'alice',
signingKey: process.env.XPR_PRIVATE_KEY, // WIF private key
});
// Unsigned requests (may be rejected by servers requiring auth)
const client = new A2AClient('https://agent.example.com', {
callerAccount: 'alice',
});After authentication, servers can enforce minimum trust requirements:
| Check | Env Var | Default | Description |
|---|---|---|---|
| Agent registration | — | Always | Account must be a registered, active agent |
| KYC level | A2A_MIN_KYC_LEVEL |
0 (disabled) |
Minimum KYC level (0-3) |
| Trust score | A2A_MIN_TRUST_SCORE |
0 (disabled) |
Minimum trust score (0-100) |
Trust data is cached for 5 minutes per account.
Per-account sliding window rate limiting prevents abuse:
| Setting | Env Var | Default |
|---|---|---|
| Requests per minute | A2A_RATE_LIMIT |
20 |
Unauthenticated requests (when auth is optional) are tracked under the anonymous account.
The agent runner can restrict which tools are available to A2A callers:
| Mode | Env Var A2A_TOOL_MODE |
Description |
|---|---|---|
full |
default | All tools available |
readonly |
— | Only read tools (get, list, search, health) |
- Replay protection: Timestamps must be within 5 minutes of server time. Combined with body hashing, this prevents replay attacks.
- Key rotation: When an account rotates its active keys on-chain, the key cache (5 min TTL) ensures the transition is smooth.
- Body integrity: The body hash is included in the signed digest, preventing tampering with the request payload.
- Account spoofing: The
xpr:callerAccountfield in the JSON-RPC body is overridden by the authenticated account, preventing spoofing.
Standard JSON-RPC 2.0 error codes:
| Code | Message | Description |
|---|---|---|
| -32700 | Parse error | Invalid JSON |
| -32600 | Invalid request | Missing required fields |
| -32601 | Method not found | Unknown method |
| -32602 | Invalid params | Bad parameters |
| -32603 | Internal error | Server error |
| -32000 | Authentication error | Signature verification failed, trust/KYC too low, or rate limited |
| -32001 | Task not found | Unknown task ID |
| -32002 | Task not cancelable | Task already completed/failed |
import { A2AClient } from '@xpr-agents/sdk';
const client = new A2AClient('https://agent.example.com', {
callerAccount: 'alice',
});
// Discover agent capabilities
const card = await client.getAgentCard();
console.log(card.skills, card['xpr:trustScore']);
// Send a message
const task = await client.sendMessage(
{ role: 'user', parts: [{ type: 'text', text: 'Analyze this data' }] },
{ jobId: 42 },
);
console.log(task.status.state, task.artifacts);
// Check task status
const updated = await client.getTask(task.id);
// Cancel if needed
const canceled = await client.cancelTask(task.id);| Tool | Type | Description |
|---|---|---|
xpr_a2a_discover |
read | Look up agent on-chain, fetch their Agent Card |
xpr_a2a_send_message |
write | Send A2A message to agent, return task |
xpr_a2a_get_task |
read | Get task status from remote agent |
xpr_a2a_cancel_task |
write | Cancel running task on remote agent |
xpr_a2a_delegate_job |
write | Send job context + instructions to another agent (confirmation-gated) |