t402 1.6.1__tar.gz → 1.7.1__tar.gz

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 (94) hide show
  1. {t402-1.6.1 → t402-1.7.1}/PKG-INFO +1 -1
  2. {t402-1.6.1 → t402-1.7.1}/pyproject.toml +1 -1
  3. {t402-1.6.1 → t402-1.7.1}/src/t402/__init__.py +169 -2
  4. {t402-1.6.1 → t402-1.7.1}/src/t402/common.py +37 -8
  5. t402-1.7.1/src/t402/encoding.py +325 -0
  6. {t402-1.6.1 → t402-1.7.1}/src/t402/facilitator.py +1 -1
  7. t402-1.7.1/src/t402/fastapi/__init__.py +79 -0
  8. t402-1.7.1/src/t402/fastapi/dependencies.py +398 -0
  9. t402-1.7.1/src/t402/fastapi/middleware.py +754 -0
  10. t402-1.7.1/src/t402/schemes/__init__.py +125 -0
  11. t402-1.7.1/src/t402/schemes/evm/__init__.py +25 -0
  12. t402-1.7.1/src/t402/schemes/evm/exact/__init__.py +29 -0
  13. t402-1.7.1/src/t402/schemes/evm/exact/client.py +265 -0
  14. t402-1.7.1/src/t402/schemes/evm/exact/server.py +181 -0
  15. t402-1.7.1/src/t402/schemes/interfaces.py +401 -0
  16. t402-1.7.1/src/t402/schemes/registry.py +477 -0
  17. t402-1.7.1/src/t402/schemes/ton/__init__.py +22 -0
  18. t402-1.7.1/src/t402/schemes/ton/exact/__init__.py +27 -0
  19. t402-1.7.1/src/t402/schemes/ton/exact/client.py +343 -0
  20. t402-1.7.1/src/t402/schemes/ton/exact/server.py +201 -0
  21. t402-1.7.1/src/t402/schemes/tron/__init__.py +22 -0
  22. t402-1.7.1/src/t402/schemes/tron/exact/__init__.py +27 -0
  23. t402-1.7.1/src/t402/schemes/tron/exact/client.py +260 -0
  24. t402-1.7.1/src/t402/schemes/tron/exact/server.py +192 -0
  25. {t402-1.6.1 → t402-1.7.1}/src/t402/types.py +178 -8
  26. t402-1.7.1/tests/test_fastapi.py +430 -0
  27. {t402-1.6.1 → t402-1.7.1}/tests/test_paywall.py +1 -1
  28. t402-1.7.1/tests/test_schemes.py +1115 -0
  29. t402-1.7.1/tests/test_v2_types.py +419 -0
  30. {t402-1.6.1 → t402-1.7.1}/uv.lock +1 -1
  31. t402-1.6.1/src/t402/encoding.py +0 -28
  32. t402-1.6.1/src/t402/fastapi/middleware.py +0 -219
  33. t402-1.6.1/tests/flask_tests/__init__.py +0 -0
  34. {t402-1.6.1 → t402-1.7.1}/.gitignore +0 -0
  35. {t402-1.6.1 → t402-1.7.1}/.python-version +0 -0
  36. {t402-1.6.1 → t402-1.7.1}/README.md +0 -0
  37. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/__init__.py +0 -0
  38. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/client.py +0 -0
  39. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/constants.py +0 -0
  40. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/router.py +0 -0
  41. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/scan.py +0 -0
  42. {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/types.py +0 -0
  43. {t402-1.6.1 → t402-1.7.1}/src/t402/chains.py +0 -0
  44. {t402-1.6.1 → t402-1.7.1}/src/t402/cli.py +0 -0
  45. {t402-1.6.1 → t402-1.7.1}/src/t402/clients/__init__.py +0 -0
  46. {t402-1.6.1 → t402-1.7.1}/src/t402/clients/base.py +0 -0
  47. {t402-1.6.1 → t402-1.7.1}/src/t402/clients/httpx.py +0 -0
  48. {t402-1.6.1 → t402-1.7.1}/src/t402/clients/requests.py +0 -0
  49. {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/__init__.py +0 -0
  50. {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/accounts.py +0 -0
  51. {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/bundlers.py +0 -0
  52. {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/paymasters.py +0 -0
  53. {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/types.py +0 -0
  54. {t402-1.6.1 → t402-1.7.1}/src/t402/evm_paywall_template.py +0 -0
  55. {t402-1.6.1 → t402-1.7.1}/src/t402/exact.py +0 -0
  56. {t402-1.6.1/src/t402/fastapi → t402-1.7.1/src/t402/flask}/__init__.py +0 -0
  57. {t402-1.6.1 → t402-1.7.1}/src/t402/flask/middleware.py +0 -0
  58. {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/__init__.py +0 -0
  59. {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/constants.py +0 -0
  60. {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/server.py +0 -0
  61. {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/tools.py +0 -0
  62. {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/types.py +0 -0
  63. {t402-1.6.1 → t402-1.7.1}/src/t402/networks.py +0 -0
  64. {t402-1.6.1 → t402-1.7.1}/src/t402/path.py +0 -0
  65. {t402-1.6.1 → t402-1.7.1}/src/t402/paywall.py +0 -0
  66. {t402-1.6.1 → t402-1.7.1}/src/t402/py.typed +0 -0
  67. {t402-1.6.1 → t402-1.7.1}/src/t402/svm.py +0 -0
  68. {t402-1.6.1 → t402-1.7.1}/src/t402/svm_paywall_template.py +0 -0
  69. {t402-1.6.1 → t402-1.7.1}/src/t402/ton.py +0 -0
  70. {t402-1.6.1 → t402-1.7.1}/src/t402/ton_paywall_template.py +0 -0
  71. {t402-1.6.1 → t402-1.7.1}/src/t402/tron.py +0 -0
  72. {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/__init__.py +0 -0
  73. {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/chains.py +0 -0
  74. {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/errors.py +0 -0
  75. {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/signer.py +0 -0
  76. {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/types.py +0 -0
  77. {t402-1.6.1/src/t402/flask → t402-1.7.1/tests/clients}/__init__.py +0 -0
  78. {t402-1.6.1 → t402-1.7.1}/tests/clients/test_base.py +0 -0
  79. {t402-1.6.1 → t402-1.7.1}/tests/clients/test_httpx.py +0 -0
  80. {t402-1.6.1 → t402-1.7.1}/tests/clients/test_requests.py +0 -0
  81. {t402-1.6.1/tests/clients → t402-1.7.1/tests/fastapi_tests}/__init__.py +0 -0
  82. {t402-1.6.1 → t402-1.7.1}/tests/fastapi_tests/test_middleware.py +0 -0
  83. {t402-1.6.1/tests/fastapi_tests → t402-1.7.1/tests/flask_tests}/__init__.py +0 -0
  84. {t402-1.6.1 → t402-1.7.1}/tests/flask_tests/test_middleware.py +0 -0
  85. {t402-1.6.1 → t402-1.7.1}/tests/test_bridge.py +0 -0
  86. {t402-1.6.1 → t402-1.7.1}/tests/test_common.py +0 -0
  87. {t402-1.6.1 → t402-1.7.1}/tests/test_encoding.py +0 -0
  88. {t402-1.6.1 → t402-1.7.1}/tests/test_exact.py +0 -0
  89. {t402-1.6.1 → t402-1.7.1}/tests/test_mcp.py +0 -0
  90. {t402-1.6.1 → t402-1.7.1}/tests/test_svm.py +0 -0
  91. {t402-1.6.1 → t402-1.7.1}/tests/test_ton.py +0 -0
  92. {t402-1.6.1 → t402-1.7.1}/tests/test_tron.py +0 -0
  93. {t402-1.6.1 → t402-1.7.1}/tests/test_types.py +0 -0
  94. {t402-1.6.1 → t402-1.7.1}/tests/test_wdk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.6.1
3
+ Version: 1.7.1
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "t402"
3
- version = "1.6.1"
3
+ version = "1.7.1"
4
4
  description = "t402: An internet native payments protocol"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -1,5 +1,5 @@
1
1
  # Package version
2
- __version__ = "1.5.3"
2
+ __version__ = "1.6.2"
3
3
 
4
4
  # Re-export commonly used items for convenience
5
5
  from t402.common import (
@@ -16,8 +16,28 @@ from t402.networks import (
16
16
  get_network_type,
17
17
  )
18
18
  from t402.types import (
19
+ # Protocol version constants
20
+ T402_VERSION,
21
+ T402_VERSION_V1,
22
+ T402_VERSION_V2,
23
+ Network,
24
+ # V1 Types (Legacy)
19
25
  PaymentRequirements,
26
+ PaymentRequirementsV1,
20
27
  PaymentPayload,
28
+ PaymentPayloadV1,
29
+ t402PaymentRequiredResponse,
30
+ t402PaymentRequiredResponseV1,
31
+ # V2 Types (Current)
32
+ ResourceInfo,
33
+ PaymentRequirementsV2,
34
+ PaymentRequiredV2,
35
+ PaymentPayloadV2,
36
+ PaymentResponseV2,
37
+ # Facilitator Types
38
+ SupportedKind,
39
+ SupportedResponse,
40
+ # Common Types
21
41
  VerifyResponse,
22
42
  SettleResponse,
23
43
  TonAuthorization,
@@ -25,6 +45,31 @@ from t402.types import (
25
45
  TronAuthorization,
26
46
  TronPaymentPayload,
27
47
  )
48
+ from t402.encoding import (
49
+ # Base64 utilities
50
+ safe_base64_encode,
51
+ safe_base64_decode,
52
+ is_valid_base64,
53
+ # Header name constants
54
+ HEADER_PAYMENT_SIGNATURE,
55
+ HEADER_PAYMENT_REQUIRED,
56
+ HEADER_PAYMENT_RESPONSE,
57
+ HEADER_X_PAYMENT,
58
+ HEADER_X_PAYMENT_RESPONSE,
59
+ # Encoding/Decoding functions
60
+ encode_payment_signature_header,
61
+ decode_payment_signature_header,
62
+ encode_payment_required_header,
63
+ decode_payment_required_header,
64
+ encode_payment_response_header,
65
+ decode_payment_response_header,
66
+ # Header detection utilities
67
+ get_payment_header_name,
68
+ get_payment_response_header_name,
69
+ detect_protocol_version_from_headers,
70
+ extract_payment_from_headers,
71
+ extract_payment_required_from_response,
72
+ )
28
73
  from t402.facilitator import FacilitatorClient, FacilitatorConfig
29
74
  from t402.exact import (
30
75
  prepare_payment_header,
@@ -200,6 +245,50 @@ from t402.wdk import (
200
245
  BalanceError as WDKBalanceError,
201
246
  WDKErrorCode,
202
247
  )
248
+ from t402.schemes import (
249
+ # Interfaces
250
+ SchemeNetworkClient,
251
+ SchemeNetworkServer,
252
+ SchemeNetworkFacilitator,
253
+ BaseSchemeNetworkClient,
254
+ BaseSchemeNetworkServer,
255
+ BaseSchemeNetworkFacilitator,
256
+ # Registry
257
+ SchemeRegistry,
258
+ ClientSchemeRegistry,
259
+ ServerSchemeRegistry,
260
+ FacilitatorSchemeRegistry,
261
+ get_client_registry,
262
+ get_server_registry,
263
+ get_facilitator_registry,
264
+ reset_global_registries,
265
+ )
266
+ from t402.schemes.evm import (
267
+ ExactEvmClientScheme,
268
+ ExactEvmServerScheme,
269
+ EvmSigner,
270
+ )
271
+ from t402.schemes.ton import (
272
+ ExactTonClientScheme,
273
+ ExactTonServerScheme,
274
+ TonSigner,
275
+ )
276
+ from t402.schemes.tron import (
277
+ ExactTronClientScheme,
278
+ ExactTronServerScheme,
279
+ TronSigner,
280
+ )
281
+
282
+ # FastAPI Integration
283
+ from t402.fastapi import (
284
+ PaymentMiddleware as FastAPIPaymentMiddleware,
285
+ PaymentConfig as FastAPIPaymentConfig,
286
+ PaymentDetails as FastAPIPaymentDetails,
287
+ PaymentRequired,
288
+ require_payment as fastapi_require_payment,
289
+ get_payment_details,
290
+ settle_payment,
291
+ )
203
292
 
204
293
  def hello() -> str:
205
294
  return "Hello from t402!"
@@ -211,6 +300,11 @@ __all__ = [
211
300
  # Core
212
301
  "hello",
213
302
  "t402_VERSION",
303
+ # Protocol Version Constants
304
+ "T402_VERSION",
305
+ "T402_VERSION_V1",
306
+ "T402_VERSION_V2",
307
+ "Network",
214
308
  # Common utilities
215
309
  "parse_money",
216
310
  "process_price_to_atomic_amount",
@@ -221,15 +315,52 @@ __all__ = [
221
315
  "is_evm_network",
222
316
  "is_svm_network",
223
317
  "get_network_type",
224
- # Types
318
+ # V1 Types (Legacy)
225
319
  "PaymentRequirements",
320
+ "PaymentRequirementsV1",
226
321
  "PaymentPayload",
322
+ "PaymentPayloadV1",
323
+ "t402PaymentRequiredResponse",
324
+ "t402PaymentRequiredResponseV1",
325
+ # V2 Types (Current)
326
+ "ResourceInfo",
327
+ "PaymentRequirementsV2",
328
+ "PaymentRequiredV2",
329
+ "PaymentPayloadV2",
330
+ "PaymentResponseV2",
331
+ # Facilitator Types
332
+ "SupportedKind",
333
+ "SupportedResponse",
334
+ # Common Types
227
335
  "VerifyResponse",
228
336
  "SettleResponse",
229
337
  "TonAuthorization",
230
338
  "TonPaymentPayload",
231
339
  "TronAuthorization",
232
340
  "TronPaymentPayload",
341
+ # Encoding utilities
342
+ "safe_base64_encode",
343
+ "safe_base64_decode",
344
+ "is_valid_base64",
345
+ # Header constants
346
+ "HEADER_PAYMENT_SIGNATURE",
347
+ "HEADER_PAYMENT_REQUIRED",
348
+ "HEADER_PAYMENT_RESPONSE",
349
+ "HEADER_X_PAYMENT",
350
+ "HEADER_X_PAYMENT_RESPONSE",
351
+ # Header encoding/decoding
352
+ "encode_payment_signature_header",
353
+ "decode_payment_signature_header",
354
+ "encode_payment_required_header",
355
+ "decode_payment_required_header",
356
+ "encode_payment_response_header",
357
+ "decode_payment_response_header",
358
+ # Header detection
359
+ "get_payment_header_name",
360
+ "get_payment_response_header_name",
361
+ "detect_protocol_version_from_headers",
362
+ "extract_payment_from_headers",
363
+ "extract_payment_required_from_response",
233
364
  # Facilitator
234
365
  "FacilitatorClient",
235
366
  "FacilitatorConfig",
@@ -387,4 +518,40 @@ __all__ = [
387
518
  "SigningError",
388
519
  "WDKBalanceError",
389
520
  "WDKErrorCode",
521
+ # Scheme Interfaces
522
+ "SchemeNetworkClient",
523
+ "SchemeNetworkServer",
524
+ "SchemeNetworkFacilitator",
525
+ "BaseSchemeNetworkClient",
526
+ "BaseSchemeNetworkServer",
527
+ "BaseSchemeNetworkFacilitator",
528
+ # Scheme Registry
529
+ "SchemeRegistry",
530
+ "ClientSchemeRegistry",
531
+ "ServerSchemeRegistry",
532
+ "FacilitatorSchemeRegistry",
533
+ "get_client_registry",
534
+ "get_server_registry",
535
+ "get_facilitator_registry",
536
+ "reset_global_registries",
537
+ # EVM Schemes
538
+ "ExactEvmClientScheme",
539
+ "ExactEvmServerScheme",
540
+ "EvmSigner",
541
+ # TON Schemes
542
+ "ExactTonClientScheme",
543
+ "ExactTonServerScheme",
544
+ "TonSigner",
545
+ # TRON Schemes
546
+ "ExactTronClientScheme",
547
+ "ExactTronServerScheme",
548
+ "TronSigner",
549
+ # FastAPI Integration
550
+ "FastAPIPaymentMiddleware",
551
+ "FastAPIPaymentConfig",
552
+ "FastAPIPaymentDetails",
553
+ "PaymentRequired",
554
+ "fastapi_require_payment",
555
+ "get_payment_details",
556
+ "settle_payment",
390
557
  ]
@@ -1,5 +1,5 @@
1
1
  from decimal import Decimal
2
- from typing import List, Optional
2
+ from typing import List, Optional, Union
3
3
 
4
4
  from t402.chains import (
5
5
  get_chain_id,
@@ -9,7 +9,15 @@ from t402.chains import (
9
9
  get_default_token_address,
10
10
  )
11
11
  from t402.networks import is_ton_network, is_tron_network
12
- from t402.types import Price, TokenAmount, PaymentRequirements, PaymentPayload
12
+ from t402.types import (
13
+ Price,
14
+ TokenAmount,
15
+ PaymentRequirementsV1,
16
+ PaymentRequirementsV2,
17
+ PaymentPayloadV1,
18
+ PaymentPayloadV2,
19
+ T402_VERSION,
20
+ )
13
21
 
14
22
 
15
23
  def parse_money(amount: str | int, address: str, network: str) -> int:
@@ -147,23 +155,44 @@ def get_usdc_address(chain_id: int | str) -> str:
147
155
 
148
156
 
149
157
  def find_matching_payment_requirements(
150
- payment_requirements: List[PaymentRequirements],
151
- payment: PaymentPayload,
152
- ) -> Optional[PaymentRequirements]:
158
+ payment_requirements: List[Union[PaymentRequirementsV1, PaymentRequirementsV2]],
159
+ payment: Union[PaymentPayloadV1, PaymentPayloadV2, dict],
160
+ ) -> Optional[Union[PaymentRequirementsV1, PaymentRequirementsV2]]:
153
161
  """
154
162
  Finds the matching payment requirements for the given payment.
155
163
 
164
+ Supports both V1 and V2 payment formats.
165
+
156
166
  Args:
157
167
  payment_requirements: The payment requirements to search through
158
- payment: The payment to match against
168
+ payment: The payment to match against (V1, V2, or dict)
159
169
 
160
170
  Returns:
161
171
  The matching payment requirements or None if no match is found
162
172
  """
173
+ # Handle dict input
174
+ if isinstance(payment, dict):
175
+ payment_scheme = payment.get("scheme")
176
+ payment_network = payment.get("network")
177
+ # V2 format uses "accepted" field
178
+ if "accepted" in payment:
179
+ accepted = payment["accepted"]
180
+ payment_scheme = accepted.get("scheme")
181
+ payment_network = accepted.get("network")
182
+ elif hasattr(payment, "accepted"):
183
+ # V2 PaymentPayload
184
+ payment_scheme = payment.accepted.scheme
185
+ payment_network = payment.accepted.network
186
+ else:
187
+ # V1 PaymentPayload
188
+ payment_scheme = payment.scheme
189
+ payment_network = payment.network
190
+
163
191
  for req in payment_requirements:
164
- if req.scheme == payment.scheme and req.network == payment.network:
192
+ if req.scheme == payment_scheme and req.network == payment_network:
165
193
  return req
166
194
  return None
167
195
 
168
196
 
169
- t402_VERSION = 1
197
+ # Re-export version constant for backward compatibility
198
+ t402_VERSION = T402_VERSION
@@ -0,0 +1,325 @@
1
+ """T402 Protocol Encoding/Decoding Utilities.
2
+
3
+ This module provides encoding and decoding utilities for the T402 payment protocol,
4
+ supporting both V1 and V2 protocol versions.
5
+
6
+ V1 Headers:
7
+ - X-PAYMENT: Payment signature (client → server)
8
+ - X-PAYMENT-RESPONSE: Settlement response (server → client)
9
+ - Response body: PaymentRequired JSON
10
+
11
+ V2 Headers:
12
+ - PAYMENT-SIGNATURE: Payment signature (client → server)
13
+ - PAYMENT-REQUIRED: Payment requirements (server → client, 402 response)
14
+ - PAYMENT-RESPONSE: Settlement response (server → client)
15
+ """
16
+
17
+ import base64
18
+ import json
19
+ import re
20
+ from typing import Any, Union
21
+
22
+ from t402.types import (
23
+ PaymentPayloadV1,
24
+ PaymentPayloadV2,
25
+ PaymentRequiredV2,
26
+ PaymentResponseV2,
27
+ SettleResponse,
28
+ T402_VERSION_V1,
29
+ T402_VERSION_V2,
30
+ )
31
+
32
+ # Header name constants
33
+ HEADER_PAYMENT_SIGNATURE = "PAYMENT-SIGNATURE" # V2: Client payment
34
+ HEADER_PAYMENT_REQUIRED = "PAYMENT-REQUIRED" # V2: Server 402 response
35
+ HEADER_PAYMENT_RESPONSE = "PAYMENT-RESPONSE" # V2: Server settlement
36
+ HEADER_X_PAYMENT = "X-PAYMENT" # V1: Client payment
37
+ HEADER_X_PAYMENT_RESPONSE = "X-PAYMENT-RESPONSE" # V1: Server settlement
38
+
39
+ # Base64 validation regex (standard base64 with optional padding)
40
+ BASE64_REGEX = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
41
+
42
+
43
+ def safe_base64_encode(data: Union[str, bytes]) -> str:
44
+ """Safely encode string or bytes to base64 string.
45
+
46
+ Args:
47
+ data: String or bytes to encode
48
+
49
+ Returns:
50
+ Base64 encoded string
51
+ """
52
+ if isinstance(data, str):
53
+ data = data.encode("utf-8")
54
+ return base64.b64encode(data).decode("utf-8")
55
+
56
+
57
+ def safe_base64_decode(data: str) -> str:
58
+ """Safely decode base64 string to bytes and then to utf-8 string.
59
+
60
+ Args:
61
+ data: Base64 encoded string
62
+
63
+ Returns:
64
+ Decoded utf-8 string
65
+ """
66
+ return base64.b64decode(data).decode("utf-8")
67
+
68
+
69
+ def is_valid_base64(data: str) -> bool:
70
+ """Check if a string is valid base64.
71
+
72
+ Args:
73
+ data: String to validate
74
+
75
+ Returns:
76
+ True if valid base64, False otherwise
77
+ """
78
+ if not data:
79
+ return False
80
+ # Check length is multiple of 4 (with padding)
81
+ if len(data) % 4 != 0:
82
+ return False
83
+ return bool(BASE64_REGEX.match(data))
84
+
85
+
86
+ # =============================================================================
87
+ # V2 Header Encoding/Decoding
88
+ # =============================================================================
89
+
90
+
91
+ def encode_payment_signature_header(payment_payload: Union[PaymentPayloadV1, PaymentPayloadV2, dict]) -> str:
92
+ """Encode a payment payload as a base64 header value.
93
+
94
+ Works with both V1 and V2 payment payloads.
95
+
96
+ Args:
97
+ payment_payload: The payment payload to encode (model or dict)
98
+
99
+ Returns:
100
+ Base64 encoded string representation of the payment payload
101
+ """
102
+ if hasattr(payment_payload, "model_dump"):
103
+ data = payment_payload.model_dump(by_alias=True, exclude_none=True)
104
+ else:
105
+ data = payment_payload
106
+ return safe_base64_encode(json.dumps(data))
107
+
108
+
109
+ def decode_payment_signature_header(header_value: str) -> dict[str, Any]:
110
+ """Decode a base64 payment signature header into a payment payload dict.
111
+
112
+ The caller should determine the version from the t402Version field
113
+ and parse into the appropriate type.
114
+
115
+ Args:
116
+ header_value: The base64 encoded payment signature header
117
+
118
+ Returns:
119
+ The decoded payment payload as a dict
120
+
121
+ Raises:
122
+ ValueError: If the header is not valid base64 or JSON
123
+ """
124
+ if not is_valid_base64(header_value):
125
+ raise ValueError("Invalid payment signature header: not valid base64")
126
+ try:
127
+ return json.loads(safe_base64_decode(header_value))
128
+ except json.JSONDecodeError as e:
129
+ raise ValueError(f"Invalid payment signature header: invalid JSON - {e}")
130
+
131
+
132
+ def encode_payment_required_header(payment_required: Union[PaymentRequiredV2, dict]) -> str:
133
+ """Encode a payment required object as a base64 header value.
134
+
135
+ Args:
136
+ payment_required: The payment required object to encode (model or dict)
137
+
138
+ Returns:
139
+ Base64 encoded string representation of the payment required object
140
+ """
141
+ if hasattr(payment_required, "model_dump"):
142
+ data = payment_required.model_dump(by_alias=True, exclude_none=True)
143
+ else:
144
+ data = payment_required
145
+ return safe_base64_encode(json.dumps(data))
146
+
147
+
148
+ def decode_payment_required_header(header_value: str) -> dict[str, Any]:
149
+ """Decode a base64 payment required header.
150
+
151
+ Args:
152
+ header_value: The base64 encoded payment required header
153
+
154
+ Returns:
155
+ The decoded payment required object as a dict
156
+
157
+ Raises:
158
+ ValueError: If the header is not valid base64 or JSON
159
+ """
160
+ if not is_valid_base64(header_value):
161
+ raise ValueError("Invalid payment required header: not valid base64")
162
+ try:
163
+ return json.loads(safe_base64_decode(header_value))
164
+ except json.JSONDecodeError as e:
165
+ raise ValueError(f"Invalid payment required header: invalid JSON - {e}")
166
+
167
+
168
+ def encode_payment_response_header(
169
+ payment_response: Union[PaymentResponseV2, SettleResponse, dict],
170
+ requirements: dict[str, Any] | None = None,
171
+ ) -> str:
172
+ """Encode a payment response as a base64 header value.
173
+
174
+ Args:
175
+ payment_response: The payment response to encode (model or dict)
176
+ requirements: Optional requirements to include (for V2 format)
177
+
178
+ Returns:
179
+ Base64 encoded string representation of the payment response
180
+ """
181
+ if hasattr(payment_response, "model_dump"):
182
+ data = payment_response.model_dump(by_alias=True, exclude_none=True)
183
+ else:
184
+ data = dict(payment_response)
185
+
186
+ # If requirements provided separately, add them
187
+ if requirements and "requirements" not in data:
188
+ data["requirements"] = requirements
189
+
190
+ return safe_base64_encode(json.dumps(data))
191
+
192
+
193
+ def decode_payment_response_header(header_value: str) -> dict[str, Any]:
194
+ """Decode a base64 payment response header.
195
+
196
+ Args:
197
+ header_value: The base64 encoded payment response header
198
+
199
+ Returns:
200
+ The decoded payment response as a dict
201
+
202
+ Raises:
203
+ ValueError: If the header is not valid base64 or JSON
204
+ """
205
+ if not is_valid_base64(header_value):
206
+ raise ValueError("Invalid payment response header: not valid base64")
207
+ try:
208
+ return json.loads(safe_base64_decode(header_value))
209
+ except json.JSONDecodeError as e:
210
+ raise ValueError(f"Invalid payment response header: invalid JSON - {e}")
211
+
212
+
213
+ # =============================================================================
214
+ # Header Detection Utilities
215
+ # =============================================================================
216
+
217
+
218
+ def get_payment_header_name(version: int) -> str:
219
+ """Get the payment header name for a protocol version.
220
+
221
+ Args:
222
+ version: Protocol version (1 or 2)
223
+
224
+ Returns:
225
+ Header name string
226
+ """
227
+ if version == T402_VERSION_V1:
228
+ return HEADER_X_PAYMENT
229
+ return HEADER_PAYMENT_SIGNATURE
230
+
231
+
232
+ def get_payment_response_header_name(version: int) -> str:
233
+ """Get the payment response header name for a protocol version.
234
+
235
+ Args:
236
+ version: Protocol version (1 or 2)
237
+
238
+ Returns:
239
+ Header name string
240
+ """
241
+ if version == T402_VERSION_V1:
242
+ return HEADER_X_PAYMENT_RESPONSE
243
+ return HEADER_PAYMENT_RESPONSE
244
+
245
+
246
+ def detect_protocol_version_from_headers(headers: dict[str, str]) -> int:
247
+ """Detect the protocol version from HTTP headers.
248
+
249
+ Checks for V2 headers first, falls back to V1.
250
+
251
+ Args:
252
+ headers: HTTP headers (case-insensitive keys)
253
+
254
+ Returns:
255
+ Protocol version (1 or 2)
256
+ """
257
+ # Normalize header keys to lowercase for comparison
258
+ lower_headers = {k.lower(): v for k, v in headers.items()}
259
+
260
+ # Check for V2 headers first
261
+ if "payment-signature" in lower_headers or "payment-required" in lower_headers:
262
+ return T402_VERSION_V2
263
+
264
+ # Fall back to V1
265
+ if "x-payment" in lower_headers:
266
+ return T402_VERSION_V1
267
+
268
+ # Default to V2 (current version)
269
+ return T402_VERSION_V2
270
+
271
+
272
+ def extract_payment_from_headers(headers: dict[str, str]) -> tuple[int, str | None]:
273
+ """Extract payment header value and detect version from headers.
274
+
275
+ Args:
276
+ headers: HTTP headers (case-insensitive keys)
277
+
278
+ Returns:
279
+ Tuple of (version, header_value or None)
280
+ """
281
+ # Normalize header keys to lowercase for comparison
282
+ lower_headers = {k.lower(): v for k, v in headers.items()}
283
+
284
+ # Check V2 first
285
+ if "payment-signature" in lower_headers:
286
+ return T402_VERSION_V2, lower_headers["payment-signature"]
287
+
288
+ # Check V1
289
+ if "x-payment" in lower_headers:
290
+ return T402_VERSION_V1, lower_headers["x-payment"]
291
+
292
+ return T402_VERSION_V2, None
293
+
294
+
295
+ def extract_payment_required_from_response(
296
+ headers: dict[str, str],
297
+ body: dict[str, Any] | None = None,
298
+ ) -> tuple[int, dict[str, Any] | None]:
299
+ """Extract payment required from HTTP response (headers or body).
300
+
301
+ V2: Payment required is in PAYMENT-REQUIRED header
302
+ V1: Payment required is in response body
303
+
304
+ Args:
305
+ headers: HTTP response headers
306
+ body: Optional response body (for V1)
307
+
308
+ Returns:
309
+ Tuple of (version, payment_required dict or None)
310
+ """
311
+ # Normalize header keys
312
+ lower_headers = {k.lower(): v for k, v in headers.items()}
313
+
314
+ # Check V2 header first
315
+ if "payment-required" in lower_headers:
316
+ try:
317
+ return T402_VERSION_V2, decode_payment_required_header(lower_headers["payment-required"])
318
+ except ValueError:
319
+ pass
320
+
321
+ # Check V1 body
322
+ if body and isinstance(body, dict) and "t402Version" in body:
323
+ return body.get("t402Version", T402_VERSION_V1), body
324
+
325
+ return T402_VERSION_V2, None
@@ -28,7 +28,7 @@ class FacilitatorConfig(TypedDict, total=False):
28
28
  class FacilitatorClient:
29
29
  def __init__(self, config: Optional[FacilitatorConfig] = None):
30
30
  if config is None:
31
- config = {"url": "https://t402.org/facilitator"}
31
+ config = {"url": "https://facilitator.t402.io"}
32
32
 
33
33
  # Validate URL format
34
34
  url = config.get("url", "")