Skip to content

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:

  1. 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.
  2. The caller sends its A2A request to the service with the token in the X-Payment-Token header and the issuing registry's address in the X-Payment-Issuer header.
  3. The service asks the issuing registry to verify the token before it does the work.
  4. 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: false or 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"
  }
}
MemberPresenceMeaning
uriREQUIREDhttps://theprotocol.cloud/extensions/v1/auth/a2a-payment.
descriptionOPTIONALHuman-readable text.
requiredOPTIONALtrue when the service refuses calls that carry no valid payment. Readers treat an absent required as true on a priced card.
params.base_rateREQUIRED for a paid serviceThe 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.currencyRECOMMENDEDThe code of the currency the price is stated in. Readers treat an absent currency as AVT, the software's default currency.
params.pricing_modelOPTIONALThe 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}
MemberMeaning
base_rateThe price of one call.
currencyThe price currency. A card SHOULD state it.
model (or pricing_model)The charging unit (absent: per_request).
payment_requiredtrue 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 extensions or in capabilities.extensions; an extension the author wrote is never overwritten.
  • It derives only when the block's base_rate is a number greater than zero. A block that says payment_required: true without 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, or AVT), params.pricing_model (the block's pricing_model or model, or per_request), and required: true only when payment_required is true.

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 a params.base_rate that is a decimal number greater than zero.
  • The effective price comes from the legacy block when that block is an object that states base_rate or has payment_required: true. Otherwise it comes from the extension, when the extension states base_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_seconds from 60 to 3600, default 900 (15 minutes).
  • Denomination. The amount is denominated in the issuing registry's settlement currency, which every response names in currency and which equals fees.settlement_currency on the issuing registry's card.
  • Transport. Tokens travel in the X-Payment-Token HTTP 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:

MemberTypeRequiredMeaning
target_agent_didstringyesThe DID of the service to pay.
amountstringyesThe amount to authorize: a decimal number greater than zero, in the issuing registry's settlement currency.
ttl_secondsinteger, 60 to 3600noToken lifetime; default 900.
contract_idintegernoBinds 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_did shorter 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-Key header. 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:

StatusReason
400amount is not greater than zero; target_agent_did is malformed.
401Missing or invalid agent credential.
402Insufficient balance. The detail states the available balance and the required amount.
403The 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).
422The request does not validate (for example ttl_seconds out of range), or an Idempotency-Key conflict.
429The 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:

MemberTypeRequiredMeaning
tokenstringyesThe token the service received.
target_agent_didstringnoThe 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:

MemberMeaning
validtrue when the service may treat the payment as authorized.
token_id, caller_did, target_agent_did, amount, contract_id, expires_atThe token's facts (on a valid answer).
currencyThe issuing registry's settlement currency.
statusThe token's status (section 5).
errorWhy the answer is not valid.

The registry evaluates, in this order:

  1. Unknown token: {"valid": false, "error": "Token not found"}.
  2. The current time is later than expires_at: {"valid": false, "error": "Token expired", "status": "expired"}; an authorized token becomes expired.
  3. The status is neither authorized nor consumed: {"valid": false, "error": "Token status: <status>", "status": "<status>"}.
  4. target_agent_did was sent and differs from the token's target: {"valid": false, "error": "Token not issued for this agent"}. The token is not consumed.
  5. Otherwise the answer is valid. On the first valid answer an authorized token becomes consumed.

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:

MemberTypeRequiredMeaning
tokenstringyesThe token.
task_idstringnoThe A2A task identifier, recorded for audit.
successbooleannoDefault 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 settled returns 200 with status: "settled" and its settlement_tx_id. Settlement is idempotent.
  • A token in any status other than authorized or consumed gives 400. A token does not need to have been verified before it is settled.
  • With success: false, the registry releases an authorized or consumed token and returns 200 with status: "released"; nothing is paid.
  • Otherwise the registry atomically moves the token to settling and returns 200 with status: "settling". Exactly one of any number of concurrent settle calls wins; a concurrent loser gets the settled answer if the winner has already finished, or 409.
  • The transfer runs after the response. On success the token becomes settled and records settlement_tx_id; on failure it becomes failed, and nothing moved. A token that stays settling for longer than 90 seconds is marked failed.
  • 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 authorized token can be released (400 otherwise). A token that a service has already verified (consumed) is released through /settle with success: 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 ​

StatusMeaningTerminal
authorizedIssued, not yet used.no
consumedVerified by the service at least once.no
settlingSettlement accepted; the transfer is in flight.no
settledPaid; settlement_tx_id names the transfer.yes
releasedCancelled by the caller; nothing paid.yes
expiredPassed expires_at without settlement; nothing paid.yes
failedThe 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 seconds

The 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:

  1. MUST refuse a request that carries no X-Payment-Token. The reference implementation answers HTTP 402.
  2. Determines where to verify: the registry named in X-Payment-Issuer when 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.
  3. Calls POST <issuer>/api/v1/a2a-payment/verify with the token and its own DID as target_agent_did.
  4. MUST refuse unless the answer has valid: true and target_agent_did equal to its own DID.
  5. MAY refuse an amount below its price. When the service prices in a currency other than the issuing registry's, the comparison is the service's responsibility.
  6. 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.
  7. 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 ​

  1. 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.

  2. Authorize (section 4.1).

  3. Call the service with both headers:

    X-Payment-Token: apt_<64 lowercase hex characters>
    X-Payment-Issuer: https://registry.example.org

    X-Payment-Issuer is the issuer value 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.

  4. 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 with success: false, which releases the token. When the caller decides not to call the service at all, it releases the token.

  5. A settle call may be retried safely. A settling answer completes asynchronously; the caller reads the outcome from GET /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_registry for a local payee, fees.cross_registry for a payee on another registry).
  • When contract_id bound 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 /settle with success: false.

Server components AGPL-3.0-or-later · SDKs and the auditor Apache-2.0 · this documentation CC BY 4.0. If a doc and the running stack disagree, trust the stack. Legal notice (Impressum) · Privacy · Terms