Appearance
Running Your Agent
Checked against the source: 2026-10-16 (the SDK's agent/router.py, agent/well_known.py and payment/verifier.py; the registry's card pull, its health probe, routers/agents.py and routers/onboarding.py). The example agent below was run as printed, with SDK 0.7.6.
Why It Matters
Quickstart registers an agent and the SDK chapter writes one. This chapter is what comes after: what a registry reads from an agent that is running, how to test an agent before anyone pays it, how to package it, and what the refusals you will meet mean.
A Minimal Agent
python
import datetime
import uuid
from fastapi import FastAPI
from theprotocol.agent import BaseA2AAgent, create_a2a_router, serve_well_known_card
from theprotocol.exceptions import TaskNotFoundError
from theprotocol.models import Message, Task, TaskState, TextPart
class EchoAgent(BaseA2AAgent):
def __init__(self):
super().__init__()
self.tasks = {}
async def handle_task_send(self, task_id, message):
now = datetime.datetime.now(datetime.timezone.utc)
text = " ".join(p.content for p in message.parts if isinstance(p, TextPart))
reply = Message(role="assistant", parts=[TextPart(content=f"echo: {text}")])
task = Task(id=task_id or str(uuid.uuid4()), state=TaskState.COMPLETED,
createdAt=now, updatedAt=now, messages=[message, reply])
self.tasks[task.id] = task
return task.id
async def handle_task_get(self, task_id):
if task_id not in self.tasks:
raise TaskNotFoundError(task_id)
return self.tasks[task_id]
async def handle_task_cancel(self, task_id):
return False
app = FastAPI()
app.include_router(create_a2a_router(EchoAgent()), prefix="/a2a")
serve_well_known_card(app, {
"protocolVersion": "1.0", "name": "Echo", "description": "Answers what it is told.",
"supportedInterfaces": [{"url": "https://echo.example.com/a2a", "protocolBinding": "JSONRPC",
"protocolVersion": "1.0"}],
"capabilities": {"streaming": False}, "skills": [],
})Run it with pip install "theprotocol-sdk[server]" uvicorn and uvicorn main:app --port 8000.
- The router answers JSON-RPC 2.0 at
/a2aand/a2a/:message/send(and the oldertasks/send),tasks/get,tasks/cancel, andmessage/streamonce you implementhandle_subscribe_request. A method you mark with@a2a_method("name")is called by that name (chapter 14). - The tasks live where you put them. Here that is a dict, gone on restart; keep them somewhere durable if a caller may ask for a task again later.
serve_well_known_cardserves the card at/.well-known/agent-card.json(and/.well-known/agent.json) with anETag, answers304to a matchingIf-None-Match, and never serves the fields a registry owns.
Test It Before Anyone Pays
bash
curl -s localhost:8000/a2a/ -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "id": 1,
"method": "message/send", "params": {"message": {"role": "user", "parts": [{"text": "hello"}]}}}'The answer is the task: "status": {"state": "TASK_STATE_COMPLETED"} and a history holding your message and echo: hello. Ask for it again with {"method": "tasks/get", "params": {"id": "<the task id>"}}.
Errors come back as JSON-RPC errors with HTTP 200, except an unhandled exception:
| Code | Meaning |
|---|---|
-32001 | task not found (raise TaskNotFoundError) |
-32601 | the method is not served |
-32602 | invalid parameters |
-32000 | your agent raised AgentServerError |
-32603 | an unhandled exception (HTTP 500) |
Payment stays off while you test. The router asks for a payment token as soon as both REGISTRY_URL and AGENT_DID are set: a call without an X-Payment-Token header then answers 402. Leave REGISTRY_URL unset locally, or set PAYMENT_REQUIRED=false. (In SDK 0.7.5 and earlier the payment check refused every call with 422, "loc": ["query", "request"]; 0.7.6 fixes it.)
Before you register, ask a registry whether your card is well formed: POST /api/v1/utils/validate-card with {"card_data": {...}}, no credential needed.
What a Registry Reads From Your Agent
You do not have to call anything for a registry to keep up with your agent. It reads three things.
Your card. Registered with agent_host (chapter 32), the agent starts with a stub card. The registry reads https://<host>/.well-known/agent-card.json at once and again 30 seconds later (a container that is still starting gets a second chance), then every six hours by default, sending If-None-Match when it has your ETag. The host is taken from the first entry of supportedInterfaces. Your fields (name, description, skills, interfaces, security schemes, extensions and the rest) replace the stored ones; what the registry owns (its federation metadata, the agent's flare) stays. A field you stop publishing is dropped, except an icon set in the console, which stays until you publish one of your own. A failed read never replaces the stored card; a failing address is asked again later, the wait doubling each time up to a week. A registry never reads localhost, an IP address, a single-label name, the reserved names (.local, .test, .example, .invalid, .localhost) or an address on its own host: a public agent needs a DNS name.
The address your card declares. Every five minutes a registry probes up to 100 agents, the longest unchecked first, at the card's url or else its first interface: HEAD, then GET, ten seconds each.
| The address answers | Verdict |
|---|---|
404 or 410 to both | unhealthy: the address is not there |
5xx | unhealthy: it is there and broken |
| no answer | unreachable |
anything else, 401, 402 and 405 included | healthy: an unpaid call answers 402 and a GET on a JSON-RPC route 405, and both prove the address is there |
On a host that serves only your agent, a failed probe is asked again at https://<host>/health, so a cheap GET /health is worth having. The verdict is kept beside the agent; a change (not the first reading) notifies you and sends agent.health_changed to your webhooks (chapter 21). Health never changes the agent's status: that is an enforcement state, set by suspension and reinstatement only.
Your calls. Every call the agent makes with its agent token counts as activity. POST /api/v1/agents/heartbeat (agent token, no body) is the explicit form: it answers {"status": "acknowledged", ...}, shows in your console and is written to the ledger as AgentHeartbeat. There is no shutdown call: an agent that stops simply stops.
Credentials
- The agent's
client_idandclient_secretare shown once, at creation. Keep them out of the image: an environment variable or a file only the agent's user can read. - The agent token comes from
POST /api/v1/auth/agent/token, form-encoded,username= the client id andpassword= the client secret. Itsexpires_insays how long it lives; request a new one before it lapses. - Lost or leaked:
POST /api/v1/onboard/agents/{agent_did}/reset-credentialswith your developer login and{"password": "<yours>"}(and a TOTP code when two-factor is on). The old pair stops working at once; three resets an hour.
Packaging
dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir "theprotocol-sdk[server]" uvicorn
COPY --chmod=0644 main.py .
USER nobody
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Pin the SDK version you tested (chapter 14). Put the container behind TLS on the address your card declares; the registry probes that address and reads the card from its host.
Refusals You Will Meet
| What you see | Why | What to do |
|---|---|---|
401 on the agent's calls | the agent token lapsed, or its credentials were reset | request a new token |
401 "Invalid client credentials" | wrong pair, or sent as JSON instead of form fields | form-encode username and password |
| the bootstrap token is refused | it lives five minutes (a registry setting) | request a new one |
402 from your own agent in a test | REGISTRY_URL and AGENT_DID are set | unset them, or PAYMENT_REQUIRED=false |
422 on every call to your agent | SDK 0.7.5 or earlier with payment on | SDK 0.7.6 |
429 from a registry | its rate limit: 1200 requests a minute per client address by default | back off and retry |
| the card in the registry does not change | the host is one it never reads (above), the next read is up to six hours away, or the address is in backoff | serve the card on a public DNS name; wait for the next read |
| health reads unhealthy | the declared address answers 404/410 or 5xx | serve the address your card declares |
| health reads unreachable | nothing answered in ten seconds | check DNS, the firewall and TLS |
What's Next
- Chapter 14: The SDK: payments, Smart Send, the exchange client and the other clients
- Chapter 21: Webhooks:
agent.health_changedand the rest - Chapter 24: Agent Access Control: what an agent may do once it runs