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,183 @@
1
+ import time
2
+ import secrets
3
+ import logging
4
+ from typing import Dict, Any
5
+ from typing_extensions import (
6
+ TypedDict,
7
+ ) # use `typing_extensions.TypedDict` instead of `typing.TypedDict` on Python < 3.12
8
+ from eth_account import Account
9
+ from .encoding import safe_base64_encode, safe_base64_decode
10
+ from .types import (
11
+ PaymentRequirements,
12
+ )
13
+ from .chains import get_chain_id
14
+ import json
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def create_nonce() -> bytes:
20
+ """Create a random 32-byte nonce for authorization signatures."""
21
+ return secrets.token_bytes(32)
22
+
23
+
24
+ def prepare_payment_header(
25
+ sender_address: str, d402_version: int, payment_requirements: PaymentRequirements
26
+ ) -> Dict[str, Any]:
27
+ """Prepare an unsigned payment header with sender address, d402 version, and payment requirements."""
28
+ nonce = create_nonce()
29
+ valid_after = str(int(time.time()) - 60) # 60 seconds before
30
+ # Ensure at least 24 hours for settlement (facilitator batches payments)
31
+ min_deadline_seconds = max(payment_requirements.max_timeout_seconds, 86400) # 24 hours
32
+ valid_before = str(int(time.time()) + min_deadline_seconds)
33
+
34
+ return {
35
+ "d402Version": d402_version,
36
+ "scheme": payment_requirements.scheme,
37
+ "network": payment_requirements.network,
38
+ "payload": {
39
+ "signature": None,
40
+ "authorization": {
41
+ "from": sender_address,
42
+ "to": payment_requirements.pay_to,
43
+ "value": payment_requirements.max_amount_required,
44
+ "validAfter": valid_after,
45
+ "validBefore": valid_before,
46
+ "nonce": nonce,
47
+ },
48
+ },
49
+ }
50
+
51
+
52
+ class PaymentHeader(TypedDict):
53
+ d402Version: int
54
+ scheme: str
55
+ network: str
56
+ payload: dict[str, Any]
57
+
58
+
59
+ def sign_payment_header(
60
+ operator_account: Account,
61
+ payment_requirements: PaymentRequirements,
62
+ header: PaymentHeader,
63
+ wallet_address: str = None,
64
+ request_path: str = None
65
+ ) -> str:
66
+ """
67
+ Sign a payment header using EIP-712 PullFundsForSettlement signature.
68
+
69
+ This signature format matches IATPWallet.sol validateConsumerSignature.
70
+
71
+ Contract Type Hash (IATPWallet.sol line 34-36):
72
+ PullFundsForSettlement(
73
+ address wallet, // Consumer's IATPWallet contract address
74
+ address provider, // Provider's IATPWallet contract address
75
+ address token, // Token address (USDC, etc.)
76
+ uint256 amount, // Payment amount
77
+ uint256 deadline, // Signature expiration
78
+ string requestPath // API path (e.g., "/mcp/tools/call")
79
+ )
80
+
81
+ Note: chainId is in the EIP-712 domain, NOT in the message (per EIP-712 standard)
82
+
83
+ Args:
84
+ operator_account: Operator account with private key for signing (EOA)
85
+ payment_requirements: Payment requirements from server
86
+ header: Payment header structure
87
+ wallet_address: Consumer's IATPWallet contract address (if None, uses operator_account.address)
88
+ request_path: API request path (if None, uses payment_requirements.resource)
89
+ """
90
+ try:
91
+ auth = header["payload"]["authorization"]
92
+
93
+ # Get wallet address (IATPWallet contract, not EOA)
94
+ consumer_wallet = wallet_address or auth["from"]
95
+
96
+ # Get request path from payment_requirements if not provided
97
+ if request_path is None:
98
+ request_path = payment_requirements.resource or "/mcp"
99
+ logger.info(f"🔍 payment_requirements.resource: {payment_requirements.resource}")
100
+ logger.info(f"🔍 Using request_path: {request_path}")
101
+
102
+ # Ensure we have a valid request path (contract requires non-empty string)
103
+ if not request_path or request_path.strip() == "":
104
+ logger.warning(f"⚠️ request_path was empty, defaulting to /mcp")
105
+ request_path = "/mcp"
106
+
107
+ # Get domain info from payment_requirements.extra (IATPWallet domain)
108
+ extra = payment_requirements.extra or {}
109
+ wallet_name = extra.get("name", "IATPWallet")
110
+ wallet_version = extra.get("version", "1")
111
+
112
+ # Build EIP-712 typed data for PullFundsForSettlement
113
+ # Note: chainId is in the domain, not the message (EIP-712 standard)
114
+ typed_data = {
115
+ "types": {
116
+ "PullFundsForSettlement": [
117
+ {"name": "wallet", "type": "address"},
118
+ {"name": "provider", "type": "address"},
119
+ {"name": "token", "type": "address"},
120
+ {"name": "amount", "type": "uint256"},
121
+ {"name": "deadline", "type": "uint256"},
122
+ {"name": "requestPath", "type": "string"},
123
+ ]
124
+ },
125
+ "primaryType": "PullFundsForSettlement",
126
+ "domain": {
127
+ "name": wallet_name,
128
+ "version": wallet_version,
129
+ "chainId": int(get_chain_id(payment_requirements.network)), # chainId in domain only
130
+ "verifyingContract": consumer_wallet, # Consumer's IATPWallet contract
131
+ },
132
+ "message": {
133
+ "wallet": consumer_wallet, # Consumer's IATPWallet contract address
134
+ "provider": auth["to"], # Provider's IATPWallet contract address
135
+ "token": payment_requirements.asset, # Token address (e.g., USDC)
136
+ "amount": int(auth["value"]),
137
+ "deadline": int(auth["validBefore"]),
138
+ "requestPath": request_path, # Actual API path, not nonce
139
+ },
140
+ }
141
+
142
+ signed_message = operator_account.sign_typed_data(
143
+ domain_data=typed_data["domain"],
144
+ message_types=typed_data["types"],
145
+ message_data=typed_data["message"],
146
+ )
147
+ signature = signed_message.signature.hex()
148
+ if not signature.startswith("0x"):
149
+ signature = f"0x{signature}"
150
+
151
+ header["payload"]["signature"] = signature
152
+
153
+ # Store wallet address and request path in header for verification
154
+ header["payload"]["authorization"]["from"] = consumer_wallet
155
+ header["payload"]["authorization"]["requestPath"] = request_path
156
+
157
+ encoded = encode_payment(header)
158
+ return encoded
159
+ except Exception:
160
+ raise
161
+
162
+
163
+ def encode_payment(payment_payload: Dict[str, Any]) -> str:
164
+ """Encode a payment payload into a base64 string, handling HexBytes and other non-serializable types."""
165
+ from hexbytes import HexBytes
166
+
167
+ def default(obj):
168
+ if isinstance(obj, HexBytes):
169
+ return obj.hex()
170
+ if hasattr(obj, "to_dict"):
171
+ return obj.to_dict()
172
+ if hasattr(obj, "hex"):
173
+ return obj.hex()
174
+ raise TypeError(
175
+ f"Object of type {obj.__class__.__name__} is not JSON serializable"
176
+ )
177
+
178
+ return safe_base64_encode(json.dumps(payment_payload, default=default))
179
+
180
+
181
+ def decode_payment(encoded_payment: str) -> Dict[str, Any]:
182
+ """Decode a base64 encoded payment string back into a PaymentPayload object."""
183
+ return json.loads(safe_base64_decode(encoded_payment))
@@ -0,0 +1,164 @@
1
+ """
2
+ D402 Price Builder - Helper for creating payment amounts.
3
+
4
+ Simplifies creating TokenAmount objects for payment configuration with any token.
5
+ """
6
+
7
+ from .types import TokenAmount, TokenAsset, EIP712Domain
8
+
9
+
10
+ class D402PriceBuilder:
11
+ """
12
+ Helper class for building D402 payment amounts with any token.
13
+
14
+ Initialize once with your token configuration, then create prices easily.
15
+
16
+ Usage:
17
+ # Initialize with ANY token configuration
18
+ builder = D402PriceBuilder(
19
+ token_address="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", # Your token
20
+ token_decimals=6, # Your decimals
21
+ network="sepolia", # Your network
22
+ token_symbol="USDC" # Your symbol
23
+ )
24
+
25
+ # Create prices easily
26
+ price_cheap = builder.create_price(0.001) # $0.001
27
+ price_standard = builder.create_price(0.01) # $0.01
28
+ price_premium = builder.create_price(0.05) # $0.05
29
+
30
+ # Use in decorators
31
+ @require_payment(price=price_premium, description="Premium API call")
32
+ async def premium_endpoint():
33
+ pass
34
+
35
+ Examples:
36
+ # USDC on Sepolia (6 decimals)
37
+ usdc_builder = D402PriceBuilder(
38
+ token_address="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
39
+ token_decimals=6,
40
+ network="sepolia",
41
+ token_symbol="USDC"
42
+ )
43
+
44
+ # TRAIA token (18 decimals)
45
+ traia_builder = D402PriceBuilder(
46
+ token_address="0x...",
47
+ token_decimals=18,
48
+ network="base-mainnet",
49
+ token_symbol="TRAIA"
50
+ )
51
+
52
+ # Any custom token
53
+ custom_builder = D402PriceBuilder(
54
+ token_address="0xYourToken...",
55
+ token_decimals=8, # Your token's decimals
56
+ network="arbitrum-mainnet",
57
+ token_symbol="CUSTOM"
58
+ )
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ token_address: str,
64
+ token_decimals: int,
65
+ network: str,
66
+ token_symbol: str = "TOKEN",
67
+ eip712_name: str = "IATPWallet",
68
+ eip712_version: str = "1"
69
+ ):
70
+ """
71
+ Initialize the price builder with token configuration.
72
+
73
+ Args:
74
+ token_address: ERC20 token contract address
75
+ token_decimals: Token decimals (6 for USDC, 18 for most ERC20)
76
+ network: Blockchain network (sepolia, base-sepolia, base-mainnet, etc.)
77
+ token_symbol: Token symbol for display (USDC, TRAIA, DAI, etc.)
78
+ eip712_name: EIP-712 domain name (default: "IATPWallet")
79
+ eip712_version: EIP-712 domain version (default: "1")
80
+ """
81
+ self.token_address = token_address
82
+ self.token_decimals = token_decimals
83
+ self.network = network
84
+ self.token_symbol = token_symbol
85
+ self.eip712_domain = EIP712Domain(
86
+ name=eip712_name,
87
+ version=eip712_version
88
+ )
89
+
90
+ def create_price(self, amount_usd: float) -> TokenAmount:
91
+ """
92
+ Create a TokenAmount from USD amount.
93
+
94
+ Automatically converts USD to token's atomic units based on decimals.
95
+
96
+ Args:
97
+ amount_usd: Amount in USD (e.g., 0.01 for 1 cent, 0.05 for 5 cents)
98
+
99
+ Returns:
100
+ TokenAmount object ready to use in payment configs
101
+
102
+ Examples:
103
+ # USDC (6 decimals)
104
+ price = builder.create_price(0.01) # → "10000" wei (0.01 * 10^6)
105
+ price = builder.create_price(0.001) # → "1000" wei
106
+ price = builder.create_price(1.00) # → "1000000" wei
107
+
108
+ # TRAIA (18 decimals)
109
+ price = builder.create_price(0.01) # → "10000000000000000" wei (0.01 * 10^18)
110
+
111
+ # Use in decorator
112
+ @require_payment(price=price, description="API call")
113
+ async def my_endpoint():
114
+ pass
115
+ """
116
+ # Convert USD to wei/atomic units based on token decimals
117
+ # Formula: amount_wei = amount_usd * (10 ** decimals)
118
+ amount_wei = str(int(amount_usd * (10 ** self.token_decimals)))
119
+
120
+ return TokenAmount(
121
+ amount=amount_wei,
122
+ asset=TokenAsset(
123
+ address=self.token_address,
124
+ decimals=self.token_decimals,
125
+ network=self.network,
126
+ symbol=self.token_symbol,
127
+ eip712=self.eip712_domain
128
+ )
129
+ )
130
+
131
+ def create_price_wei(self, amount_wei: str) -> TokenAmount:
132
+ """
133
+ Create a TokenAmount from wei/atomic units directly.
134
+
135
+ Useful when you already know the exact atomic amount.
136
+
137
+ Args:
138
+ amount_wei: Amount in atomic units as string (e.g., "10000" for 0.01 USDC)
139
+
140
+ Returns:
141
+ TokenAmount object ready to use in payment configs
142
+
143
+ Example:
144
+ # Exact atomic amount (useful for non-USD pricing)
145
+ price = builder.create_price_wei("10000") # Exactly 10000 atomic units
146
+ """
147
+ return TokenAmount(
148
+ amount=amount_wei,
149
+ asset=TokenAsset(
150
+ address=self.token_address,
151
+ decimals=self.token_decimals,
152
+ network=self.network,
153
+ symbol=self.token_symbol,
154
+ eip712=self.eip712_domain
155
+ )
156
+ )
157
+
158
+ def __repr__(self) -> str:
159
+ return f"D402PriceBuilder(token={self.token_symbol}, network={self.network}, decimals={self.token_decimals})"
160
+
161
+
162
+ __all__ = ["D402PriceBuilder"]
163
+
164
+
@@ -0,0 +1,61 @@
1
+ """D402 payment middleware for various server frameworks.
2
+
3
+ This module provides framework-specific middleware implementations for the d402
4
+ payment protocol. Each framework has its own module with appropriate middleware.
5
+
6
+ Supported frameworks:
7
+ - Starlette: For Starlette-based applications (including FastMCP)
8
+ - FastAPI: For FastAPI applications
9
+ - MCP: For Model Context Protocol (MCP) servers with tool decorators
10
+
11
+ Usage examples:
12
+
13
+ 1. MCP Server (FastMCP):
14
+ from traia_iatp.d402.servers import D402PaymentMiddleware, require_payment_for_tool
15
+
16
+ app = mcp.streamable_http_app()
17
+ app.add_middleware(D402PaymentMiddleware, ...)
18
+
19
+ 2. FastAPI Server:
20
+ from traia_iatp.d402.servers.fastapi import D402FastAPIMiddleware, require_payment
21
+
22
+ app = FastAPI()
23
+ middleware = D402FastAPIMiddleware(...)
24
+ middleware.add_to_app(app)
25
+
26
+ 3. Starlette Server:
27
+ from traia_iatp.d402.servers import D402PaymentMiddleware
28
+
29
+ app = Starlette()
30
+ app.add_middleware(D402PaymentMiddleware, ...)
31
+ """
32
+
33
+ from .starlette import (
34
+ D402PaymentMiddleware,
35
+ require_payment,
36
+ extract_payment_configs,
37
+ build_payment_config,
38
+ )
39
+ from .mcp import (
40
+ EndpointPaymentInfo,
41
+ get_active_api_key,
42
+ require_payment_for_tool,
43
+ settle_payment,
44
+ )
45
+
46
+ __all__ = [
47
+ # Starlette middleware (works for any Starlette-based app)
48
+ "D402PaymentMiddleware",
49
+
50
+ # Generic decorators and extractors (for any server type)
51
+ "require_payment",
52
+ "extract_payment_configs",
53
+ "build_payment_config",
54
+
55
+ # MCP-specific helpers (wraps require_payment for MCP tools)
56
+ "EndpointPaymentInfo",
57
+ "get_active_api_key",
58
+ "require_payment_for_tool",
59
+ "settle_payment",
60
+ ]
61
+
@@ -0,0 +1,139 @@
1
+ """Base classes and utilities for d402 server middleware.
2
+
3
+ This module provides shared functionality used across different server framework
4
+ implementations.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, Any, Optional
9
+ from enum import Enum
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PaymentMode(Enum):
15
+ """Payment mode for a request."""
16
+ FREE_WITH_API_KEY = "free_with_api_key" # Client has valid API key
17
+ PAID_WITH_SERVER_KEY = "paid_with_server_key" # Client paid, server uses internal key
18
+ NO_PAYMENT = "no_payment" # No payment required
19
+
20
+
21
+ class BasePaymentConfig:
22
+ """Base configuration for payment-enabled servers.
23
+
24
+ This class provides common configuration used across all server frameworks.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ server_address: str,
30
+ requires_auth: bool = False,
31
+ internal_api_key: Optional[str] = None,
32
+ testing_mode: bool = False,
33
+ facilitator_url: Optional[str] = None,
34
+ facilitator_api_key: Optional[str] = None,
35
+ server_name: Optional[str] = None
36
+ ):
37
+ """Initialize payment configuration.
38
+
39
+ Args:
40
+ server_address: Address where payments should be sent
41
+ requires_auth: Whether server accepts API keys for free access
42
+ internal_api_key: Server's internal API key (used when client pays)
43
+ testing_mode: If True, skip facilitator verification
44
+ facilitator_url: URL of the payment facilitator
45
+ facilitator_api_key: API key for facilitator authentication
46
+ server_name: Name/ID of this server for tracking
47
+ """
48
+ self.server_address = server_address
49
+ self.requires_auth = requires_auth
50
+ self.internal_api_key = internal_api_key
51
+ self.testing_mode = testing_mode
52
+ self.facilitator_url = facilitator_url
53
+ self.facilitator_api_key = facilitator_api_key
54
+ self.server_name = server_name
55
+
56
+ self.validate()
57
+
58
+ def validate(self):
59
+ """Validate configuration."""
60
+ if not self.server_address:
61
+ raise ValueError("server_address is required")
62
+
63
+ if not self.testing_mode and not self.facilitator_url:
64
+ raise ValueError("facilitator_url required when testing_mode is False")
65
+
66
+
67
+ def extract_api_key_from_headers(headers: Dict[str, str]) -> Optional[str]:
68
+ """Extract API key from request headers.
69
+
70
+ Supports both Authorization: Bearer <token> and X-API-KEY: <token> formats.
71
+
72
+ Args:
73
+ headers: Request headers (dict or Headers object)
74
+
75
+ Returns:
76
+ API key string if found, None otherwise
77
+ """
78
+ # Authorization header
79
+ auth = headers.get("authorization") or headers.get("Authorization", "")
80
+ if auth.lower().startswith("bearer "):
81
+ return auth[7:].strip()
82
+
83
+ # X-API-KEY header
84
+ api_key = headers.get("x-api-key") or headers.get("X-API-KEY", "")
85
+ if api_key:
86
+ return api_key.strip()
87
+
88
+ return None
89
+
90
+
91
+ def log_payment_verification_start(tool_name: str, has_auth: bool, payment_header_present: bool):
92
+ """Log start of payment verification process.
93
+
94
+ Args:
95
+ tool_name: Name of the tool/endpoint being called
96
+ has_auth: Whether client has API key
97
+ payment_header_present: Whether payment header is present
98
+ """
99
+ if has_auth:
100
+ logger.info(f"✅ {tool_name}: Client authenticated with API key (Mode 1: Free)")
101
+ elif payment_header_present:
102
+ logger.info(f"💰 {tool_name}: Payment header RECEIVED - validating...")
103
+ else:
104
+ logger.info(f"💰 {tool_name}: Payment required (Mode 2) - HTTP 402")
105
+
106
+
107
+ def log_payment_validation_success(
108
+ tool_name: str,
109
+ payment_amount: int,
110
+ required_amount: int,
111
+ from_address: str,
112
+ to_address: str,
113
+ request_path: str
114
+ ):
115
+ """Log successful payment validation.
116
+
117
+ Args:
118
+ tool_name: Name of the tool/endpoint
119
+ payment_amount: Amount paid
120
+ required_amount: Amount required
121
+ from_address: Payer address
122
+ to_address: Payee address
123
+ request_path: Request path for signature binding
124
+ """
125
+ logger.info(f"✅ {tool_name}: Payment VERIFIED successfully (Mode 2: Paid)")
126
+ logger.info(f" Payment amount: {payment_amount} wei (required: {required_amount} wei)")
127
+ logger.info(f" From (wallet): {from_address}")
128
+ logger.info(f" To (provider): {to_address}")
129
+ logger.info(f" Request path: {request_path}")
130
+
131
+
132
+ __all__ = [
133
+ "PaymentMode",
134
+ "BasePaymentConfig",
135
+ "extract_api_key_from_headers",
136
+ "log_payment_verification_start",
137
+ "log_payment_validation_success",
138
+ ]
139
+
@@ -0,0 +1,140 @@
1
+ """
2
+ Example: General-purpose API server with D402 payment support.
3
+
4
+ This example shows how to use the @require_payment decorator for a
5
+ custom FastAPI server with multiple endpoints at different prices.
6
+
7
+ This is the same pattern as MCP servers, just generalized for any endpoint!
8
+ """
9
+
10
+ import os
11
+ from fastapi import FastAPI, Request
12
+ import uvicorn
13
+
14
+ from traia_iatp.d402.servers import (
15
+ D402PaymentMiddleware,
16
+ require_payment,
17
+ extract_payment_configs
18
+ )
19
+ from traia_iatp.d402 import D402PriceBuilder
20
+
21
+ # Initialize FastAPI app
22
+ app = FastAPI(title="Paid Analysis API")
23
+
24
+ # Get configuration from environment
25
+ SERVER_ADDRESS = os.getenv("SERVER_ADDRESS", "0x1234567890123456789012345678901234567890")
26
+ NETWORK = os.getenv("NETWORK", "sepolia")
27
+ TOKEN_ADDRESS = os.getenv("TOKEN_ADDRESS", "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238")
28
+ TOKEN_DECIMALS = int(os.getenv("TOKEN_DECIMALS", "6"))
29
+ TOKEN_SYMBOL = os.getenv("TOKEN_SYMBOL", "USDC")
30
+
31
+ # Create price builder - works with ANY token!
32
+ # Just pass your token configuration from environment
33
+ price_builder = D402PriceBuilder(
34
+ token_address=TOKEN_ADDRESS,
35
+ token_decimals=TOKEN_DECIMALS,
36
+ network=NETWORK,
37
+ token_symbol=TOKEN_SYMBOL
38
+ )
39
+
40
+ # ============================================================================
41
+ # API ENDPOINTS - Different prices for different operations
42
+ # ============================================================================
43
+
44
+ @app.post("/quick-check")
45
+ @require_payment(
46
+ price=price_builder.create_price(0.001), # $0.001 - uses builder!
47
+ endpoint_path="/quick-check",
48
+ description="Quick health check of data"
49
+ )
50
+ async def quick_check(request: Request, data: dict):
51
+ """Cheap, fast check - only $0.001"""
52
+ return {"status": "ok", "data_quality": "good"}
53
+
54
+
55
+ @app.post("/analyze")
56
+ @require_payment(
57
+ price=price_builder.create_price(0.01), # $0.01 - uses builder!
58
+ endpoint_path="/analyze",
59
+ description="Standard analysis"
60
+ )
61
+ async def analyze(request: Request, data: dict):
62
+ """Standard analysis - $0.01"""
63
+ return {"analysis": "detailed results", "confidence": 0.95}
64
+
65
+
66
+ @app.post("/deep-analysis")
67
+ @require_payment(
68
+ price=price_builder.create_price(0.05), # $0.05 - uses builder!
69
+ endpoint_path="/deep-analysis",
70
+ description="Comprehensive deep analysis with ML models"
71
+ )
72
+ async def deep_analysis(request: Request, data: dict):
73
+ """Expensive operation - $0.05"""
74
+ return {
75
+ "analysis": "very detailed results",
76
+ "confidence": 0.99,
77
+ "model": "advanced-ml-v2"
78
+ }
79
+
80
+
81
+ @app.get("/health")
82
+ async def health():
83
+ """Health check - FREE (no decorator, no payment required)"""
84
+ return {"status": "healthy"}
85
+
86
+
87
+ # ============================================================================
88
+ # D402 MIDDLEWARE SETUP
89
+ # ============================================================================
90
+
91
+ def create_app_with_middleware():
92
+ """Add D402 middleware to the app."""
93
+
94
+ # Extract payment configs from @require_payment decorators
95
+ # This is the SAME approach as MCP servers, just generalized!
96
+ payment_configs = extract_payment_configs(app, SERVER_ADDRESS)
97
+
98
+ print("=" * 80)
99
+ print("D402 Payment Configuration:")
100
+ print(f" Server Address: {SERVER_ADDRESS}")
101
+ print(f" Protected endpoints: {len(payment_configs)}")
102
+ for path, config in payment_configs.items():
103
+ price_usd = config['price_float']
104
+ print(f" {path}: ${price_usd} USD")
105
+ print("=" * 80)
106
+
107
+ # Add D402 middleware
108
+ facilitator_url = os.getenv("D402_FACILITATOR_URL", "http://localhost:7070")
109
+ testing_mode = os.getenv("D402_TESTING_MODE", "true").lower() == "true"
110
+
111
+ app.add_middleware(
112
+ D402PaymentMiddleware,
113
+ server_address=SERVER_ADDRESS,
114
+ tool_payment_configs=payment_configs, # Same interface as MCP!
115
+ requires_auth=False, # Payment only (no API keys)
116
+ testing_mode=testing_mode,
117
+ facilitator_url=facilitator_url,
118
+ server_name="example-analysis-api"
119
+ )
120
+
121
+ print("✅ D402 middleware added")
122
+ print(f" Testing mode: {testing_mode}")
123
+ print(f" Facilitator: {facilitator_url}")
124
+ print("=" * 80)
125
+
126
+ return app
127
+
128
+
129
+ if __name__ == "__main__":
130
+ # Create app with middleware
131
+ app_with_d402 = create_app_with_middleware()
132
+
133
+ # Run server
134
+ port = int(os.getenv("PORT", "8000"))
135
+ uvicorn.run(
136
+ app_with_d402,
137
+ host="0.0.0.0",
138
+ port=port
139
+ )
140
+