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,481 @@
1
+ """Custom d402 facilitator that interfaces with IATP Settlement Layer."""
2
+
3
+ import logging
4
+ from typing import Optional, Dict, Any
5
+ from datetime import datetime, timezone
6
+ import httpx
7
+ from eth_account import Account
8
+ from web3 import Web3
9
+ from eth_account.messages import encode_defunct
10
+
11
+ from .types import (
12
+ PaymentPayload,
13
+ PaymentRequirements,
14
+ VerifyResponse,
15
+ SettleResponse
16
+ )
17
+ from .models import IATPSettlementRequest
18
+
19
+
20
+ def get_now_in_utc():
21
+ """Get current time in UTC."""
22
+ return datetime.now(timezone.utc)
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class IATPSettlementFacilitator:
28
+ """Custom d402 facilitator that settles payments through IATP Settlement Layer.
29
+
30
+ This facilitator verifies d402 payment headers and then submits settlement
31
+ requests to the facilitator service for batch on-chain settlement.
32
+
33
+ Flow:
34
+ 1. MCP Server receives request with X-PAYMENT header
35
+ 2. Facilitator /verify verifies the payment signature and authorization
36
+ 3. MCP Server processes the request
37
+ 4. Facilitator /settle accepts provider attestation and queues for settlement
38
+ 5. Facilitator cron batches and submits to IATPSettlementLayer.sol on-chain
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ facilitator_url: str,
44
+ facilitator_api_key: Optional[str] = None,
45
+ provider_operator_key: Optional[str] = None,
46
+ web3_provider: Optional[str] = None,
47
+ server_name: Optional[str] = None,
48
+ server_url: Optional[str] = None
49
+ ):
50
+ """Initialize the IATP Settlement Facilitator.
51
+
52
+ Args:
53
+ facilitator_url: URL of the facilitator service (handles both /verify and /settle)
54
+ facilitator_api_key: Optional API key for facilitator authentication
55
+ provider_operator_key: Operator private key for provider attestation signing
56
+ web3_provider: Optional Web3 provider URL for direct blockchain interaction
57
+ server_name: Optional MCP server name/ID (sent to facilitator for tracking)
58
+ server_url: Optional MCP server URL (sent to facilitator for tracking)
59
+ """
60
+ self.facilitator_url = facilitator_url.rstrip("/")
61
+ self.facilitator_api_key = facilitator_api_key
62
+ self.provider_operator_key = provider_operator_key
63
+ self.server_name = server_name
64
+ self.server_url = server_url
65
+
66
+ # Initialize Web3 if provider is given
67
+ self.w3 = Web3(Web3.HTTPProvider(web3_provider)) if web3_provider else None
68
+
69
+ # Initialize operator account if key provided
70
+ self.operator_account = None
71
+ if provider_operator_key:
72
+ if provider_operator_key.startswith("0x"):
73
+ provider_operator_key = provider_operator_key[2:]
74
+ self.operator_account = Account.from_key(provider_operator_key)
75
+
76
+ async def verify(
77
+ self,
78
+ payment: PaymentPayload,
79
+ payment_requirements: PaymentRequirements
80
+ ) -> VerifyResponse:
81
+ """Verify a payment header is valid.
82
+
83
+ If facilitator_url is configured, calls external facilitator /verify endpoint
84
+ which returns a payment_uuid. Otherwise performs local verification.
85
+
86
+ This checks:
87
+ 1. Signature is valid
88
+ 2. Authorization is not expired
89
+ 3. Amount matches requirements
90
+ 4. From address has sufficient balance
91
+
92
+ Args:
93
+ payment: Payment payload from X-PAYMENT header
94
+ payment_requirements: Payment requirements from server
95
+
96
+ Returns:
97
+ VerifyResponse with validation result and payment_uuid (if from external facilitator)
98
+ """
99
+ # If external facilitator URL is configured, call it
100
+ if self.facilitator_url:
101
+ try:
102
+ headers = {"Content-Type": "application/json"}
103
+ if self.facilitator_api_key:
104
+ headers["X-API-Key"] = self.facilitator_api_key
105
+
106
+ verify_request = {
107
+ "paymentPayload": payment.model_dump(by_alias=True),
108
+ "paymentRequirements": payment_requirements.model_dump(by_alias=True)
109
+ }
110
+
111
+ # Include server name and URL if available
112
+ if self.server_name:
113
+ verify_request["serverName"] = self.server_name
114
+ if self.server_url:
115
+ verify_request["serverUrl"] = self.server_url
116
+
117
+ async with httpx.AsyncClient(timeout=30.0) as client:
118
+ response = await client.post(
119
+ f"{self.facilitator_url}/verify",
120
+ json=verify_request,
121
+ headers=headers
122
+ )
123
+
124
+ if response.status_code == 200:
125
+ result = response.json()
126
+ # Parse response and extract payment_uuid
127
+ verify_response = VerifyResponse(**result)
128
+ if verify_response.payment_uuid:
129
+ logger.info(f"Payment verified via facilitator with payment_uuid: {verify_response.payment_uuid[:20]}...")
130
+ return verify_response
131
+ else:
132
+ error_msg = f"Facilitator verify error: {response.status_code} - {response.text}"
133
+ logger.error(error_msg)
134
+ return VerifyResponse(
135
+ is_valid=False,
136
+ invalid_reason=error_msg,
137
+ payer=None,
138
+ payment_uuid=None
139
+ )
140
+ except Exception as e:
141
+ logger.error(f"Error calling external facilitator verify: {e}")
142
+ # Fall back to local verification
143
+ logger.warning("Falling back to local verification")
144
+
145
+ # Local verification (fallback or if no facilitator_url configured)
146
+ try:
147
+ # Extract payment details
148
+ if payment.scheme != "exact":
149
+ return VerifyResponse(
150
+ is_valid=False,
151
+ invalid_reason=f"Unsupported scheme: {payment.scheme}",
152
+ payer=None,
153
+ payment_uuid=None
154
+ )
155
+
156
+ payload = payment.payload
157
+ authorization = payload.authorization
158
+ signature = payload.signature
159
+
160
+ # Verify the signature matches the authorization
161
+ payer = authorization.from_
162
+
163
+ # Reconstruct the EIP-712 message and verify signature
164
+ # This would use the exact EIP-712 domain from payment_requirements.extra
165
+ eip712_domain = payment_requirements.extra or {}
166
+
167
+ # For now, perform basic validation
168
+ # In production, this should verify the EIP-3009 signature
169
+ if not payer or not signature:
170
+ return VerifyResponse(
171
+ is_valid=False,
172
+ invalid_reason="Missing payer or signature",
173
+ payer=None,
174
+ payment_uuid=None
175
+ )
176
+
177
+ # Verify amount matches requirements
178
+ if authorization.value != payment_requirements.max_amount_required:
179
+ return VerifyResponse(
180
+ is_valid=False,
181
+ invalid_reason=f"Amount mismatch: expected {payment_requirements.max_amount_required}, got {authorization.value}",
182
+ payer=payer,
183
+ payment_uuid=None
184
+ )
185
+
186
+ # Verify not expired
187
+ import time
188
+ current_time = int(time.time())
189
+ valid_after = int(authorization.valid_after)
190
+ valid_before = int(authorization.valid_before)
191
+
192
+ if current_time < valid_after or current_time > valid_before:
193
+ return VerifyResponse(
194
+ is_valid=False,
195
+ invalid_reason="Authorization expired or not yet valid",
196
+ payer=payer,
197
+ payment_uuid=None
198
+ )
199
+
200
+ # Verify to address matches pay_to
201
+ if authorization.to.lower() != payment_requirements.pay_to.lower():
202
+ return VerifyResponse(
203
+ is_valid=False,
204
+ invalid_reason=f"Pay-to address mismatch",
205
+ payer=payer,
206
+ payment_uuid=None
207
+ )
208
+
209
+ # All checks passed (local verification - no payment_uuid)
210
+ return VerifyResponse(
211
+ is_valid=True,
212
+ invalid_reason=None,
213
+ payer=payer,
214
+ payment_uuid=None # Local verification doesn't provide payment_uuid
215
+ )
216
+
217
+ except Exception as e:
218
+ logger.error(f"Error verifying payment: {e}")
219
+ return VerifyResponse(
220
+ is_valid=False,
221
+ invalid_reason=f"Verification error: {str(e)}",
222
+ payer=None,
223
+ payment_uuid=None
224
+ )
225
+
226
+ async def settle(
227
+ self,
228
+ payment: PaymentPayload,
229
+ payment_requirements: PaymentRequirements
230
+ ) -> SettleResponse:
231
+ """Settle a verified payment through the IATP Settlement Layer.
232
+
233
+ This submits the payment to the facilitator, which will:
234
+ 1. Verify both consumer and provider signatures
235
+ 2. Queue for batch settlement
236
+ 3. Batch submit to IATPSettlementLayer.settleRequests() on-chain
237
+ 4. Credit the provider's balance after confirmation
238
+
239
+ Args:
240
+ payment: Verified payment payload
241
+ payment_requirements: Payment requirements
242
+
243
+ Returns:
244
+ SettleResponse with settlement result
245
+ """
246
+ try:
247
+ payload = payment.payload
248
+ authorization = payload.authorization
249
+ consumer_signature = payload.signature
250
+
251
+ # Create the service request struct (matches Solidity ServiceRequest)
252
+ service_request = {
253
+ "consumer": authorization.from_,
254
+ "provider": payment_requirements.pay_to,
255
+ "amount": authorization.value,
256
+ "timestamp": int(authorization.valid_after),
257
+ "serviceDescription": Web3.keccak(
258
+ text=payment_requirements.description
259
+ ).hex()
260
+ }
261
+
262
+ # Create provider attestation if operator key is available
263
+ provider_signature = None
264
+ extra = payment_requirements.extra or {}
265
+ output_hash = extra.get("output_hash")
266
+ payment_uuid = extra.get("payment_uuid") # Primary payment identifier from facilitator verify
267
+ facilitator_fee_percent = extra.get("facilitator_fee_percent", 250) # Get fee from facilitator verify response
268
+
269
+ if not payment_uuid:
270
+ logger.warning("No payment_uuid in payment_requirements.extra - attestation may not be linkable")
271
+
272
+ if self.operator_account:
273
+ # Create EIP-712 ProviderAttestation signature matching IATPWallet.sol
274
+ # ProviderAttestation(bytes32 consumerSignature, bytes32 outputHash, uint256 timestamp, bytes32 serviceDescription, uint256 facilitatorFeePercent)
275
+
276
+ # Hash the consumer signature bytes
277
+ consumer_signature_hash = Web3.keccak(hexstr=consumer_signature)
278
+
279
+ # Prepare output hash for contract verification
280
+ # Python: output_hash = keccak256(output_json) ← First hash (line 161 in mcp_middleware.py)
281
+ # Contract: outputHashHash = keccak256(outputHash bytes) ← Second hash (line 245 in IATPWallet.sol)
282
+ # Provider signs over outputHashHash (the double-hashed value)
283
+ if output_hash:
284
+ # Output hash is hex string like "0xabcd..." (already hashed once)
285
+ output_hash_bytes = bytes.fromhex(output_hash[2:] if output_hash.startswith("0x") else output_hash)
286
+ # Hash it again to match what contract will compute: keccak256(outputHash)
287
+ output_hash_hash = Web3.keccak(output_hash_bytes)
288
+ logger.debug(f"Output hash (1st): {output_hash[:20]}...")
289
+ logger.debug(f"Output hash (2nd): {output_hash_hash.hex()[:20]}...")
290
+ else:
291
+ # Use zero hash if no output provided
292
+ output_hash_hash = Web3.keccak(b"")
293
+
294
+ # Get service description hash
295
+ service_description_hash = Web3.keccak(text=payment_requirements.description)
296
+
297
+ # Attestation timestamp = current time when service is executed
298
+ # This is when provider actually rendered the service
299
+ import time as time_module
300
+ attestation_timestamp = int(time_module.time())
301
+
302
+ # Build EIP-712 typed data for ProviderAttestation
303
+ # Domain should be the Provider's IATPWallet domain
304
+ from .chains import get_chain_id
305
+
306
+ typed_data = {
307
+ "types": {
308
+ "ProviderAttestation": [
309
+ {"name": "consumerSignature", "type": "bytes32"},
310
+ {"name": "outputHash", "type": "bytes32"},
311
+ {"name": "timestamp", "type": "uint256"},
312
+ {"name": "serviceDescription", "type": "bytes32"},
313
+ {"name": "facilitatorFeePercent", "type": "uint256"},
314
+ ]
315
+ },
316
+ "primaryType": "ProviderAttestation",
317
+ "domain": {
318
+ "name": "IATPWallet",
319
+ "version": "1",
320
+ "chainId": int(get_chain_id(payment.network)),
321
+ "verifyingContract": payment_requirements.pay_to, # Provider's IATPWallet address
322
+ },
323
+ "message": {
324
+ "consumerSignature": consumer_signature_hash,
325
+ "outputHash": output_hash_hash,
326
+ "timestamp": attestation_timestamp,
327
+ "serviceDescription": service_description_hash,
328
+ "facilitatorFeePercent": facilitator_fee_percent,
329
+ },
330
+ }
331
+
332
+ # Sign with provider's operator key
333
+ logger.info(f"🔐 Signing provider attestation with EIP-712...")
334
+ logger.info(f" Domain: {typed_data['domain']}")
335
+ logger.info(f" Message fields:")
336
+ logger.info(f" consumerSignature (hash): {consumer_signature_hash.hex()[:40]}...")
337
+ logger.info(f" outputHash (double-hash): {output_hash_hash.hex()[:40]}...")
338
+ logger.info(f" timestamp: {attestation_timestamp}")
339
+ logger.info(f" serviceDescription (hash): {service_description_hash.hex()[:40]}...")
340
+ logger.info(f" facilitatorFeePercent: {facilitator_fee_percent}")
341
+
342
+ signed = self.operator_account.sign_typed_data(
343
+ domain_data=typed_data["domain"],
344
+ message_types=typed_data["types"],
345
+ message_data=typed_data["message"],
346
+ )
347
+ provider_signature = signed.signature.hex()
348
+ if not provider_signature.startswith("0x"):
349
+ provider_signature = f"0x{provider_signature}"
350
+
351
+ logger.info(f"✅ Provider attestation (EIP-712) created:")
352
+ logger.info(f" Signature: {provider_signature[:40]}...")
353
+ logger.info(f" Signer (operator): {self.operator_account.address}")
354
+ if output_hash:
355
+ logger.info(f" Output hash: {output_hash[:20]}...")
356
+ if payment_uuid:
357
+ logger.info(f" Payment UUID: {payment_uuid[:20]}...")
358
+ logger.info(f" Facilitator fee: {facilitator_fee_percent} basis points ({facilitator_fee_percent/100}%)")
359
+
360
+ # Prepare settlement request for facilitator
361
+ # Use camelCase field names to match facilitator's SettleRequest model
362
+ settlement_request = {
363
+ "paymentUuid": payment_uuid, # Required
364
+ "providerSignature": provider_signature or "0x", # Required
365
+ "outputHash": output_hash or "0x", # Optional
366
+ "serviceDescription": payment_requirements.description, # Required
367
+ "facilitatorFeePercent": facilitator_fee_percent, # Default: 250
368
+ "attestationTimestamp": attestation_timestamp # When provider executed service
369
+ }
370
+
371
+ # Submit to facilitator
372
+ headers = {"Content-Type": "application/json"}
373
+ if self.facilitator_api_key:
374
+ headers["X-API-Key"] = self.facilitator_api_key
375
+
376
+ logger.info(f"📤 Sending settle request to facilitator:")
377
+ logger.info(f" Payment UUID: {payment_uuid}")
378
+ logger.info(f" Output hash: {output_hash[:20] if output_hash else 'N/A'}...")
379
+
380
+ async with httpx.AsyncClient(timeout=30.0) as client:
381
+ response = await client.post(
382
+ f"{self.facilitator_url}/settle",
383
+ json=settlement_request,
384
+ headers=headers
385
+ )
386
+
387
+ if response.status_code == 200:
388
+ result = response.json()
389
+ return SettleResponse(
390
+ success=True,
391
+ error_reason=None,
392
+ transaction=result.get("transactionHash"),
393
+ network=payment.network,
394
+ payer=authorization.from_
395
+ )
396
+ else:
397
+ error_msg = f"Facilitator settle error: {response.status_code} - {response.text}"
398
+ logger.error(error_msg)
399
+ return SettleResponse(
400
+ success=False,
401
+ error_reason=error_msg,
402
+ transaction=None,
403
+ network=payment.network,
404
+ payer=authorization.from_
405
+ )
406
+
407
+ except Exception as e:
408
+ logger.error(f"Error settling payment: {e}")
409
+ return SettleResponse(
410
+ success=False,
411
+ error_reason=f"Settlement error: {str(e)}",
412
+ transaction=None,
413
+ network=payment.network,
414
+ payer=None
415
+ )
416
+
417
+ async def list(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
418
+ """List discoverable IATP services that accept d402 payments.
419
+
420
+ This queries the Traia registry for utility agents with d402 enabled.
421
+
422
+ Args:
423
+ request: Optional filters for discovery
424
+
425
+ Returns:
426
+ List of discoverable services
427
+ """
428
+ # This would query the MongoDB registry
429
+ # For now, return empty list
430
+ return {
431
+ "d402Version": 1,
432
+ "items": [],
433
+ "pagination": {
434
+ "limit": 100,
435
+ "offset": 0,
436
+ "total": 0
437
+ }
438
+ }
439
+
440
+
441
+ def create_iatp_facilitator(
442
+ facilitator_url: str = "https://facilitator.d402.net",
443
+ facilitator_api_key: Optional[str] = None,
444
+ provider_operator_key: Optional[str] = None,
445
+ web3_provider: Optional[str] = None
446
+ ) -> IATPSettlementFacilitator:
447
+ """Convenience function to create an IATP Settlement Facilitator.
448
+
449
+ Args:
450
+ facilitator_url: URL of the facilitator service (handles /verify and /settle)
451
+ facilitator_api_key: Optional API key for facilitator
452
+ provider_operator_key: Provider's operator private key for attestation signing
453
+ web3_provider: Optional Web3 provider URL
454
+
455
+ Returns:
456
+ Configured IATPSettlementFacilitator
457
+
458
+ Example:
459
+ facilitator = create_iatp_facilitator(
460
+ facilitator_url="http://localhost:8080",
461
+ facilitator_api_key=os.getenv("FACILITATOR_API_KEY"),
462
+ provider_operator_key=os.getenv("OPERATOR_PRIVATE_KEY")
463
+ )
464
+
465
+ # Use in d402 middleware
466
+ from traia_iatp.d402 import D402Config, require_iatp_payment
467
+
468
+ config = D402Config(
469
+ enabled=True,
470
+ pay_to_address="0x...", # Utility agent contract address
471
+ default_price=D402ServicePrice(...),
472
+ facilitator_url="http://localhost:8080"
473
+ )
474
+ """
475
+ return IATPSettlementFacilitator(
476
+ facilitator_url=facilitator_url,
477
+ facilitator_api_key=facilitator_api_key,
478
+ provider_operator_key=provider_operator_key,
479
+ web3_provider=web3_provider
480
+ )
481
+