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,253 @@
1
+ """FastAPI middleware for d402 payment protocol.
2
+
3
+ This module provides FastAPI-specific middleware for the d402 payment protocol.
4
+ Since FastAPI is built on Starlette, this middleware wraps the Starlette middleware
5
+ with FastAPI-friendly configuration and decorators.
6
+
7
+ Example usage:
8
+
9
+ ```python
10
+ from fastapi import FastAPI
11
+ from traia_iatp.d402.servers.fastapi import D402FastAPIMiddleware, require_payment
12
+
13
+ app = FastAPI()
14
+
15
+ # Add payment middleware
16
+ middleware = D402FastAPIMiddleware(
17
+ server_address="0x...",
18
+ internal_api_key="your_api_key",
19
+ facilitator_url="https://facilitator.d402.net"
20
+ )
21
+ middleware.add_to_app(app)
22
+
23
+ # Protect specific endpoints with payment
24
+ @app.post("/api/analyze")
25
+ @require_payment(price_usd=0.01, description="Sentiment analysis")
26
+ async def analyze(request: Request):
27
+ return {"result": "analysis complete"}
28
+ ```
29
+ """
30
+
31
+ import logging
32
+ import os
33
+ from typing import Dict, Any, Optional, Callable, List
34
+ from functools import wraps
35
+
36
+ from fastapi import FastAPI, Request
37
+ from fastapi.responses import JSONResponse
38
+
39
+ from .starlette import D402PaymentMiddleware
40
+ from ..types import TokenAmount, TokenAsset, EIP712Domain, PaymentRequirements
41
+ from ..common import process_price_to_atomic_amount
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ class D402FastAPIMiddleware:
47
+ """
48
+ FastAPI-specific wrapper for d402 payment middleware.
49
+
50
+ This class provides a FastAPI-friendly interface to the underlying
51
+ Starlette middleware, with additional utilities for route protection.
52
+
53
+ Usage:
54
+ middleware = D402FastAPIMiddleware(
55
+ server_address="0x...",
56
+ internal_api_key="your_key",
57
+ facilitator_url="https://facilitator.example.com"
58
+ )
59
+ middleware.add_to_app(app)
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ server_address: str,
65
+ requires_auth: bool = False,
66
+ internal_api_key: Optional[str] = None,
67
+ testing_mode: bool = False,
68
+ facilitator_url: Optional[str] = None,
69
+ facilitator_api_key: Optional[str] = None,
70
+ server_name: Optional[str] = None,
71
+ protected_paths: Optional[List[str]] = None
72
+ ):
73
+ """Initialize FastAPI middleware.
74
+
75
+ Args:
76
+ server_address: Address where payments should be sent
77
+ requires_auth: Whether server accepts API keys for free access
78
+ internal_api_key: Server's internal API key (used when client pays)
79
+ testing_mode: If True, skip facilitator verification
80
+ facilitator_url: URL of the payment facilitator
81
+ facilitator_api_key: API key for facilitator authentication
82
+ server_name: Name/ID of this server for tracking
83
+ protected_paths: List of path patterns that require payment (e.g., ["/api/*"])
84
+ """
85
+ self.server_address = server_address
86
+ self.requires_auth = requires_auth
87
+ self.internal_api_key = internal_api_key
88
+ self.testing_mode = testing_mode
89
+ self.facilitator_url = facilitator_url
90
+ self.facilitator_api_key = facilitator_api_key
91
+ self.server_name = server_name
92
+ self.protected_paths = protected_paths or []
93
+
94
+ # Tool payment configs will be populated by decorators
95
+ self.tool_payment_configs: Dict[str, Dict[str, Any]] = {}
96
+
97
+ def add_to_app(self, app: FastAPI):
98
+ """Add the d402 payment middleware to a FastAPI app.
99
+
100
+ Args:
101
+ app: FastAPI application instance
102
+ """
103
+ app.add_middleware(
104
+ D402PaymentMiddleware,
105
+ tool_payment_configs=self.tool_payment_configs,
106
+ server_address=self.server_address,
107
+ requires_auth=self.requires_auth,
108
+ internal_api_key=self.internal_api_key,
109
+ testing_mode=self.testing_mode,
110
+ facilitator_url=self.facilitator_url,
111
+ facilitator_api_key=self.facilitator_api_key,
112
+ server_name=self.server_name
113
+ )
114
+ logger.info(f"✅ D402 payment middleware added to FastAPI app")
115
+ logger.info(f" Server address: {self.server_address}")
116
+ logger.info(f" Testing mode: {self.testing_mode}")
117
+
118
+ def register_endpoint(
119
+ self,
120
+ path: str,
121
+ price_wei: str,
122
+ token_address: str,
123
+ network: str,
124
+ description: str = ""
125
+ ):
126
+ """Register an endpoint that requires payment.
127
+
128
+ Args:
129
+ path: Endpoint path (e.g., "/api/analyze")
130
+ price_wei: Price in wei (smallest unit of token)
131
+ token_address: Token contract address
132
+ network: Network name (e.g., "sepolia", "base-mainnet")
133
+ description: Description of the service
134
+ """
135
+ self.tool_payment_configs[path] = {
136
+ "price_wei": price_wei,
137
+ "token_address": token_address,
138
+ "network": network,
139
+ "description": description,
140
+ "server_address": self.server_address
141
+ }
142
+ logger.info(f"📝 Registered payment endpoint: {path} (price: {price_wei} wei)")
143
+
144
+
145
+ def require_payment(
146
+ price_usd: Optional[float] = None,
147
+ price_wei: Optional[str] = None,
148
+ token_address: Optional[str] = None,
149
+ network: str = "sepolia",
150
+ description: str = ""
151
+ ):
152
+ """
153
+ Decorator to mark a FastAPI endpoint as requiring payment.
154
+
155
+ Can specify price in USD (will use default USDC) or in wei with custom token.
156
+
157
+ Usage with USD:
158
+ @app.post("/api/analyze")
159
+ @require_payment(price_usd=0.01, description="Sentiment analysis")
160
+ async def analyze():
161
+ return {"result": "done"}
162
+
163
+ Usage with custom token:
164
+ @app.post("/api/analyze")
165
+ @require_payment(
166
+ price_wei="1000",
167
+ token_address="0x...",
168
+ network="base-mainnet",
169
+ description="Sentiment analysis"
170
+ )
171
+ async def analyze():
172
+ return {"result": "done"}
173
+
174
+ Args:
175
+ price_usd: Price in USD (uses default USDC token)
176
+ price_wei: Price in wei (smallest unit of token)
177
+ token_address: Token contract address (required if price_wei is set)
178
+ network: Network name (default: "sepolia")
179
+ description: Description of the service
180
+
181
+ Returns:
182
+ Decorated function
183
+ """
184
+ def decorator(func: Callable):
185
+ # Store payment metadata on function
186
+ if price_usd is not None:
187
+ # Convert USD to wei using default USDC
188
+ from ..common import process_price_to_atomic_amount
189
+ from ..types import Money
190
+
191
+ price = Money(usd=str(price_usd))
192
+ wei_amount, asset_addr, eip712_domain = process_price_to_atomic_amount(price, network)
193
+
194
+ func._d402_payment_config = {
195
+ "price_wei": wei_amount,
196
+ "token_address": asset_addr,
197
+ "network": network,
198
+ "description": description or f"API call: {func.__name__}"
199
+ }
200
+ elif price_wei is not None and token_address is not None:
201
+ # Use custom token and price
202
+ func._d402_payment_config = {
203
+ "price_wei": price_wei,
204
+ "token_address": token_address,
205
+ "network": network,
206
+ "description": description or f"API call: {func.__name__}"
207
+ }
208
+ else:
209
+ raise ValueError("Must specify either price_usd or (price_wei + token_address)")
210
+
211
+ @wraps(func)
212
+ async def wrapper(*args, **kwargs):
213
+ # Payment is handled by middleware
214
+ return await func(*args, **kwargs)
215
+
216
+ return wrapper
217
+
218
+ return decorator
219
+
220
+
221
+ def get_api_key(request: Request) -> Optional[str]:
222
+ """
223
+ Get the active API key for the current request.
224
+
225
+ This returns the API key that was resolved by the payment middleware:
226
+ - Client's API key if they provided one (Mode 1: Free)
227
+ - Server's API key if client paid (Mode 2: Paid)
228
+
229
+ Usage:
230
+ @app.post("/api/analyze")
231
+ async def analyze(request: Request):
232
+ api_key = get_api_key(request)
233
+ headers = {"Authorization": f"Bearer {api_key}"}
234
+ response = requests.get("https://api.example.com", headers=headers)
235
+ return response.json()
236
+
237
+ Args:
238
+ request: FastAPI request object
239
+
240
+ Returns:
241
+ API key string if available, None otherwise
242
+ """
243
+ if hasattr(request.state, 'api_key_to_use'):
244
+ return request.state.api_key_to_use
245
+ return None
246
+
247
+
248
+ __all__ = [
249
+ "D402FastAPIMiddleware",
250
+ "require_payment",
251
+ "get_api_key",
252
+ ]
253
+
@@ -0,0 +1,304 @@
1
+ """D402 payment helpers for MCP servers with official SDK.
2
+
3
+ This module provides decorators and helper functions for d402 payment protocol
4
+ specifically for Model Context Protocol (MCP) servers using FastMCP.
5
+
6
+ Works with official MCP SDK (mcp.server.fastmcp).
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ from typing import Optional, Dict, Any, Callable
12
+ from functools import wraps
13
+
14
+ # Import Context from official SDK
15
+ from mcp.server.fastmcp import Context
16
+
17
+ from starlette.requests import Request
18
+ from starlette.responses import JSONResponse
19
+ from web3 import Web3
20
+
21
+ from ..types import PaymentPayload, Price, TokenAmount, TokenAsset, EIP712Domain, PaymentRequirements, d402PaymentRequiredResponse
22
+ from ..encoding import safe_base64_decode
23
+ from ..common import process_price_to_atomic_amount, d402_VERSION
24
+ from ..facilitator import IATPSettlementFacilitator
25
+
26
+
27
+ class D402PaymentRequiredException(Exception):
28
+ """Exception raised when payment is required (HTTP 402)."""
29
+ def __init__(self, payment_response: Dict[str, Any]):
30
+ self.payment_response = payment_response
31
+ super().__init__("Payment required")
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class EndpointPaymentInfo:
37
+ """Payment information for a specific endpoint."""
38
+ def __init__(
39
+ self,
40
+ settlement_token_address: str,
41
+ settlement_token_network: str,
42
+ payment_price_float: float,
43
+ payment_price_wei: str,
44
+ server_address: str
45
+ ):
46
+ self.settlement_token_address = settlement_token_address
47
+ self.settlement_token_network = settlement_token_network
48
+ self.payment_price_float = payment_price_float
49
+ self.payment_price_wei = payment_price_wei
50
+ self.server_address = server_address
51
+
52
+
53
+ def get_active_api_key(context: Any) -> Optional[str]:
54
+ """
55
+ Get the API key to use for calling external APIs.
56
+
57
+ Returns api_key_to_use which was set by:
58
+ 1. D402PaymentMiddleware (in request.state)
59
+ 2. @require_payment_for_tool decorator (copied to context.state)
60
+
61
+ Priority:
62
+ 1. context.state.api_key_to_use (set by decorator)
63
+ 2. request.state.api_key_to_use (set by middleware)
64
+
65
+ Args:
66
+ context: MCP context object
67
+
68
+ Returns:
69
+ API key string (client's OR server's) if authorized, None otherwise
70
+
71
+ Usage in tools:
72
+ api_key = get_active_api_key(context)
73
+ if api_key:
74
+ headers = {"Authorization": f"Bearer {api_key}"}
75
+ """
76
+ try:
77
+ # Check request.state (set by middleware)
78
+ # Context is a Pydantic model - we can't set arbitrary fields on it
79
+ # So we read directly from request.state where middleware stored it
80
+ logger.debug(f"get_active_api_key: Checking context type={type(context).__name__}")
81
+ logger.debug(f" has request_context: {hasattr(context, 'request_context')}")
82
+
83
+ if hasattr(context, 'request_context') and context.request_context:
84
+ logger.debug(f" request_context exists")
85
+ if hasattr(context.request_context, 'request') and context.request_context.request:
86
+ request = context.request_context.request
87
+ logger.debug(f" request exists, has state: {hasattr(request, 'state')}")
88
+ if hasattr(request, 'state'):
89
+ api_key = getattr(request.state, 'api_key_to_use', None)
90
+ logger.debug(f" api_key_to_use: {api_key[:10] if api_key else None}")
91
+ if api_key:
92
+ return api_key
93
+
94
+ logger.warning(f"get_active_api_key: Could not find api_key_to_use in request.state")
95
+
96
+ except Exception as e:
97
+ logger.error(f"get_active_api_key error: {e}")
98
+ import traceback
99
+ logger.error(traceback.format_exc())
100
+
101
+ return None
102
+
103
+
104
+ async def settle_payment(
105
+ context: Any,
106
+ endpoint_info: EndpointPaymentInfo,
107
+ output_data: Any,
108
+ middleware: Optional[Any] = None # Middleware instance (Starlette or FastMCP)
109
+ ) -> bool:
110
+ """
111
+ Settle a payment after successful API call with output hash attestation.
112
+
113
+ Complete 402 settlement flow:
114
+ 1. Hash the output data (result returned to client)
115
+ 2. Provider signs over output_hash + consumer_request
116
+ 3. Submit to facilitator with proof of service completion
117
+ 4. Facilitator submits to IATP Settlement Layer on-chain
118
+
119
+ This should be called AFTER the tool successfully processes the request
120
+ to submit the payment settlement to the facilitator/blockchain.
121
+
122
+ Args:
123
+ context: MCP context (contains payment_payload)
124
+ endpoint_info: Endpoint payment requirements
125
+ output_data: The actual output/result being returned to client (will be hashed)
126
+ middleware: Optional D402MCPMiddleware instance (for facilitator access)
127
+
128
+ Returns:
129
+ bool: True if settlement submitted successfully
130
+
131
+ Usage in tools (for production settlement):
132
+ # Execute API call
133
+ response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
134
+ result = response.json()
135
+
136
+ # Settle payment with output hash
137
+ if context.state.payment_payload:
138
+ await settle_payment(context, endpoint_payment, output_data=result, middleware=...)
139
+
140
+ return result
141
+ """
142
+ try:
143
+ payment_payload = getattr(context.state, 'payment_payload', None) if hasattr(context, 'state') else None
144
+ if not payment_payload:
145
+ logger.debug("No payment to settle (authenticated mode)")
146
+ return True # Not an error - client used their own API key
147
+
148
+ # Skip settlement in testing mode
149
+ if middleware and middleware.testing_mode:
150
+ logger.info("⚠️ Testing mode: Skipping payment settlement")
151
+ return True
152
+
153
+ # Step 1: Hash the output data (proof of service completion)
154
+ import json
155
+ from web3 import Web3
156
+
157
+ logger.info("🔐 Starting payment settlement process...")
158
+
159
+ # Serialize output to JSON and hash it
160
+ output_json = json.dumps(output_data, sort_keys=True, separators=(',', ':'))
161
+ output_hash = Web3.keccak(text=output_json).hex()
162
+ logger.info(f"📊 Output data serialized: {len(output_json)} bytes")
163
+ logger.info(f"🔑 Output hash calculated: {output_hash}")
164
+ logger.info(f" First 1000 chars of output: {output_json[:1000]}")
165
+
166
+ # Step 2: Get payment_uuid from context (from facilitator verify response)
167
+ # The payment_uuid is the primary payment identifier from the facilitator
168
+ # It was set in verify_endpoint_payment() after facilitator.verify() returned it
169
+ payment_uuid = None
170
+ if hasattr(context, 'state') and hasattr(context.state, 'payment_uuid'):
171
+ payment_uuid = context.state.payment_uuid
172
+
173
+ if not payment_uuid:
174
+ logger.warning("No payment_uuid found in context - payment may not have been verified via facilitator")
175
+
176
+ # Step 3: Get facilitator fee from context (set by verify response)
177
+ facilitator_fee_percent = 250 # Default
178
+ if hasattr(context, 'state') and hasattr(context.state, 'facilitator_fee_percent'):
179
+ facilitator_fee_percent = context.state.facilitator_fee_percent
180
+
181
+ # Step 4: Create PaymentRequirements for this endpoint
182
+ # Include output_hash, payment_uuid, and facilitatorFeePercent in extra data
183
+ extra_data = {
184
+ "output_hash": output_hash,
185
+ "facilitator_fee_percent": facilitator_fee_percent
186
+ }
187
+ if payment_uuid:
188
+ extra_data["payment_uuid"] = payment_uuid
189
+
190
+ payment_requirements = PaymentRequirements(
191
+ scheme="exact",
192
+ network=endpoint_info.settlement_token_network,
193
+ pay_to=endpoint_info.server_address,
194
+ max_amount_required=endpoint_info.payment_price_wei,
195
+ max_timeout_seconds=300,
196
+ description=f"Service completed - output_hash: {output_hash}",
197
+ resource="",
198
+ mime_type="application/json",
199
+ asset=endpoint_info.settlement_token_address,
200
+ extra=extra_data # Include output hash, payment_uuid, and facilitatorFeePercent
201
+ )
202
+
203
+ # Step 4: Settle via facilitator
204
+ # The facilitator will:
205
+ # - Create provider attestation signing over the consumer's request + output_hash
206
+ # - Submit to relayer with proof of service completion
207
+ # - Relayer submits to IATPSettlementLayer on-chain
208
+ if middleware and middleware.facilitator:
209
+ try:
210
+ logger.info(f"📤 Submitting settlement to facilitator...")
211
+ logger.info(f" Payment UUID: {payment_uuid if payment_uuid else 'N/A'}")
212
+ logger.info(f" Output hash: {output_hash}")
213
+ logger.info(f" Amount: {endpoint_info.payment_price_wei} wei")
214
+
215
+ settle_result = await middleware.facilitator.settle(payment_payload, payment_requirements)
216
+ if settle_result.success:
217
+ logger.info(f"✅ Payment settlement request accepted by facilitator:")
218
+ logger.info(f" Status: PENDING_SETTLEMENT (queued for on-chain settlement)")
219
+ logger.info(f" Network: {settle_result.network}")
220
+ logger.info(f" Payer: {settle_result.payer}")
221
+ logger.info(f" Output Hash: {output_hash}")
222
+ logger.info(f" Note: Facilitator cron will batch-settle on-chain")
223
+ return True
224
+ else:
225
+ logger.error(f"❌ Payment settlement FAILED: {settle_result.error_reason}")
226
+ # TODO: Queue for retry
227
+ return False
228
+ except Exception as e:
229
+ logger.error(f"Error settling payment via facilitator: {e}")
230
+ # Don't fail the request if settlement fails
231
+ # Settlement can be retried later
232
+ logger.warning("Settlement failed but request completed - will retry later")
233
+ # TODO: Queue settlement for retry
234
+ return False
235
+ else:
236
+ logger.warning("No facilitator available for settlement")
237
+ # TODO: Queue settlement for later retry
238
+ return False
239
+
240
+ except Exception as e:
241
+ import traceback
242
+ logger.error(f"Error in settle_payment: {e}")
243
+ logger.error(f"Traceback:\n{traceback.format_exc()}")
244
+ return False
245
+
246
+
247
+ def require_payment_for_tool(
248
+ price: Price,
249
+ description: str = ""
250
+ ):
251
+ """
252
+ Decorator for MCP tools that require payment - METADATA ONLY.
253
+
254
+ This decorator ONLY stores payment configuration metadata on the function.
255
+ All payment processing (verify, settle) is handled by D402PaymentMiddleware.
256
+
257
+ Usage:
258
+ @mcp.tool()
259
+ @require_payment_for_tool(
260
+ price=TokenAmount(
261
+ amount="1000",
262
+ asset=TokenAsset(
263
+ address="0xUSDC...",
264
+ decimals=6,
265
+ network="base-sepolia",
266
+ eip712=EIP712Domain(name="IATPWallet", version="1")
267
+ )
268
+ ),
269
+ description="Get cryptocurrency price data"
270
+ )
271
+ async def get_price(context: Context, coin_id: str) -> Dict[str, Any]:
272
+ # Payment already verified by middleware
273
+ api_key = get_active_api_key(context)
274
+ response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
275
+ return response.json()
276
+
277
+ Args:
278
+ price: Payment configuration (TokenAmount with network, token, etc)
279
+ description: Service description for settlement
280
+
281
+ Returns:
282
+ Decorator that attaches metadata to the function
283
+ """
284
+ def decorator(func: Callable):
285
+ # Store payment metadata on function for middleware extraction
286
+ func._d402_payment_config = {
287
+ "price": price,
288
+ "description": description
289
+ }
290
+
291
+ # Return function unchanged - middleware handles all payment logic
292
+ return func
293
+
294
+ return decorator
295
+
296
+
297
+ __all__ = [
298
+ "D402PaymentRequiredException",
299
+ "EndpointPaymentInfo",
300
+ "get_active_api_key",
301
+ "settle_payment",
302
+ "require_payment_for_tool",
303
+ ]
304
+