traia-iatp 0.1.2__py3-none-any.whl → 0.1.67__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. traia_iatp/__init__.py +105 -8
  2. traia_iatp/cli/main.py +85 -1
  3. traia_iatp/client/__init__.py +28 -3
  4. traia_iatp/client/crewai_a2a_tools.py +32 -12
  5. traia_iatp/client/d402_a2a_client.py +348 -0
  6. traia_iatp/contracts/__init__.py +11 -0
  7. traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
  8. traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
  9. traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
  10. traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
  11. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  12. traia_iatp/contracts/wallet_creator.py +369 -0
  13. traia_iatp/core/models.py +17 -3
  14. traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
  15. traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
  16. traia_iatp/d402/README.md +489 -0
  17. traia_iatp/d402/__init__.py +54 -0
  18. traia_iatp/d402/asgi_wrapper.py +469 -0
  19. traia_iatp/d402/chains.py +102 -0
  20. traia_iatp/d402/client.py +150 -0
  21. traia_iatp/d402/clients/__init__.py +7 -0
  22. traia_iatp/d402/clients/base.py +218 -0
  23. traia_iatp/d402/clients/httpx.py +266 -0
  24. traia_iatp/d402/common.py +114 -0
  25. traia_iatp/d402/encoding.py +28 -0
  26. traia_iatp/d402/examples/client_example.py +197 -0
  27. traia_iatp/d402/examples/server_example.py +171 -0
  28. traia_iatp/d402/facilitator.py +481 -0
  29. traia_iatp/d402/mcp_middleware.py +296 -0
  30. traia_iatp/d402/models.py +116 -0
  31. traia_iatp/d402/networks.py +98 -0
  32. traia_iatp/d402/path.py +43 -0
  33. traia_iatp/d402/payment_introspection.py +126 -0
  34. traia_iatp/d402/payment_signing.py +183 -0
  35. traia_iatp/d402/price_builder.py +164 -0
  36. traia_iatp/d402/servers/__init__.py +61 -0
  37. traia_iatp/d402/servers/base.py +139 -0
  38. traia_iatp/d402/servers/example_general_server.py +140 -0
  39. traia_iatp/d402/servers/fastapi.py +253 -0
  40. traia_iatp/d402/servers/mcp.py +304 -0
  41. traia_iatp/d402/servers/starlette.py +878 -0
  42. traia_iatp/d402/starlette_middleware.py +529 -0
  43. traia_iatp/d402/types.py +300 -0
  44. traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
  45. traia_iatp/mcp/__init__.py +3 -0
  46. traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
  47. traia_iatp/mcp/mcp_agent_template.py +78 -13
  48. traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
  49. traia_iatp/mcp/templates/README.md.j2 +104 -8
  50. traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
  51. traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
  52. traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
  53. traia_iatp/mcp/templates/env.example.j2 +60 -0
  54. traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
  55. traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
  56. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  57. traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
  58. traia_iatp/mcp/templates/server.py.j2 +174 -197
  59. traia_iatp/mcp/traia_mcp_adapter.py +182 -20
  60. traia_iatp/registry/__init__.py +47 -12
  61. traia_iatp/registry/atlas_search_indexes.json +108 -54
  62. traia_iatp/registry/iatp_search_api.py +169 -39
  63. traia_iatp/registry/mongodb_registry.py +241 -69
  64. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
  65. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
  66. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
  67. traia_iatp/registry/readmes/README.md +3 -3
  68. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
  69. traia_iatp/scripts/__init__.py +2 -0
  70. traia_iatp/scripts/create_wallet.py +244 -0
  71. traia_iatp/server/a2a_server.py +22 -7
  72. traia_iatp/server/iatp_server_template_generator.py +23 -0
  73. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  74. traia_iatp/server/templates/Dockerfile.j2 +23 -1
  75. traia_iatp/server/templates/README.md +2 -2
  76. traia_iatp/server/templates/README.md.j2 +5 -5
  77. traia_iatp/server/templates/__main__.py.j2 +374 -66
  78. traia_iatp/server/templates/agent.py.j2 +12 -11
  79. traia_iatp/server/templates/agent_config.json.j2 +3 -3
  80. traia_iatp/server/templates/agent_executor.py.j2 +45 -27
  81. traia_iatp/server/templates/env.example.j2 +32 -4
  82. traia_iatp/server/templates/gitignore.j2 +7 -0
  83. traia_iatp/server/templates/pyproject.toml.j2 +13 -12
  84. traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
  85. traia_iatp/server/templates/server.py.j2 +197 -10
  86. traia_iatp/special_agencies/registry_search_agency.py +1 -1
  87. traia_iatp/utils/iatp_utils.py +6 -6
  88. traia_iatp-0.1.67.dist-info/METADATA +320 -0
  89. traia_iatp-0.1.67.dist-info/RECORD +117 -0
  90. traia_iatp-0.1.2.dist-info/METADATA +0 -414
  91. traia_iatp-0.1.2.dist-info/RECORD +0 -72
  92. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
  93. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
  94. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
  95. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Optional, Union, Dict, Literal, List
