A2A call linkage and billing by caller
An external A2A agent carries a platform-issued call credential into model calls. The platform uses it to tie the agent's internal model calls to a specific A2A call and bill them to the caller.
A2A call linkage and billing by caller
An external A2A agent that unit administrator A connects is called by members B, C, and others. While handling a request the agent also calls the platform model gateway. By default those model calls are all recorded under the model key the agent uses — that is, under A — and the platform cannot tell which A2A call a given model call served.
After connecting A2A call credentials:
- Every A2A call's request detail shows all model calls the agent made internally, plus their count, token count, and spend total. A model-call detail can jump back to the matching A2A call.
- Model calls inside the agent are recorded under callers B and C. Quotas are judged against the caller's personal quota, and still against that key's own quota.
- The agent's detail Monitoring tab lists model spend by caller. Calls that did not carry a credential are listed separately as Agent's own calls.
Linkage and billing do not depend on a trace ID, and the caller does not have to change any request.
1. How it works
- When a caller invokes the agent through the platform, the platform issues one call credential (starting with
zac_) for each A2A request it forwards, and places it in two places: request headerX-Zenova-Call-Context, and fieldzenova_call_contextinparams.message.metadata. - The agent takes the credential from the inbound request and stores it in this request's scope (see section 4).
- The agent calls the platform model gateway with its dedicated model key, and passes the credential unchanged in request-body
metadata.zenova_call_context. - The model gateway verifies the credential's signature and expiry locally, confirms that the agent the credential was issued to is the agent bound to that key, then records this model call under the caller and notes which A2A call it belongs to.
The credential is issued and verified locally on the gateway node. It does not need any admin-console API.
2. Billing rules
- Only credentials the caller claimed themselves are billed per person. The platform issues a credential when the caller claimed the connection from the app market, used a personal MCP token, or started the call on a platform page as themselves. Tokens an administrator issues on the agent detail page and hands to a business system do not get a credential. Those calls are recorded under the key owner. When you need per-person billing, have the caller claim the connection themselves.
- Model calls without a credential are recorded under the key owner. The platform always allows those calls and does not error. In monitoring they go under Agent's own calls. Timed jobs the agent starts itself belong here.
- The credential is valid for 1 hour. Judged by the model request's start time: a streaming call that started inside the window and finished after it still completes. Later model calls of a long task that exceeds 1 hour receive
A2A_CALL_TOKEN_EXPIRED. - Quota is judged in two places. A model call must satisfy both that dedicated key's quota and the caller's personal quota. Either shortfall rejects the call. When the caller's quota is short, the existing quota error code is returned; the outward message does not include the caller's quota detail.
3. Prerequisites
- Connect the agent: connect that A2A agent in the admin console A2A catalog and enable it.
- Create an agent-dedicated model key: open that agent's A2A agent detail, and in the Details tab Model keys area click Create. The agent is prefilled and locked. Choose a workspace and a model group, then submit. The key plaintext is shown only once; give it immediately to the agent's operators to store. An ordinary API key cannot be used for billing by caller: when an ordinary key carries a credential, the credential is ignored and the call is recorded under the key owner.
- Configure the model-gateway address: How to connect under the Model keys area gives this deployment's model-gateway address and copy-ready code snippets.
- Caller claims: the caller claims a connection to that agent in the app market (or uses a personal MCP token, or calls directly on a platform page), then calls as usual. No extra configuration is needed.
4. Agent-side integration
4.1 Take the credential out
The credential sits in both request header X-Zenova-Call-Context and params.message.metadata.zenova_call_context. Either is fine. When both exist but the values differ, the request was changed in transit and should be rejected. When neither is present, this request has no credential, and model calls are recorded under the key owner.
4.2 Store the credential in request scope
The same agent process handles requests from several callers at once. The credential must be stored in request scope, for example Python's contextvars.ContextVar.
Do not store the credential in a global, a module variable, or a model client's defaults (default headers, default body). Otherwise C's request arriving overwrites B's credential, and B's later model calls carry C's credential, so spend is billed to C. asyncio.create_task and asyncio.gather copy the current context, so tool calls and subtasks inherit this request's credential automatically.
import contextvars
METADATA_KEY = "zenova_call_context"
_current_token = contextvars.ContextVar("zenova_call_context", default=None)
def model_call_metadata():
token = _current_token.get()
return {METADATA_KEY: token} if token else None4.3 Carry the credential on every model call
OpenAI Python SDK: take the credential at each call and write it into metadata.
metadata = model_call_metadata()
kwargs = {"metadata": metadata} if metadata else {}
response = await client.chat.completions.create(
model=model,
messages=messages,
**kwargs,
)AgentScope: when the agent calls a model internally it only passes messages and tools, so subclass OpenAIChatModel and write metadata each time in _call_api.
from agentscope.model import OpenAIChatModel
class ZenovaChatModel(OpenAIChatModel):
async def _call_api(self, model_name, messages, tools=None, tool_choice=None, **kwargs):
metadata = model_call_metadata()
if metadata is not None:
kwargs["metadata"] = metadata
return await super()._call_api(
model_name, messages, tools=tools, tool_choice=tool_choice, **kwargs
)The model gateway also accepts request header X-Zenova-Call-Context. Use either the header or body metadata.zenova_call_context; when both are present, the values must match. Most agent frameworks cannot change headers per request, so using only the body is recommended.
Complete runnable examples (OpenAI Python SDK, AgentScope, and a concurrency self-test script) are in lobsterpool/examples/a2a-call-attribution/ in the product repository.
5. View linkage and spend
| Location | Contents |
|---|---|
| A2A request detail → Agent internal model calls | All model calls the agent made with a credential during this A2A call, plus count, token count, and spend total. Click a row to open that model request's detail |
| Model request detail | Caller shows as Agent X (calling for B), with View A2A call to jump back to the matching A2A request |
| A2A agent detail → Monitoring tab → Model spend by caller | Model spend of that agent's dedicated key in the selected time range, summarized by caller. Calls that did not carry a credential are summarized as one Agent's own calls row |
When an A2A call has no internal model calls, the Agent internal model calls area shows that this call had no model calls through the platform, or the agent did not carry a call credential.
6. Error codes
When the model gateway fails to validate the credential it rejects the request outright. It does not fall back to billing the key owner. The agent should return the failure to the caller as-is.
| Error code | HTTP | Meaning | Remedy |
|---|---|---|---|
A2A_CALL_TOKEN_CONFLICT | 400 | Both the header and body metadata carried a credential, but the values differ | Keep only one, or make the two values the same |
A2A_CALL_TOKEN_MALFORMED | 400 | The credential is not a single string, for example a duplicate field in the body, or several values in the header | Pass through the string taken from the inbound request; do not rewrite it |
A2A_CALL_TOKEN_EXPIRED | 401 | The credential has exceeded its 1-hour validity | See section 2 item 3 |
A2A_CALL_TOKEN_INVALID | 401 | Signature check failed (the credential was changed, forged, or rotated), or the credential's unit differs from the key's unit | Pass only the credential injected when the platform forwarded, and use a dedicated key created in this unit |
A2A_CALL_TOKEN_AGENT_MISMATCH | 403 | The agent the credential was issued to is not the agent bound to the key | Each agent uses its own dedicated key |
All error codes are in Error reference.
Call models with enterprise identity
A business system carries a user token issued by the enterprise identity source and calls models directly. Usage is metered by person and department; department quotas still apply.
Security vendor integration guide
Two paths for a security vendor to connect detection into the gateway: the content-safety detector HTTP API and a WASM gateway extension