Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions extensions/settlement/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Settlement Extension

This directory contains the specification for the A2A Settlement Extension
(A2A-SE), which adds escrow-based token settlement to the A2A task lifecycle.

The extension enables agents to:

- Declare skill-level pricing in their Agent Cards
- Hold funds in escrow while work is in progress
- Release payment on task completion, refund on failure
- Resolve disputes when requester and provider disagree

A2A-SE is a **data + profile extension**: it adds structured settlement data to
Agent Cards and overlays settlement metadata onto the core request-response
messages via the `metadata` field. It does not add new RPC methods or task
states to the core protocol.

The v1 directory contains the specification document. A library implementation
in Python is present in `samples/python/extensions/settlement`. A live reference
exchange is available at <https://exchange.a2a-settlement.org/api/v1>.

## Resources

- [Full specification](https://github.com/a2a-settlement/a2a-settlement/blob/main/SPEC.md)
- Python SDK: `pip install a2a-settlement` ([source](https://github.com/a2a-settlement/a2a-settlement/tree/main/sdk))
- TypeScript SDK: `npm install @a2a-settlement/sdk` ([source](https://github.com/a2a-settlement/a2a-settlement/tree/main/sdk-ts))
- [Live exchange API docs](https://exchange.a2a-settlement.org/docs)
265 changes: 265 additions & 0 deletions extensions/settlement/v1/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# A2A Settlement Extension (A2A-SE) — v1

## Overview

The A2A Settlement Extension adds escrow-based token settlement to the A2A task
lifecycle. Agents declare pricing in their Agent Cards. Clients create escrow
before sending a task, and release or refund based on the terminal task state.

A2A-SE requires zero modifications to the core A2A protocol. It uses the
existing `capabilities.extensions` mechanism for Agent Card integration, and
the existing `metadata` field on Messages and Tasks for settlement context.

## Extension URI

```text
https://a2a-settlement.org/extensions/settlement/v1
```

## Agent Card Declaration

Agents that support settlement declare the extension in their Agent Card's
`capabilities.extensions` array:

```json
{
"capabilities": {
"extensions": [
{
"uri": "https://a2a-settlement.org/extensions/settlement/v1",
"description": "Accepts token-based payment via A2A Settlement Exchange",
"required": false,
"params": {
"exchangeUrls": [
"https://exchange.a2a-settlement.org/api/v1"
],
"preferredExchange": "https://exchange.a2a-settlement.org/api/v1",
"accountIds": {
"https://exchange.a2a-settlement.org/api/v1": "provider-uuid"
},
"pricing": {
"sentiment-analysis": {
"baseTokens": 10,
"model": "per-request",
"currency": "ATE"
}
},
"reputation": 0.87,
"availability": 0.95
}
}
]
}
}
```

### Extension Params

| Field | Type | Required | Description |
|---------------------|----------|----------|------------------------------------------------------------|
| `exchangeUrls` | string[] | Yes | Exchange endpoints the agent is registered on |
| `preferredExchange` | string | No | The agent's preferred exchange from the list |
| `accountIds` | object | Yes | Map of exchange URL to agent's account ID on that exchange |
| `pricing` | object | No | Map of skill ID to pricing configuration |
| `currency` | string | No | Default currency (default: `ATE`) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

For the currency field in the Extension Params table, the 'Required' column states 'No', but the description mentions 'Default currency (default: ATE)'. While this is technically correct, it might be clearer to indicate that it's optional but has a default value, or add a note about the default in the 'Required' column itself for better readability.

| `reputation` | number | No | Agent's reputation score (0.0 – 1.0) |
| `availability` | number | No | Agent's availability score (0.0 – 1.0) |

When `required` is `false`, the agent accepts both paid and unpaid requests
(freemium model). When `required` is `true`, the agent rejects tasks that do
not include settlement metadata.

### Pricing Models

| Model | Description | Example |
|---------------|------------------------------------------|----------------------------------|
| `per-request` | Fixed cost per task invocation | 10 tokens per sentiment analysis |
| `per-unit` | Cost per unit of input (per 1K chars) | 2 tokens per 1,000 characters |
| `per-minute` | Cost per minute of processing time | 5 tokens per minute of compute |
| `negotiable` | Price determined during task negotiation | Agent proposes price in response |

## Settlement Flow Mapped to A2A TaskStates

Settlement actions map directly to existing A2A TaskState transitions. No new
task states are required.

```text
A2A TaskState Settlement Action
───────────── ─────────────────
SUBMITTED ──────► Client creates escrow on exchange
WORKING ──────► No action (escrow holds)
INPUT_REQUIRED ──────► No action (escrow holds during multi-turn)
COMPLETED ──────► Client releases escrow (tokens → provider)
FAILED ──────► Client refunds escrow (tokens → client)
CANCELED ──────► Client refunds escrow (tokens → client)
REJECTED ──────► Client refunds escrow (tokens → client)
```

## Settlement Metadata

Settlement context is passed through A2A's existing `metadata` field using a
namespaced key `a2a-se` to avoid collisions.

### Client's Initial Message

When creating a task, the client includes the escrow reference:

```json
{
"messageId": "msg-uuid",
"role": "user",
"parts": [
{ "text": "Analyze the sentiment of this earnings transcript." }
],
"metadata": {
"a2a-se": {
"escrowId": "escrow-uuid-from-exchange",
"amount": 10,
"feeAmount": 1,
"exchangeUrl": "https://exchange.a2a-settlement.org/api/v1",
"expiresAt": "2026-02-17T12:30:00Z"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The expiresAt field is shown in the example client message metadata. However, the Python implementation's build_metadata function explicitly removes this field if its value is falsy (e.g., None or empty string). To avoid potential confusion, please clarify in the specification whether expiresAt is an optional field in the a2a-se metadata block.

}
}
}
```

### Provider's Task Response

The provider acknowledges the escrow in its response metadata:

```json
{
"id": "task-uuid",
"status": {
"state": "TASK_STATE_WORKING"
},
"metadata": {
"a2a-se": {
"escrowId": "escrow-uuid-from-exchange",
"settlementStatus": "acknowledged"
}
}
}
```

### Settlement Status Values

| Status | Meaning |
|----------------|----------------------------------------------------|
| `pending` | Escrow created, awaiting agent acknowledgment |
| `acknowledged` | Agent confirmed receipt of escrow reference |
| `review` | Task completed, requester reviewing before release |
| `released` | Tokens transferred to provider |
| `refunded` | Tokens returned to requester |
| `expired` | Escrow TTL exceeded without resolution |
| `disputed` | Transaction flagged by either party |

## Settlement Exchange API

The settlement exchange is an external REST API that manages accounts, escrow,
and token balances. The exchange is an **interface**: any conforming
implementation (hosted, self-hosted, on-chain) can serve as the settlement
rail.

### Core Endpoints

| Method | Path | Description |
|--------|--------------------------|-----------------------------------------------------------------------|
| `POST` | `/exchange/escrow` | Create an escrow |
| `POST` | `/exchange/release` | Release escrowed tokens to provider. Body: `{"escrow_id": "<id>"}` |
| `POST` | `/exchange/refund` | Refund escrowed tokens to requester. Body: `{"escrow_id": "<id>"}` |
| `POST` | `/exchange/dispute` | Flag an escrow as disputed. Body: `{"escrow_id": "<id>"}` |
| `GET` | `/exchange/escrows/{id}` | Look up an escrow by ID |
Comment on lines +168 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The API endpoint design could be more aligned with REST conventions for better clarity and predictability.

  1. Inconsistent Pluralization: The path for creating an escrow is POST /exchange/escrow (singular), while looking one up is GET /exchange/escrows/{id} (plural). It's conventional to use the plural form for the resource collection. I suggest POST /exchange/escrows.

  2. RPC-style Actions: The release, refund, and dispute operations are modeled as top-level endpoints taking an escrow_id in the body. A more RESTful approach would be to model these as actions on a specific escrow resource, for example: POST /exchange/escrows/{id}/release.

Here's a suggested revision of the endpoints table:

Method Path Description
POST /exchange/escrows Create an escrow
POST /exchange/escrows/{id}/release Release escrowed tokens to provider
POST /exchange/escrows/{id}/refund Refund escrowed tokens to requester
POST /exchange/escrows/{id}/dispute Flag an escrow as disputed
GET /exchange/escrows/{id} Look up an escrow by ID

This would make the API more predictable and easier to use.

| `GET` | `/exchange/balance` | Get agent's token balance |
Comment on lines +166 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's an inconsistency in the JSON key naming convention within the specification. The Settlement Metadata sections (lines 103-143) use camelCase (e.g., escrowId, exchangeUrl), which is common for JSON APIs. However, this Core Endpoints table and the Batch Escrow for Pipelines section (lines 175-202) specify snake_case keys (e.g., escrow_id, provider_id).

To ensure consistency and prevent implementation errors, it would be best to use a single convention throughout the spec. I'd recommend standardizing on camelCase, as it's already used in other parts of the A2A protocol.

Comment on lines +166 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The API endpoint paths in the "Core Endpoints" table (e.g., /exchange/escrow, /exchange/release) appear inconsistent with RESTful practices and the linked reference implementation (https://exchange.a2a-settlement.org/docs). The reference implementation uses paths like /api/v1/escrow and /api/v1/escrow/{escrow_id}/release. To avoid confusion for implementers, the specification should align with the reference implementation.

A more conventional RESTful design would look like this:

| Method | Path                            | Description                               |
|--------|---------------------------------|-------------------------------------------|
| `POST` | `/escrows`                      | Create an escrow                          |
| `POST` | `/escrows/{id}/release`         | Release escrowed tokens to provider       |
| `POST` | `/escrows/{id}/refund`          | Refund escrowed tokens to requester       |
| `POST` | `/escrows/{id}/dispute`         | Flag an escrow as disputed                |
| `GET`  | `/escrows/{id}`                 | Look up an escrow by ID                   |
| `GET`  | `/balance`                      | Get agent's token balance                 |


### Batch Escrow for Pipelines

Multi-step workflows can create all escrows atomically with dependency ordering:

```json
{
"group_id": "pipeline-001",
"escrows": [
{
"provider_id": "translator-agent",
"amount": 10,
"task_type": "translate"
},
{
"provider_id": "sentiment-agent",
"amount": 15,
"task_type": "sentiment",
"depends_on": ["$0"]
}
Comment on lines +192 to +193
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The depends_on: ["$0"] syntax is intuitive, but to make the specification as clear as possible, it would be beneficial to explicitly define that this notation refers to other escrows in the same batch by their 0-based index.

]
}
```

Values in `depends_on` use positional references (`$0`, `$1`, ...) referring to
other escrows in the same batch by their zero-based index. The field models
sequential dependencies: an escrow cannot be released until all upstream escrows
have been released. When an upstream escrow is refunded, all downstream escrows
are automatically cascade-refunded.

## Extension Activation

Clients activate the settlement extension using the `A2A-Extensions` HTTP
header:

```http
POST /agents/provider HTTP/1.1
A2A-Extensions: https://a2a-settlement.org/extensions/settlement/v1
Content-Type: application/json

{
"jsonrpc": "2.0",
"method": "message/send",
"id": "1",
"params": {
"message": {
"messageId": "msg-1",
"role": "user",
"parts": [{"text": "Analyze this text"}],
"metadata": {
"a2a-se": {
"escrowId": "esc-uuid",
"amount": 10,
"exchangeUrl": "https://exchange.a2a-settlement.org/api/v1"
}
}
}
}
}
```

## Client Workflow

1. **Discover** provider via Agent Card; check for settlement extension URI.
2. **Negotiate exchange** — intersect `exchangeUrls` with client's exchanges.
3. **Create escrow** on the selected exchange.
4. **Send A2A message** with `a2a-se` metadata containing the `escrowId`.
5. **On terminal state** — release (COMPLETED) or refund (FAILED/CANCELED/REJECTED).

## Provider Workflow

1. **Declare** settlement extension in Agent Card with pricing.
2. **On incoming message** — read `metadata["a2a-se"]["escrowId"]`.
3. **Verify escrow** by calling `GET /exchange/escrows/{id}` on the exchange.
4. **Execute task** normally via A2A.
5. Requester handles release/refund based on task outcome.

## Security Considerations

- All exchange endpoints require Bearer token authentication.
- Escrow creation supports idempotency keys to prevent duplicate holds.
- Escrows have configurable TTL (default 30 minutes) with automatic expiration.
- Disputes freeze the escrow until operator resolution.
- The extension MUST NOT bypass the agent's primary security controls.

## Reference Implementation

- Specification: <https://github.com/a2a-settlement/a2a-settlement/blob/main/SPEC.md>
- Python SDK: `pip install a2a-settlement`
- TypeScript SDK: `npm install @a2a-settlement/sdk`
Comment on lines +261 to +263
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's an inconsistency in how the Python and TypeScript SDKs are referenced here compared to extensions/settlement/README.md. This file mentions pip install a2a-settlement and npm install @a2a-settlement/sdk, which are package installation commands, but the README.md links to source code repositories. Please ensure consistency in how these SDKs are presented across documentation files.

- Live exchange: <https://exchange.a2a-settlement.org/api/v1>
- Live stats: <https://exchange.a2a-settlement.org/api/v1/stats>
44 changes: 44 additions & 0 deletions samples/python/extensions/settlement/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Settlement Extension Implementation

This is the Python implementation of the A2A Settlement Extension defined in
`extensions/settlement/v1`.

## What it does

Adds escrow-based token settlement to A2A agent interactions. Funds are held in
escrow while an agent works on a task, then released on completion or refunded
on failure.

## Quick start

```python
from settlement_ext import SettlementExtension

ext = SettlementExtension(
exchange_url="https://exchange.a2a-settlement.org/api/v1",
api_key="ate_your_key",
account_id="your-agent-uuid",
Comment on lines +19 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The api_key and account_id values ("ate_your_key", "your-agent-uuid") are placeholders. It would be beneficial to explicitly mention in the comments that these should be replaced with actual values, for example, by adding # Replace with your actual API key.

Suggested change
api_key="ate_your_key",
account_id="your-agent-uuid",
api_key="ate_your_key", # Replace with your actual API key
account_id="your-agent-uuid", # Replace with your actual account ID

)

# Add to your AgentCard
card = ext.add_to_card(card, pricing={
"sentiment-analysis": {"baseTokens": 10, "model": "per-request"}
})

# Server side: wrap your executor to auto-verify escrow
agent_executor = ext.wrap_executor(agent_executor)

# Client side: wrap your client to auto-settle on task completion
client = ext.wrap_client(client)
```

## Usage patterns

The extension provides several integration levels, from fully manual to fully
managed. See the `SettlementExtension` class documentation for all options.

## Live exchange

A public sandbox exchange is available at
<https://exchange.a2a-settlement.org/api/v1> for testing. Register an agent to
get an API key and 100 starter tokens.
11 changes: 11 additions & 0 deletions samples/python/extensions/settlement/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "settlement_ext"
version = "0.1.0"
description = "A2A Settlement Extension — escrow-based payment for A2A agents"
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["a2a-sdk>=0.3.0", "a2a-settlement>=0.8.0"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The dependencies are pinned to specific versions (a2a-sdk>=0.3.0, a2a-settlement>=0.8.0). For a library or extension, it's often better to use more flexible version specifiers (e.g., ~=0.3.0 or >=0.3.0, <0.4.0) to allow for minor updates and bug fixes without requiring manual intervention, unless strict pinning is explicitly desired for stability or compatibility reasons.

Suggested change
dependencies = ["a2a-sdk>=0.3.0", "a2a-settlement>=0.8.0"]
dependencies = ["a2a-sdk~=0.3.0", "a2a-settlement~=0.8.0"]


[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Loading