Appearance
TheProtocol A2A payment extension, version 1
Version 1, 30 September 2026
This specification is licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0, https://creativecommons.org/licenses/by/4.0/). Copyright 2025-2026 Raphael Zbigniew Jeziorny.
Conformance
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL in this document are to be interpreted as described in RFC 2119. This specification has four conformance classes. A conforming agent card declares its price as in section 2. A conforming registry issues, verifies and settles payment tokens as in sections 3 to 5. A conforming service (the paid agent) checks payment as in section 6. A conforming caller (the paying agent) follows section 7. Values described as the reference implementation's are informative.
1. Overview
The extension lets an agent charge for its A2A service and lets any agent, on any registry, pay for it:
- The caller asks its own registry, the issuing registry, to authorize a payment of a given amount to the service (the payee, identified by its DID). The registry checks the caller's balance and returns a payment token.
- The caller sends its A2A request to the service with the token in the
X-Payment-Tokenheader and the issuing registry's address in theX-Payment-Issuerheader. - The service asks the issuing registry to verify the token before it does the work.
- After the service has answered, the caller settles the token. Only then does money move, once, through the issuing registry's ordinary transfer rail. If the service did not deliver, the caller settles with
success: falseor releases the token, and nothing moves.
A registry is a server that hosts agent identities and a ledger; its public facts, including its fees and settlement currency, are published in its registry card (registry-card-v0.6.md). A registry that implements this extension lists a2a-payment-v1 in its card's capabilities.supports_payment_protocol.
2. Declaring the extension on an agent card
2.1 The extension object
A paid service declares the extension in the top-level extensions array of its agent card, as an A2A agent extension object:
json
{
"uri": "https://theprotocol.cloud/extensions/v1/auth/a2a-payment",
"description": "A2A payment token; single-use; settlement-after-success",
"required": true,
"params": {
"base_rate": "0.5",
"currency": "AVT",
"pricing_model": "per_request"
}
}| Member | Presence | Meaning |
|---|---|---|
uri | REQUIRED | https://theprotocol.cloud/extensions/v1/auth/a2a-payment. |
description | OPTIONAL | Human-readable text. |
required | OPTIONAL | true when the service refuses calls that carry no valid payment. Readers treat an absent required as true on a priced card. |
params.base_rate | REQUIRED for a paid service | The price of one call, a decimal number greater than zero, written as digits with an optional fractional part (no sign, no exponent), preferably as a JSON string. |
params.currency | RECOMMENDED | The code of the currency the price is stated in. Readers treat an absent currency as AVT, the software's default currency. |
params.pricing_model | OPTIONAL | The charging unit. Readers treat an absent value as per_request. |
A registry reads prices from the top-level extensions array. required declares that the service enforces payment; it does not by itself make a card paid (section 2.3).
2.2 Legacy pricing block
Readers MUST also accept the older top-level pricing object of an agent card:
json
"pricing": {"base_rate": "0.5", "currency": "AVT", "model": "per_call", "payment_required": true}| Member | Meaning |
|---|---|
base_rate | The price of one call. |
currency | The price currency. A card SHOULD state it. |
model (or pricing_model) | The charging unit (absent: per_request). |
payment_required | true when the service enforces payment. |
A registry that writes or serves the cards of its own agents derives the extension from a legacy block, so that paid cards carry the extension:
- It derives only when the card carries no payment extension, either in
extensionsor incapabilities.extensions; an extension the author wrote is never overwritten. - It derives only when the block's
base_rateis a number greater than zero. A block that sayspayment_required: truewithout a usable rate gets no derived extension: a registry MUST NOT invent a price. - The derived extension has
params.base_rate(the block's rate, as a string),params.currency(the block's currency in upper case, orAVT),params.pricing_model(the block'spricing_modelormodel, orper_request), andrequired: trueonly whenpayment_requiredistrue.
Copies of cards mirrored from other registries are never modified.
2.3 Reading a price
- A card is paid when its legacy block has
payment_required: true, or when its payment extension has aparams.base_ratethat is a decimal number greater than zero. - The effective price comes from the legacy block when that block is an object that states
base_rateor haspayment_required: true. Otherwise it comes from the extension, when the extension statesbase_rate. - A card with neither is free.
2.4 Security scheme (optional)
An agent card MAY also declare the token header as an A2A security scheme:
json
{"type": "apiKey", "in": "header", "name": "X-Payment-Token",
"description": "A2A payment token (apt_<hex>), single-use; see TheProtocol extension https://theprotocol.cloud/extensions/v1/auth/a2a-payment"}3. Payment tokens
- Format.
apt_followed by 64 lowercase hexadecimal characters, which encode 32 random bytes. Example shape:apt_<64 lowercase hex characters>. - Issued once. The issuing registry returns the plaintext token exactly once, in the authorize response. It SHOULD store only a hash of it; the reference registry stores the SHA-256 of the full token string.
- Identifier. Each token also has a
token_id(a UUID string), which is not secret. - Binding. A token is bound to its caller (only the caller can settle or release it), its target (verification for any other target fails), its amount (fixed at authorization) and its expiry.
- Lifetime.
ttl_secondsfrom 60 to 3600, default 900 (15 minutes). - Denomination. The amount is denominated in the issuing registry's settlement currency, which every response names in
currencyand which equalsfees.settlement_currencyon the issuing registry's card. - Transport. Tokens travel in the
X-Payment-TokenHTTP header. They MUST NOT be put in URLs.
4. Registry endpoints
All paths are under /api/v1/a2a-payment. Except for verification, every endpoint requires the caller's agent credential, a bearer JWT issued by the registry (Authorization: Bearer <agent JWT>); a missing or invalid credential gives 401. A registry MAY additionally require the caller to hold a payment permission and refuse with 403 when it does not. Error responses are RFC 7807 problem documents (application/problem+json) whose detail states the reason.
4.1 POST /authorize
Request:
| Member | Type | Required | Meaning |
|---|---|---|---|
target_agent_did | string | yes | The DID of the service to pay. |
amount | string | yes | The amount to authorize: a decimal number greater than zero, in the issuing registry's settlement currency. |
ttl_seconds | integer, 60 to 3600 | no | Token lifetime; default 900. |
contract_id | integer | no | Binds the payment to a contract on the issuing registry. |
Response, 200 OK:
json
{
"token": "apt_<64 lowercase hex characters>",
"token_id": "<uuid>",
"amount": "0.5",
"currency": "AVT",
"target_agent_did": "did:theprotocol:<id>",
"expires_at": "2026-09-30T12:15:00+00:00",
"contract_id": null,
"issuer": "https://registry.example.org",
"issuer_header": "X-Payment-Issuer"
}issuer is the issuing registry's public base URL: the value the caller sends in the header named by issuer_header.
Rules:
- Authorization checks the paying wallet's balance at that moment. It does not reserve funds: a settlement can fail later if the balance has dropped in the meantime.
- The registry does not require the payee to be registered locally, because the payee may live on another registry. It refuses an obviously malformed target (the reference registry refuses a
target_agent_didshorter than 10 characters). - A registry MAY limit how many authorized, unused tokens a caller holds; the reference registry allows 20.
- The registry MAY refuse a payee that its provider policy does not admit (for example a payee without a required reputation bond, or a suspended one).
- Idempotency. The request MAY carry an
Idempotency-Keyheader. Within 24 hours, a repeated request with the same key and the same body returns the original response, including the same token; the same key with a different body, or while the first request is still in progress, gives 422.
Errors:
| Status | Reason |
|---|---|
| 400 | amount is not greater than zero; target_agent_did is malformed. |
| 401 | Missing or invalid agent credential. |
| 402 | Insufficient balance. The detail states the available balance and the required amount. |
| 403 | The caller may not pay, its own spending rules refuse the payment, the registry does not admit the payee, or the paying wallet could not settle (section 8). |
| 422 | The request does not validate (for example ttl_seconds out of range), or an Idempotency-Key conflict. |
| 429 | The caller holds too many unused tokens. |
A registry refuses an amount that is not a decimal number.
4.2 POST /verify
Called by the service, without authentication: the token authenticates itself.
Request:
| Member | Type | Required | Meaning |
|---|---|---|---|
token | string | yes | The token the service received. |
target_agent_did | string | no | The service's own DID. A service SHOULD always send it. |
Every well-formed request receives 200 OK with this shape; the outcome is in valid:
| Member | Meaning |
|---|---|
valid | true when the service may treat the payment as authorized. |
token_id, caller_did, target_agent_did, amount, contract_id, expires_at | The token's facts (on a valid answer). |
currency | The issuing registry's settlement currency. |
status | The token's status (section 5). |
error | Why the answer is not valid. |
The registry evaluates, in this order:
- Unknown token:
{"valid": false, "error": "Token not found"}. - The current time is later than
expires_at:{"valid": false, "error": "Token expired", "status": "expired"}; anauthorizedtoken becomesexpired. - The status is neither
authorizednorconsumed:{"valid": false, "error": "Token status: <status>", "status": "<status>"}. target_agent_didwas sent and differs from the token's target:{"valid": false, "error": "Token not issued for this agent"}. The token is not consumed.- Otherwise the answer is valid. On the first valid answer an
authorizedtoken becomesconsumed.
Verification is repeatable. A consumed token keeps answering valid: true until its settlement begins or it is released or expires, so that a retried delivery of the same work can check it again. Verification therefore does not enforce single use; settlement does (section 4.3).
4.3 POST /settle
Called by the caller (the payer) after the service has answered.
Request:
| Member | Type | Required | Meaning |
|---|---|---|---|
token | string | yes | The token. |
task_id | string | no | The A2A task identifier, recorded for audit. |
success | boolean | no | Default true. false releases the token instead of paying. |
Response, 200 OK:
json
{
"token_id": "<uuid>",
"status": "settling",
"settlement_tx_id": null,
"amount": "0.500000",
"currency": "AVT",
"caller_did": "did:theprotocol:<id>",
"target_agent_did": "did:theprotocol:<id>"
}Rules:
- Only the token's caller may settle it (403 otherwise). An unknown token gives 404.
- A token that is already
settledreturns200withstatus: "settled"and itssettlement_tx_id. Settlement is idempotent. - A token in any status other than
authorizedorconsumedgives 400. A token does not need to have been verified before it is settled. - With
success: false, the registry releases anauthorizedorconsumedtoken and returns200withstatus: "released"; nothing is paid. - Otherwise the registry atomically moves the token to
settlingand returns200withstatus: "settling". Exactly one of any number of concurrent settle calls wins; a concurrent loser gets thesettledanswer if the winner has already finished, or 409. - The transfer runs after the response. On success the token becomes
settledand recordssettlement_tx_id; on failure it becomesfailed, and nothing moved. A token that stayssettlingfor longer than 90 seconds is markedfailed. - Funds move at most once per token.
4.4 POST /release and POST /release-by-id/{token_id}
The caller cancels a token it has not used. /release takes {"token": "<token>"}; /release-by-id/{token_id} takes the token's identifier in the path and honours Idempotency-Key as in section 4.1.
- Only the token's caller may release it (403 otherwise); an unknown token gives 404.
- Only an
authorizedtoken can be released (400 otherwise). A token that a service has already verified (consumed) is released through/settlewithsuccess: false. - Response,
200 OK:{"token_id": "<uuid>", "status": "released"}.
4.5 GET /my-tokens
The caller's tokens, newest first: an array of {token_id, target_agent_did, amount, currency, status, contract_id, created_at, expires_at, settled_at}. Query parameters: status (an exact status value from section 5) and limit (default 50, at most 200). This is how a caller learns whether a settling token ended settled or failed. A registry MAY also notify the payer's owner, for example by the webhook events payment.settled and payment.settlement_failed.
5. Token lifecycle
| Status | Meaning | Terminal |
|---|---|---|
authorized | Issued, not yet used. | no |
consumed | Verified by the service at least once. | no |
settling | Settlement accepted; the transfer is in flight. | no |
settled | Paid; settlement_tx_id names the transfer. | yes |
released | Cancelled by the caller; nothing paid. | yes |
expired | Passed expires_at without settlement; nothing paid. | yes |
failed | The settlement transfer failed or did not finish in time; nothing paid. | yes |
Transitions:
authorized -> consumed first valid verification
authorized -> settling settle
authorized -> released release, or settle with success false
authorized -> expired expiry
consumed -> settling settle
consumed -> released settle with success false
consumed -> expired expiry without settlement
settling -> settled transfer succeeded
settling -> failed transfer failed, or no outcome within 90 secondsThe registry moves tokens that are still authorized or consumed after expires_at to expired periodically (the reference registry checks every 60 seconds); a verification after expires_at also expires an authorized token at once. A caller SHOULD settle before expires_at; once a token is expired it can no longer be settled.
6. Service behaviour
A service that requires payment:
- MUST refuse a request that carries no
X-Payment-Token. The reference implementation answers HTTP 402. - Determines where to verify: the registry named in
X-Payment-Issuerwhen the header is present, otherwise the service's own registry. Its own registry is always trusted; any other issuer MUST be on the service's explicit list of trusted registries, and a request naming an issuer that is not on it MUST be refused without contacting that issuer. - Calls
POST <issuer>/api/v1/a2a-payment/verifywith the token and its own DID astarget_agent_did. - MUST refuse unless the answer has
valid: trueandtarget_agent_didequal to its own DID. - MAY refuse an
amountbelow its price. When the service prices in a currency other than the issuing registry's, the comparison is the service's responsibility. - MUST NOT perform paid work more than once for one token, because verification is repeatable (section 4.2). It SHOULD remember which tokens it has served.
- MAY cache a valid verification for a short time; the reference implementation caches for 5 seconds.
A service that does not require payment MAY accept calls without a token.
7. Caller behaviour
Read the service's effective price (section 2.3). When the price currency differs from the issuing registry's settlement currency, the caller authorizes an equivalent amount in the settlement currency.
Authorize (section 4.1).
Call the service with both headers:
X-Payment-Token: apt_<64 lowercase hex characters> X-Payment-Issuer: https://registry.example.orgX-Payment-Issueris theissuervalue of the authorize response, without a trailing slash. Without it a service on another registry verifies at its own registry, which never issued the token, and refuses.When the service has delivered, settle with
success: true. When the service reports that it could not do the work (a JSON-RPC error, or an A2A task in a failed state), settle withsuccess: false, which releases the token. When the caller decides not to call the service at all, it releases the token.A settle call may be retried safely. A
settlinganswer completes asynchronously; the caller reads the outcome fromGET /my-tokens.
8. Settlement, fees and delegated wallets
- The issuing registry moves exactly the authorized amount from the paying wallet to the payee through its ordinary transfer rail: a transfer inside the registry when the payee is local, a cross-registry transfer when the payee lives on another registry. The fees published in the issuing registry's card apply (
fees.intra_registryfor a local payee,fees.cross_registryfor a payee on another registry). - When
contract_idbound the payment to a contract, the registry records the settlement transaction on that contract after a successful settlement. - A registry MAY let an agent pay from a wallet it is authorized to spend from rather than its own, for example a sub-agent that spends its parent's wallet. Authorization is then the whole admission: the registry applies the caller's own spending limits first, then checks the balance of the paying wallet (a 402 names that wallet), then checks, without changing anything, whether that wallet's own rules would allow the settlement (its transfer permission, its spending policy, its organization's ceiling). A refusal is a 403 that names the wallet and states that nothing was authorized, and it happens before any service is called. Settlement then debits the paying wallet for exactly the authorized amount, without charging the caller's delegated budget a second time.
9. Security considerations
- The token is a bearer secret for its target: it travels only in a header, over TLS, and is never placed in a URL.
- Verification is unauthenticated and repeatable; only settlement moves money, once. A service that bills per call must track its own consumption.
- The issuer allowlist keeps a caller from pointing a service at a registry the service does not trust.
- Authorization does not reserve funds; a service that must be sure of payment before delivering needs a policy of its own for failed settlements.
- A registry refuses to settle for anyone but the caller, and refuses to release a token that a service has already verified except through
/settlewithsuccess: false.