6
+ from typing_extensions import (
7
+ TypedDict,
8
+ ) # use `typing_extensions.TypedDict` instead of `typing.TypedDict` on Python < 3.12
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
11
+ from pydantic.alias_generators import to_camel
12
+
13
+ from .networks import SupportedNetworks
14
+
15
+
16
+ # Add HTTP request structure types
17
+ class HTTPVerbs(str, Enum):
18
+ GET = "GET"
19
+ POST = "POST"
20
+ PUT = "PUT"
21
+ DELETE = "DELETE"
22
+ PATCH = "PATCH"
23
+ OPTIONS = "OPTIONS"
24
+ HEAD = "HEAD"
25
+
26
+
27
+ class HTTPInputSchema(BaseModel):
28
+ """Schema for HTTP request input, excluding spec and method which are handled by the middleware"""
29
+
30
+ query_params: Optional[Dict[str, str]] = None
31
+ body_type: Optional[
32
+ Literal["json", "form-data", "multipart-form-data", "text", "binary"]
33
+ ] = None
34
+ body_fields: Optional[Dict[str, Any]] = None
35
+ header_fields: Optional[Dict[str, Any]] = None
36
+
37
+ model_config = ConfigDict(
38
+ alias_generator=to_camel,
39
+ populate_by_name=True,
40
+ from_attributes=True,
41
+ )
42
+
43
+
44
+ class HTTPRequestStructure(HTTPInputSchema):
45
+ """Complete HTTP request structure including protocol type and method"""
46
+
47
+ type: Literal["http"]
48
+ method: HTTPVerbs
49
+
50
+
51
+ # For now we only support HTTP, but could add MCP and OpenAPI later
52
+ RequestStructure = HTTPRequestStructure
53
+
54
+
55
+ class TokenAmount(BaseModel):
56
+ """Represents an amount of tokens in atomic units with asset information"""
57
+
58
+ amount: str
59
+ asset: TokenAsset
60
+
61
+ @field_validator("amount")
62
+ def validate_amount(cls, v):
63
+ try:
64
+ int(v)
65
+ except ValueError:
66
+ raise ValueError("amount must be an integer encoded as a string")
67
+ return v
68
+
69
+
70
+ class TokenAsset(BaseModel):
71
+ """Represents token asset information including EIP-712 domain data and network"""
72
+
73
+ address: str
74
+ decimals: int
75
+ eip712: EIP712Domain
76
+ network: Optional[str] = None # Blockchain network (e.g., "sepolia", "base-sepolia")
77
+
78
+ @field_validator("decimals")
79
+ def validate_decimals(cls, v):
80
+ if v < 0 or v > 255:
81
+ raise ValueError("decimals must be between 0 and 255")
82
+ return v
83
+
84
+
85
+ class EIP712Domain(BaseModel):
86
+ """EIP-712 domain information for token signing"""
87
+
88
+ name: str
89
+ version: str
90
+
91
+
92
+ # Price can be either Money (USD string) or TokenAmount
93
+ Money = Union[str, int] # e.g., "$0.01", 0.01, "0.001"
94
+ Price = Union[Money, TokenAmount]
95
+
96
+
97
+ class PaymentRequirements(BaseModel):
98
+ scheme: str
99
+ network: SupportedNetworks
100
+ max_amount_required: str
101
+ resource: str
102
+ description: str
103
+ mime_type: str
104
+ output_schema: Optional[Any] = None
105
+ pay_to: str
106
+ max_timeout_seconds: int
107
+ asset: str
108
+ extra: Optional[dict[str, Any]] = None
109
+
110
+ model_config = ConfigDict(
111
+ alias_generator=to_camel,
112
+ populate_by_name=True,
113
+ from_attributes=True,
114
+ )
115
+
116
+ @field_validator("max_amount_required")
117
+ def validate_max_amount_required(cls, v):
118
+ try:
119
+ int(v)
120
+ except ValueError:
121
+ raise ValueError(
122
+ "max_amount_required must be an integer encoded as a string"
123
+ )
124
+ return v
125
+
126
+
127
+ # Returned by a server as json alongside a 402 response code
128
+ class d402PaymentRequiredResponse(BaseModel):
129
+ d402_version: int
130
+ accepts: list[PaymentRequirements]
131
+ error: str
132
+
133
+ model_config = ConfigDict(
134
+ alias_generator=to_camel,
135
+ populate_by_name=True,
136
+ from_attributes=True,
137
+ )
138
+
139
+
140
+ class PullFundsAuthorization(BaseModel):
141
+ """
142
+ Authorization data for payment header (wire format).
143
+
144
+ This structure is sent in the payment header and includes fields for:
145
+ - EIP-712 signature: wallet, provider, token, amount, deadline, requestPath
146
+ - Transport metadata: valid_after, valid_before (for payment window)
147
+
148
+ Note: Only some fields are signed (see IATPWallet.sol PULL_FUNDS_FOR_SETTLEMENT_TYPEHASH)
149
+ """
150
+ from_: str = Field(alias="from") # Consumer's IATPWallet address
151
+ to: str # Provider's IATPWallet address
152
+ value: str # Payment amount
153
+ valid_after: str = Field(alias="validAfter") # Not in signature (transport only)
154
+ valid_before: str = Field(alias="validBefore") # Maps to 'deadline' in signature
155
+ request_path: str = Field(alias="requestPath") # API path (signed)
156
+
157
+ model_config = ConfigDict(
158
+ alias_generator=to_camel,
159
+ populate_by_name=True,
160
+ from_attributes=True,
161
+ )
162
+
163
+ @field_validator("value")
164
+ def validate_value(cls, v):
165
+ try:
166
+ int(v)
167
+ except ValueError:
168
+ raise ValueError("value must be an integer encoded as a string")
169
+ return v
170
+
171
+
172
+ class ExactPaymentPayload(BaseModel):
173
+ """Payment payload with PullFundsForSettlement signature."""
174
+ signature: str
175
+ authorization: PullFundsAuthorization
176
+
177
+
178
+ class VerifyResponse(BaseModel):
179
+ is_valid: bool = Field(alias="isValid")
180
+ invalid_reason: Optional[str] = Field(None, alias="invalidReason")
181
+ payer: Optional[str]
182
+ payment_uuid: Optional[str] = Field(None, alias="paymentUuid") # Unique payment identifier from facilitator
183
+ facilitator_fee_percent: Optional[int] = Field(250, alias="facilitatorFeePercent") # Fee percent from facilitator (default 2.5% = 250 basis points)
184
+
185
+ model_config = ConfigDict(
186
+ alias_generator=to_camel,
187
+ populate_by_name=True,
188
+ from_attributes=True,
189
+ )
190
+
191
+
192
+ class SettleResponse(BaseModel):
193
+ success: bool
194
+ error_reason: Optional[str] = None
195
+ transaction: Optional[str] = None
196
+ network: Optional[str] = None
197
+ payer: Optional[str] = None
198
+
199
+ model_config = ConfigDict(
200
+ alias_generator=to_camel,
201
+ populate_by_name=True,
202
+ from_attributes=True,
203
+ )
204
+
205
+
206
+ # Union of payloads for each scheme
207
+ SchemePayloads = ExactPaymentPayload
208
+
209
+
210
+ class PaymentPayload(BaseModel):
211
+ d402_version: int
212
+ scheme: str
213
+ network: str
214
+ payload: SchemePayloads
215
+
216
+ model_config = ConfigDict(
217
+ alias_generator=to_camel,
218
+ populate_by_name=True,
219
+ from_attributes=True,
220
+ )
221
+
222
+
223
+ class D402Headers(BaseModel):
224
+ x_payment: str
225
+
226
+
227
+ class UnsupportedSchemeException(Exception):
228
+ pass
229
+
230
+
231
+ class PaywallConfig(TypedDict, total=False):
232
+ """Configuration for paywall UI customization"""
233
+
234
+ cdp_client_key: str
235
+ app_name: str
236
+ app_logo: str
237
+ session_token_endpoint: str
238
+
239
+
240
+ class DiscoveredResource(BaseModel):
241
+ """A discovery resource represents a discoverable resource in the D402 ecosystem."""
242
+
243
+ resource: str
244
+ type: str = Field(..., pattern="^http$") # Currently only supports 'http'
245
+ d402_version: int = Field(..., alias="d402Version")
246
+ accepts: List["PaymentRequirements"]
247
+ last_updated: datetime = Field(
248
+ ...,
249
+ alias="lastUpdated",
250
+ description="ISO 8601 formatted datetime string with UTC timezone (e.g. 2025-08-09T01:07:04.005Z)",
251
+ )
252
+ metadata: Optional[dict] = None
253
+
254
+ model_config = ConfigDict(
255
+ alias_generator=to_camel,
256
+ populate_by_name=True,
257
+ from_attributes=True,
258
+ )
259
+
260
+
261
+ class ListDiscoveryResourcesRequest(BaseModel):
262
+ """Request parameters for listing discovery resources."""
263
+
264
+ type: Optional[str] = None
265
+ limit: Optional[int] = None
266
+ offset: Optional[int] = None
267
+
268
+ model_config = ConfigDict(
269
+ alias_generator=to_camel,
270
+ populate_by_name=True,
271
+ from_attributes=True,
272
+ )
273
+
274
+
275
+ class DiscoveryResourcesPagination(BaseModel):
276
+ """Pagination information for discovery resources responses."""
277
+
278
+ limit: int
279
+ offset: int
280
+ total: int
281
+
282
+ model_config = ConfigDict(
283
+ alias_generator=to_camel,
284
+ populate_by_name=True,
285
+ from_attributes=True,
286
+ )
287
+
288
+
289
+ class ListDiscoveryResourcesResponse(BaseModel):
290
+ """Response from the discovery resources endpoint."""
291
+
292
+ d402_version: int = Field(..., alias="d402Version")
293
+ items: List[DiscoveredResource]
294
+ pagination: DiscoveryResourcesPagination
295
+
296
+ model_config = ConfigDict(
297
+ alias_generator=to_camel,
298
+ populate_by_name=True,
299
+ from_attributes=True,
300
+ )
@@ -0,0 +1,357 @@
1
+ # d402 Payment Flow in MCP Adapter
2
+
3
+ This document explains how the `TraiaMCPAdapter` integrates with d402 payment protocol to handle HTTP 402 Payment Required responses from MCP servers.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────┐
9
+ │ CrewAI Agent/Task │
10
+ │ Calls MCP tool (e.g., get_prompt) │
11
+ └───────────────────────┬─────────────────────────────────────────┘
12
+
13
+
14
+ ┌─────────────────────────────────────────────────────────────────┐
15
+ │ TraiaMCPAdapter (extends MCPServerAdapter) │
16
+ │ - Monkey-patches httpx.AsyncClient.__init__ │
17
+ │ - Adds d402 payment hooks to all httpx clients │
18
+ └───────────────────────┬─────────────────────────────────────────┘
19
+
20
+
21
+ ┌─────────────────────────────────────────────────────────────────┐
22
+ │ MCPServerAdapter (from crewai_tools) │
23
+ │ - Creates httpx.AsyncClient instances │
24
+ │ - Makes MCP protocol requests (tools/call) │
25
+ │ - Returns BaseTool instances │
26
+ └───────────────────────┬─────────────────────────────────────────┘
27
+
28
+
29
+ ┌─────────────────────────────────────────────────────────────────┐
30
+ │ httpx.AsyncClient (with d402 hooks applied) │
31
+ │ - Event hooks: on_request, on_response │
32
+ │ - Automatically handles 402 responses │
33
+ └───────────────────────┬─────────────────────────────────────────┘
34
+
35
+
36
+ ┌─────────────────────────────────────────────────────────────────┐
37
+ │ MCP Server (FastMCP) │
38
+ │ - Receives tools/call request │
39
+ │ - Returns 402 Payment Required if no payment │
40
+ │ - Processes request if payment provided │
41
+ └─────────────────────────────────────────────────────────────────┘
42
+ ```
43
+
44
+ ## Detailed Flow
45
+
46
+ ### 1. Initialization Phase
47
+
48
+ When `TraiaMCPAdapter` is created with d402 payment support:
49
+
50
+ ```python
51
+ adapter = create_mcp_adapter_with_x402(
52
+ url="http://localhost:8000/mcp",
53
+ account=client_account,
54
+ max_value=1_000_000
55
+ )
56
+ ```
57
+
58
+ **What happens:**
59
+ 1. `TraiaMCPAdapter.__init__()` is called
60
+ 2. If `d402_account` is provided, `_apply_d402_patch()` is called
61
+ 3. This monkey-patches `httpx.AsyncClient.__init__` globally
62
+ 4. The patch ensures ALL future `httpx.AsyncClient` instances get d402 payment hooks
63
+
64
+ ```python
65
+ def _apply_d402_patch(self):
66
+ original_init = httpx.AsyncClient.__init__
67
+
68
+ def patched_init(client_self, *args, **kwargs):
69
+ # Call original init first
70
+ original_init(client_self, *args, **kwargs)
71
+
72
+ # Add d402 payment hooks to the client
73
+ hooks = d402_payment_hooks(d402_account, max_value=d402_max_value)
74
+ client_self.event_hooks = hooks # or merge with existing
75
+ ```
76
+
77
+ ### 2. Context Manager Entry
78
+
79
+ When entering the adapter context:
80
+
81
+ ```python
82
+ with adapter as tools:
83
+ # Tools are now available
84
+ ```
85
+
86
+ **What happens:**
87
+ 1. `TraiaMCPAdapter.__enter__()` is called
88
+ 2. This calls `super().__enter__()` which calls `MCPServerAdapter.__enter__()`
89
+ 3. `MCPServerAdapter` creates an `httpx.AsyncClient` instance
90
+ 4. **Because of our monkey-patch, this client automatically gets d402 hooks**
91
+ 5. `MCPServerAdapter` makes an initial request to list tools
92
+ 6. Returns list of `BaseTool` instances
93
+
94
+ ### 3. Tool Execution Phase
95
+
96
+ When a CrewAI agent calls an MCP tool:
97
+
98
+ ```python
99
+ result = tool._run(query="test")
100
+ ```
101
+
102
+ **What happens:**
103
+ 1. The `BaseTool` (created by `MCPServerAdapter`) executes
104
+ 2. Internally, it uses the `httpx.AsyncClient` that was created in step 2
105
+ 3. Makes an MCP protocol request: `POST /mcp` with JSON-RPC:
106
+ ```json
107
+ {
108
+ "jsonrpc": "2.0",
109
+ "id": 1,
110
+ "method": "tools/call",
111
+ "params": {
112
+ "name": "get_prompt",
113
+ "arguments": {"query": "test"}
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### 4. First Request (No Payment)
119
+
120
+ ```
121
+ Client → Server: POST /mcp (no X-Payment header)
122
+ Server → Client: HTTP 402 Payment Required
123
+ ```
124
+
125
+ **Server Response:**
126
+ ```json
127
+ {
128
+ "jsonrpc": "2.0",
129
+ "id": 1,
130
+ "error": {
131
+ "code": 402,
132
+ "message": "Payment required",
133
+ "data": {
134
+ "d402Version": 1,
135
+ "accepts": [
136
+ {
137
+ "scheme": "exact",
138
+ "network": "base-sepolia",
139
+ "maxAmountRequired": "1000",
140
+ "payTo": "0x...",
141
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
142
+ ...
143
+ }
144
+ ]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### 5. d402 Hook Intercepts 402 Response
151
+
152
+ The `HttpxHooks.on_response()` method is automatically called:
153
+
154
+ ```python
155
+ async def on_response(self, response: Response) -> Response:
156
+ if response.status_code != 402:
157
+ return response # Not a 402, pass through
158
+
159
+ # Parse payment requirements
160
+ data = response.json()
161
+ payment_response = d402PaymentRequiredResponse(**data)
162
+
163
+ # Select payment requirements (matches token/network)
164
+ selected_requirements = self.client.select_payment_requirements(
165
+ payment_response.accepts
166
+ )
167
+
168
+ # Create signed payment header
169
+ payment_header = self.client.create_payment_header(
170
+ selected_requirements, payment_response.d402_version
171
+ )
172
+
173
+ # Retry request with payment
174
+ request = response.request
175
+ request.headers["X-Payment"] = payment_header
176
+
177
+ # Retry the request
178
+ async with AsyncClient() as client:
179
+ retry_response = await client.send(request)
180
+ return retry_response
181
+ ```
182
+
183
+ **What happens:**
184
+ 1. Hook detects HTTP 402 status
185
+ 2. Parses payment requirements from response
186
+ 3. Selects appropriate payment option (matches token/network)
187
+ 4. Creates EIP-3009 signed payment authorization using client's account
188
+ 5. Base64-encodes the payment header
189
+ 6. Retries the original request with `X-Payment` header
190
+
191
+ ### 6. Payment Header Creation
192
+
193
+ The `d402Client.create_payment_header()` method:
194
+
195
+ ```python
196
+ def create_payment_header(self, payment_requirements, d402_version):
197
+ # Creates unsigned header structure
198
+ unsigned_header = {
199
+ "d402Version": d402_version,
200
+ "scheme": payment_requirements.scheme,
201
+ "network": payment_requirements.network,
202
+ "payload": {
203
+ "signature": None,
204
+ "authorization": {
205
+ "from": self.account.address, # CLIENT's address
206
+ "to": payment_requirements.pay_to, # SERVER's address
207
+ "value": payment_requirements.max_amount_required,
208
+ "validAfter": timestamp - 60,
209
+ "validBefore": timestamp + timeout,
210
+ "nonce": random_nonce(),
211
+ },
212
+ },
213
+ }
214
+
215
+ # Signs with EIP-3009 transferWithAuthorization
216
+ signed_header = sign_payment_header(
217
+ self.account, # CLIENT signs
218
+ payment_requirements,
219
+ unsigned_header
220
+ )
221
+
222
+ return base64_encode(signed_header)
223
+ ```
224
+
225
+ **Key Points:**
226
+ - **CLIENT signs**: The payment is signed by the client's account (`d402_account`)
227
+ - **SERVER receives**: Payment is sent to server's address (`pay_to`)
228
+ - **EIP-3009**: Uses transferWithAuthorization (gasless payment)
229
+ - **Base64 encoded**: Payment header is base64-encoded for HTTP header
230
+
231
+ ### 7. Retry Request (With Payment)
232
+
233
+ ```
234
+ Client → Server: POST /mcp (with X-Payment header)
235
+ Server → Client: HTTP 200 OK (with result)
236
+ ```
237
+
238
+ **Request Headers:**
239
+ ```
240
+ X-Payment: <base64-encoded-signed-payment>
241
+ Content-Type: application/json
242
+ ```
243
+
244
+ **Server Processing:**
245
+ 1. MCP server's `D402MCPMiddleware` extracts `X-Payment` header
246
+ 2. Decodes and validates the payment signature
247
+ 3. Verifies payment matches endpoint requirements
248
+ 4. Processes the tool call
249
+ 5. Returns result
250
+
251
+ **Server Response:**
252
+ ```json
253
+ {
254
+ "jsonrpc": "2.0",
255
+ "id": 1,
256
+ "result": {
257
+ "content": [
258
+ {
259
+ "type": "text",
260
+ "text": "Tool result here..."
261
+ }
262
+ ]
263
+ }
264
+ }
265
+ ```
266
+
267
+ ## Key Components
268
+
269
+ ### 1. Monkey-Patching Strategy
270
+
271
+ **Why monkey-patch?**
272
+ - `MCPServerAdapter` is from `crewai_tools` - we don't control its implementation
273
+ - It creates `httpx.AsyncClient` instances internally
274
+ - We need to inject d402 hooks into those clients
275
+ - Monkey-patching `__init__` ensures ALL httpx clients get hooks
276
+
277
+ **Limitations:**
278
+ - Global patch affects ALL httpx clients (not just MCP)
279
+ - Must restore original `__init__` in `__exit__`
280
+ - Thread-safety: Only one adapter should be active at a time
281
+
282
+ ### 2. Event Hooks
283
+
284
+ httpx event hooks are called automatically:
285
+ - `on_request`: Before request is sent (currently no-op)
286
+ - `on_response`: After response is received (handles 402)
287
+
288
+ ### 3. Payment Flow
289
+
290
+ ```
291
+ 1. Tool call → httpx request
292
+ 2. Server returns 402
293
+ 3. Hook intercepts 402
294
+ 4. Creates signed payment
295
+ 5. Retries request with X-Payment header
296
+ 6. Server processes and returns result
297
+ ```
298
+
299
+ ## Example: Complete Flow
300
+
301
+ ```python
302
+ from eth_account import Account
303
+ from traia_iatp.mcp.mcp_agent_template import run_with_mcp_tools, MCPServerInfo
304
+ from crewai import Agent, Task
305
+
306
+ # 1. Setup
307
+ account = Account.from_key("0x...") # CLIENT's account
308
+ mcp_server = MCPServerInfo(
309
+ id="canza-001",
310
+ name="canza-mcp",
311
+ url="http://localhost:8000/mcp",
312
+ ...
313
+ )
314
+
315
+ # 2. Create agent and task
316
+ agent = MCPAgentBuilder.create_agent(...)
317
+ task = Task(description="Call get_prompt", agent=agent)
318
+
319
+ # 3. Run with d402 payment
320
+ result = run_with_mcp_tools(
321
+ tasks=[task],
322
+ mcp_server=mcp_server,
323
+ d402_account=account, # CLIENT's account for signing
324
+ d402_max_value=1_000_000 # Safety limit
325
+ )
326
+
327
+ # What happens internally:
328
+ # 1. TraiaMCPAdapter patches httpx.AsyncClient.__init__
329
+ # 2. MCPServerAdapter creates httpx client (gets hooks automatically)
330
+ # 3. Agent calls tool → httpx makes request
331
+ # 4. Server returns 402 → hook intercepts
332
+ # 5. Hook creates payment → retries with X-Payment header
333
+ # 6. Server processes → returns result
334
+ # 7. Result returned to agent
335
+ ```
336
+
337
+ ## Important Notes
338
+
339
+ 1. **Client Account**: The `d402_account` is the CLIENT's account that signs payments
340
+ 2. **Server Address**: The server's payment address is configured server-side (SERVER_ADDRESS env var)
341
+ 3. **Automatic**: The entire payment flow is automatic - agents don't need to handle 402s
342
+ 4. **Transparent**: CrewAI agents see normal tool execution, payment happens behind the scenes
343
+ 5. **Per-Request**: Each tool call that requires payment goes through this flow
344
+
345
+ ## Troubleshooting
346
+
347
+ **Payment not working?**
348
+ - Check that monkey-patch is applied (look for debug logs)
349
+ - Verify httpx client has event_hooks set
350
+ - Check that 402 response is being intercepted
351
+ - Verify payment signature is valid
352
+
353
+ **Hooks not applied?**
354
+ - Ensure `d402_account` is provided
355
+ - Check that `D402_AVAILABLE` is True
356
+ - Verify httpx.AsyncClient is being created AFTER patch is applied
357
+
@@ -3,6 +3,7 @@
3
3
  from .client import MCPClient
4
4
  from .mcp_agent_template import MCPServerConfig, MCPAgentBuilder, run_with_mcp_tools, MCPServerInfo
5
5
  from .traia_mcp_adapter import TraiaMCPAdapter, create_mcp_adapter
6
+ from .d402_mcp_tool_adapter import D402MCPToolAdapter, create_d402_mcp_adapter
6
7
 
7
8
  __all__ = [
8
9
  "MCPClient",
@@ -12,4 +13,6 @@ __all__ = [
12
13
  "MCPServerInfo",
13
14
  "TraiaMCPAdapter",
14
15
  "create_mcp_adapter",
16
+ "D402MCPToolAdapter",
17
+ "create_d402_mcp_adapter",
15
18
  ]