Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Talk to a Tenzro Node via the A2A Protocol

AI AgentsIntermediate20 min

Google's Agent-to-Agent (A2A) protocol is an open JSON-RPC + SSE standard for letting autonomous agents discover, invoke, and stream from each other. Every Tenzro node exposes an A2A server on port 3002 (live at a2a.tenzro.network) advertising 19 skills— wallet, identity, inference, AP2 payments, swarm orchestration, task marketplace, bridges, and more. This tutorial covers discovery, blocking send, polling, and streaming.

What You'll Build

  • Discover a node's skills via /.well-known/agent.json
  • Send a task with message/send and retrieve it with tasks/get
  • Stream partial artifacts over SSE via /a2a/stream
  • Talk to the live testnet endpoint from TypeScript and Python

Endpoints

RPC Methods

Step 1: Discover the Agent Card

GET https://a2a.tenzro.network/.well-known/agent.json

{
  "name": "Tenzro Node",
  "description": "Verifiable AI infrastructure — wallets, identity, payments, inference, bridges.",
  "url": "https://a2a.tenzro.network",
  "version": "0.1.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "stateTransitionHistory": true
  },
  "defaultInputModes":  ["application/json", "text/plain"],
  "defaultOutputModes": ["application/json", "text/plain"],
  "skills": [
    { "id": "wallet",              "name": "Wallet Operations" },
    { "id": "identity",            "name": "TDIP Identity" },
    { "id": "inference",           "name": "Model Inference" },
    { "id": "settlement",          "name": "Settlement" },
    { "id": "verification",        "name": "ZK / TEE Verification" },
    { "id": "staking",             "name": "Staking" },
    { "id": "token",               "name": "Token Operations" },
    { "id": "contract",            "name": "Contract Deployment" },
    { "id": "ap2-payments",        "name": "AP2 Mandates" },
    { "id": "agent_spawning",      "name": "Agent Spawning" },
    { "id": "swarm_orchestration", "name": "Swarm Orchestration" },
    { "id": "task_marketplace",    "name": "Task Marketplace" },
    { "id": "agent_marketplace",   "name": "Agent Templates" },
    { "id": "join",                "name": "Network Participation" },
    { "id": "nft",                 "name": "NFT Operations" },
    { "id": "bridge",              "name": "Cross-Chain Bridge" },
    { "id": "compliance",          "name": "Compliance" },
    { "id": "crosschain",          "name": "Cross-Chain Ops" },
    { "id": "events",              "name": "Event Stream" }
  ]
}

Step 2: Send a Task

POST https://a2a.tenzro.network/a2a
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "message/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [
        { "type": "text", "text": "Transfer 10 TNZO to 0xRecipient..." }
      ]
    }
  }
}

-> {
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "task": {
      "id":      "task-01HR...",
      "state":   "submitted",
      "session": "sess-01HR..."
    }
  }
}

Step 3: Poll for Completion

POST /a2a   body: { "jsonrpc":"2.0","id":"2","method":"tasks/get","params":{ "taskId":"task-01HR..." } }

-> {
  "result": {
    "task": {
      "id":     "task-01HR...",
      "state":  "completed",   // submitted | working | input_required | completed | failed | cancelled
      "artifacts": [
        {
          "type": "application/json",
          "data": { "tx_hash": "0x...", "status": "included" }
        }
      ]
    }
  }
}

Step 4: Stream via SSE

For long-running tasks (inference, cross-chain bridges, swarm orchestration), SSE is preferred — the server emits task.state and task.artifact events as they happen:

POST https://a2a.tenzro.network/a2a/stream
Accept: text/event-stream
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "3",
  "method": "message/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Run inference on llama-3-70b: summarize today's news" }]
    }
  }
}

--- SSE stream ---
event: task.state
data: { "taskId":"task-01HR...","state":"working" }

event: task.artifact
data: { "taskId":"task-01HR...","artifact":{ "type":"text/plain","data":"partial token..." } }

event: task.artifact
data: { "taskId":"task-01HR...","artifact":{ "type":"text/plain","data":"another token..." } }

event: task.state
data: { "taskId":"task-01HR...","state":"completed" }

Step 5: TypeScript Client (Blocking + Poll)

// Minimal A2A client — no SDK dependency.
const BASE = "https://a2a.tenzro.network";

// 1. Discover
const card = await fetch(BASE + "/.well-known/agent.json").then(r => r.json());
console.log("skills:", card.skills.map((s: any) => s.id));

// 2. Send a task (blocking)
const send = await fetch(BASE + "/a2a", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0", id: "1", method: "message/send",
    params: { message: { role: "user", parts: [{ type: "text", text: "get node status" }] } },
  }),
}).then(r => r.json());

const taskId = send.result.task.id;

// 3. Poll until done
while (true) {
  const r = await fetch(BASE + "/a2a", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ jsonrpc:"2.0", id:"2", method:"tasks/get", params:{ taskId } }),
  }).then(r => r.json());
  if (["completed","failed","cancelled"].includes(r.result.task.state)) {
    console.log(r.result.task.artifacts);
    break;
  }
  await new Promise(r => setTimeout(r, 500));
}

Step 6: TypeScript Client (SSE Streaming)

// Stream tokens as they arrive using EventSource-style SSE parsing.
const resp = await fetch("https://a2a.tenzro.network/a2a/stream", {
  method: "POST",
  headers: { "content-type": "application/json", accept: "text/event-stream" },
  body: JSON.stringify({
    jsonrpc: "2.0", id: "1", method: "message/send",
    params: { message: { role: "user", parts: [{ type: "text", text: "generate a haiku" }] } },
  }),
});

const reader = resp.body!.getReader();
const dec    = new TextDecoder();

for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  const chunk = dec.decode(value);
  for (const line of chunk.split("\n")) {
    if (line.startsWith("data: ")) {
      const evt = JSON.parse(line.slice(6));
      if (evt.artifact?.type === "text/plain") process.stdout.write(evt.artifact.data);
    }
  }
}

Step 7: Python Client

# Python A2A client using requests + sseclient-py
import requests
from sseclient import SSEClient

BASE = "https://a2a.tenzro.network"

# 1. Discover skills
card = requests.get(f"{BASE}/.well-known/agent.json").json()
print("available skills:", [s["id"] for s in card["skills"]])

# 2. Streaming inference
payload = {
    "jsonrpc": "2.0", "id": "1", "method": "message/send",
    "params": {"message": {"role": "user", "parts": [
        {"type": "text", "text": "Summarize the Tenzro whitepaper"}
    ]}},
}
resp = requests.post(f"{BASE}/a2a/stream", json=payload, stream=True)
for event in SSEClient(resp).events():
    print(event.event, event.data)

A2A vs MCP. MCP is a tool-calling protocol — Claude or other LLMs pick specific tools to run. A2A is a peer-to-peer protocol where one agent delegates whole tasks to another and streams back artifacts. Tenzro runs both on every node (MCP on 3001, A2A on 3002) so you can compose: an LLM on your laptop calls a Tenzro MCP tool, which internally delegates to another Tenzro node via A2A.

What You Learned

Next Steps