a2a-protocol-core 0.2.1__tar.gz → 0.3.0__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 (35) hide show
  1. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/CHANGELOG.md +38 -0
  2. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/PKG-INFO +72 -1
  3. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/README.md +66 -0
  4. a2a_protocol_core-0.3.0/examples/attest_byo_tx.py +56 -0
  5. a2a_protocol_core-0.3.0/examples/screen_and_verify.py +52 -0
  6. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/pyproject.toml +14 -1
  7. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/__init__.py +36 -3
  8. a2a_protocol_core-0.3.0/src/a2a_protocol_core/_retry.py +55 -0
  9. a2a_protocol_core-0.3.0/src/a2a_protocol_core/attestation_verify.py +264 -0
  10. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/client.py +38 -5
  11. a2a_protocol_core-0.3.0/src/a2a_protocol_core/screen.py +198 -0
  12. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/x402_pay.py +241 -2
  13. a2a_protocol_core-0.3.0/tests/conftest.py +7 -0
  14. a2a_protocol_core-0.3.0/tests/test_attestation_verify.py +245 -0
  15. a2a_protocol_core-0.3.0/tests/test_client.py +141 -0
  16. a2a_protocol_core-0.3.0/tests/test_pay_dispatch.py +173 -0
  17. a2a_protocol_core-0.3.0/tests/test_retry.py +74 -0
  18. a2a_protocol_core-0.3.0/tests/test_screen.py +255 -0
  19. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/.gitignore +0 -0
  20. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/LICENSE +0 -0
  21. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/examples/agent_pay_alias.py +0 -0
  22. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/examples/trigger_payment_hook.py +0 -0
  23. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/addressing.py +0 -0
  24. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/canonical_hash.py +0 -0
  25. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/py.typed +0 -0
  26. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/schemas.py +0 -0
  27. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/semantic_normalizer.py +0 -0
  28. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_addressing.py +0 -0
  29. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_canonical_hash.py +0 -0
  30. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_cross_language_vectors.py +0 -0
  31. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_schemas.py +0 -0
  32. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_semantic_normalizer.py +0 -0
  33. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_x402_pay.py +0 -0
  34. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/vectors/canonical_vectors.json +0 -0
  35. {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/vectors/generate_vectors.py +0 -0
@@ -3,6 +3,44 @@
3
3
  All notable changes to `a2a-protocol-core` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.3.0] — 2026-07-20
7
+
8
+ The version where the SDK can buy and verify the product end to end: pay DNS of
9
+ Money's own USDC-priced endpoints, screen a counterparty before paying it, and
10
+ cryptographically verify the attestation you paid for.
11
+
12
+ ### Added
13
+ - `pay_alias_usdc_algorand(...)` — pay a `pay:` alias in USDC on Algorand from
14
+ the caller's own wallet. Signs the USDC transfer leg locally via the OFFICIAL
15
+ x402 client mechanisms (new `[algorand]` extra → `x402-avm[avm]`); the
16
+ facilitator co-signs the fee leg and submits. Omit the amount to pay the
17
+ alias's declared (enforced) price.
18
+ - `pay_alias(...)` — rail-dispatching front door: `currency="XRP"` → XRPL,
19
+ `"USDC"` → Algorand. Deterministic table lookup, no scoring.
20
+ - `screen(...)` / `screen_with_payment_header(...)` / `ScreenResult` — client
21
+ for the paid counterparty screen (`GET /x402/screen/{target}`): pay the
22
+ server-priced fee, receive the OFAC + resolution attestation about a
23
+ caller-named `pay:` alias or raw address. "Screen before you pay," one call.
24
+ - `verify_attestation(...)` — client-side verification of the signed
25
+ attestation's `eddsa-jcs-2022` Data Integrity proof: resolves the issuer's
26
+ `did:web` document, enforces `assertionMethod` authorization, verifies the
27
+ Ed25519 signature (new `[verify]` extra → `cryptography`). JCS
28
+ canonicalization, base58btc, and did:web resolution are pure Python; an
29
+ unsigned attestation FAILS verification by design.
30
+ - `fetch_requirement_header(...)` — raw `PAYMENT-REQUIRED` header fetch (any
31
+ currency, amount optional) for callers driving official x402 clients.
32
+ - Bounded retry with exponential backoff on every idempotent HTTP leg
33
+ (challenge fetch, settle, capabilities, DID-document fetch): connection
34
+ errors and 502/503/504 only, fixed attempt count, no jitter. Settle legs are
35
+ retry-safe because the server checks idempotency before verification. The
36
+ payment-hook POST is never auto-retried — re-send with the same
37
+ `job_id` + `semantic_hash` (the server's idempotency key) instead.
38
+
39
+ ### Changed
40
+ - `A2APaymentHookClient` now raises `A2AClientError` (with `status_code` and a
41
+ `body` snippet) instead of a bare `requests.HTTPError`, so agents can branch
42
+ on the failure. `capabilities()` retries transient failures.
43
+
6
44
  ## [0.2.1] — 2026-07-20
7
45
 
8
46
  Conformance fix for the XRPL exact scheme's field placement. Required before
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-protocol-core
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Open, deterministic protocol core for the DNS of Money A2A payment surface (FAS-1 pay: addressing, canonical hashing, payment-hook client).
5
5
  Project-URL: Homepage, https://dnsofmoney.com
6
6
  Project-URL: Specification, https://github.com/dnsofmoney/dns-of-money
@@ -17,9 +17,14 @@ Classifier: Topic :: Office/Business :: Financial
17
17
  Requires-Python: >=3.10
18
18
  Requires-Dist: pydantic>=2.10.0
19
19
  Requires-Dist: requests>=2.31.0
20
+ Provides-Extra: algorand
21
+ Requires-Dist: x402-avm[avm]>=2.0.2; extra == 'algorand'
20
22
  Provides-Extra: dev
23
+ Requires-Dist: cryptography>=42.0.0; extra == 'dev'
21
24
  Requires-Dist: pytest>=8.3.0; extra == 'dev'
22
25
  Requires-Dist: ruff>=0.8.0; extra == 'dev'
26
+ Provides-Extra: verify
27
+ Requires-Dist: cryptography>=42.0.0; extra == 'verify'
23
28
  Provides-Extra: xrpl
24
29
  Requires-Dist: xrpl-py>=2.5.0; extra == 'xrpl'
25
30
  Description-Content-Type: text/markdown
@@ -53,6 +58,9 @@ Runtime deps are intentionally minimal: `pydantic` and `requests`.
53
58
  | `canonical_hash` | A2A-008 | metadata- & vocabulary-stable payment-intent hash |
54
59
  | `schemas` | A2A-041 | payment-hook request/response wire models |
55
60
  | `client` | A2A-041 | `A2APaymentHookClient` over `/v1/a2a/*` |
61
+ | `x402_pay` | x402 | one-call pay: XRP on XRPL (`[xrpl]`) / USDC on Algorand (`[algorand]`) |
62
+ | `screen` | x402 | paid counterparty screen — "screen before you pay" |
63
+ | `attestation_verify` | did:web | verify the attestation's Ed25519 proof (`[verify]`) |
56
64
 
57
65
  ## Quick start
58
66
 
@@ -124,6 +132,69 @@ result = attest_settled_payment(
124
132
  DNS of Money **verifies an already-settled transaction and returns metadata** — it
125
133
  never holds your keys or your funds.
126
134
 
135
+ ## Pay in USDC on Algorand (the priced endpoints)
136
+
137
+ DNS of Money's own paid endpoints declare their price in USDC on Algorand.
138
+ `pay_alias_usdc_algorand` (or the rail-dispatching `pay_alias`) signs the USDC
139
+ transfer leg locally via the **official x402 client mechanisms** — omit the
140
+ amount and the server quotes its declared, enforced price:
141
+
142
+ ```bash
143
+ pip install "a2a-protocol-core[algorand]"
144
+ ```
145
+
146
+ ```python
147
+ from a2a_protocol_core import pay_alias_usdc_algorand
148
+
149
+ result = pay_alias_usdc_algorand(
150
+ base_url="https://api.dnsofmoney.com",
151
+ alias="pay:dnsofmoney",
152
+ api_key="fas_live_...",
153
+ mnemonic="...your 25-word Algorand mnemonic...", # signs locally, never sent
154
+ )
155
+ print(result.tx_hash, result.summary.verdict)
156
+ ```
157
+
158
+ ## Screen before you pay
159
+
160
+ Pay the screening fee, get an OFAC + resolution attestation about a
161
+ **caller-named** target — a `pay:` alias or a raw any-chain address — before you
162
+ send it money:
163
+
164
+ ```python
165
+ from a2a_protocol_core import screen
166
+
167
+ result = screen(
168
+ base_url="https://api.dnsofmoney.com",
169
+ target="pay:vendor.alpha",
170
+ api_key="fas_live_...",
171
+ algorand_mnemonic="...", # fee is USDC on Algorand (server-priced)
172
+ verify=True, # also check the attestation signature
173
+ )
174
+ if result.verdict == "CLEAR":
175
+ ... # proceed to pay the vendor
176
+ ```
177
+
178
+ ## Verify the attestation yourself
179
+
180
+ Attestations are W3C Verifiable Credentials signed `eddsa-jcs-2022` by a
181
+ `did:web` issuer. `verify_attestation` resolves the issuer's DID document,
182
+ enforces `assertionMethod` authorization, and checks the Ed25519 signature —
183
+ so the proof chain ends at *your* process, not at TLS:
184
+
185
+ ```bash
186
+ pip install "a2a-protocol-core[verify]"
187
+ ```
188
+
189
+ ```python
190
+ from a2a_protocol_core import verify_attestation
191
+
192
+ v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
193
+ assert v.verified
194
+ ```
195
+
196
+ An unsigned attestation **fails** verification by design.
197
+
127
198
  ## Why canonical hashing?
128
199
 
129
200
  Two agents describing the same payment with different words ("send" vs
@@ -27,6 +27,9 @@ Runtime deps are intentionally minimal: `pydantic` and `requests`.
27
27
  | `canonical_hash` | A2A-008 | metadata- & vocabulary-stable payment-intent hash |
28
28
  | `schemas` | A2A-041 | payment-hook request/response wire models |
29
29
  | `client` | A2A-041 | `A2APaymentHookClient` over `/v1/a2a/*` |
30
+ | `x402_pay` | x402 | one-call pay: XRP on XRPL (`[xrpl]`) / USDC on Algorand (`[algorand]`) |
31
+ | `screen` | x402 | paid counterparty screen — "screen before you pay" |
32
+ | `attestation_verify` | did:web | verify the attestation's Ed25519 proof (`[verify]`) |
30
33
 
31
34
  ## Quick start
32
35
 
@@ -98,6 +101,69 @@ result = attest_settled_payment(
98
101
  DNS of Money **verifies an already-settled transaction and returns metadata** — it
99
102
  never holds your keys or your funds.
100
103
 
104
+ ## Pay in USDC on Algorand (the priced endpoints)
105
+
106
+ DNS of Money's own paid endpoints declare their price in USDC on Algorand.
107
+ `pay_alias_usdc_algorand` (or the rail-dispatching `pay_alias`) signs the USDC
108
+ transfer leg locally via the **official x402 client mechanisms** — omit the
109
+ amount and the server quotes its declared, enforced price:
110
+
111
+ ```bash
112
+ pip install "a2a-protocol-core[algorand]"
113
+ ```
114
+
115
+ ```python
116
+ from a2a_protocol_core import pay_alias_usdc_algorand
117
+
118
+ result = pay_alias_usdc_algorand(
119
+ base_url="https://api.dnsofmoney.com",
120
+ alias="pay:dnsofmoney",
121
+ api_key="fas_live_...",
122
+ mnemonic="...your 25-word Algorand mnemonic...", # signs locally, never sent
123
+ )
124
+ print(result.tx_hash, result.summary.verdict)
125
+ ```
126
+
127
+ ## Screen before you pay
128
+
129
+ Pay the screening fee, get an OFAC + resolution attestation about a
130
+ **caller-named** target — a `pay:` alias or a raw any-chain address — before you
131
+ send it money:
132
+
133
+ ```python
134
+ from a2a_protocol_core import screen
135
+
136
+ result = screen(
137
+ base_url="https://api.dnsofmoney.com",
138
+ target="pay:vendor.alpha",
139
+ api_key="fas_live_...",
140
+ algorand_mnemonic="...", # fee is USDC on Algorand (server-priced)
141
+ verify=True, # also check the attestation signature
142
+ )
143
+ if result.verdict == "CLEAR":
144
+ ... # proceed to pay the vendor
145
+ ```
146
+
147
+ ## Verify the attestation yourself
148
+
149
+ Attestations are W3C Verifiable Credentials signed `eddsa-jcs-2022` by a
150
+ `did:web` issuer. `verify_attestation` resolves the issuer's DID document,
151
+ enforces `assertionMethod` authorization, and checks the Ed25519 signature —
152
+ so the proof chain ends at *your* process, not at TLS:
153
+
154
+ ```bash
155
+ pip install "a2a-protocol-core[verify]"
156
+ ```
157
+
158
+ ```python
159
+ from a2a_protocol_core import verify_attestation
160
+
161
+ v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
162
+ assert v.verified
163
+ ```
164
+
165
+ An unsigned attestation **fails** verification by design.
166
+
101
167
  ## Why canonical hashing?
102
168
 
103
169
  Two agents describing the same payment with different words ("send" vs
@@ -0,0 +1,56 @@
1
+ """
2
+ Bring-your-own-wallet attestation — no signing extras needed.
3
+
4
+ For agents that already settled the payment through their own wallet stack
5
+ (their own xrpl-py flow, a Coinbase Agentic Wallet, anything that produced a
6
+ validated XRPL tx hash): skip the SDK's signing path entirely and just buy the
7
+ read-only verify + signed attestation for the tx you already made. Base
8
+ install only — no [xrpl], no [algorand].
9
+
10
+ pip install a2a-protocol-core # + [verify] to check the signature
11
+
12
+ Env:
13
+ FAS_API_KEY tenant API key
14
+ SETTLED_TX the already-validated XRPL payment hash
15
+ PAY_ALIAS alias that was paid (default pay:vendor.alpha)
16
+ AMOUNT_XRP the amount that tx paid (default 0.10)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import sys
23
+
24
+ from a2a_protocol_core import attest_settled_payment, verify_attestation
25
+ from a2a_protocol_core.attestation_verify import AttestationVerificationError
26
+
27
+
28
+ def main() -> int:
29
+ api_key = os.getenv("FAS_API_KEY", "").strip()
30
+ tx_hash = os.getenv("SETTLED_TX", "").strip()
31
+ if not api_key or not tx_hash:
32
+ print("set FAS_API_KEY and SETTLED_TX")
33
+ return 2
34
+
35
+ result = attest_settled_payment(
36
+ base_url=os.getenv("FAS_BASE_URL", "https://api.dnsofmoney.com"),
37
+ alias=os.getenv("PAY_ALIAS", "pay:vendor.alpha"),
38
+ amount_xrp=os.getenv("AMOUNT_XRP", "0.10"),
39
+ tx_hash=tx_hash,
40
+ api_key=api_key,
41
+ )
42
+ print(f"settled : {result.settled} (idempotent={result.idempotent})")
43
+ print(f"verdict : {result.summary.verdict}")
44
+ print(f"signed : {result.summary.signed}")
45
+
46
+ try:
47
+ v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
48
+ print(f"verified : {v.verified} by {v.issuer}")
49
+ except AttestationVerificationError as exc:
50
+ print(f"verify : FAILED — {exc}")
51
+ return 1
52
+ return 0
53
+
54
+
55
+ if __name__ == "__main__":
56
+ sys.exit(main())
@@ -0,0 +1,52 @@
1
+ """
2
+ Screen a counterparty before paying it — and verify the attestation yourself.
3
+
4
+ The end-to-end 0.3.0 flow: pay the screening fee in USDC on Algorand from your
5
+ own wallet (server-priced; you never name the amount), receive the OFAC +
6
+ resolution attestation about the TARGET you named, and check the issuer's
7
+ Ed25519 signature against its did:web document locally.
8
+
9
+ pip install "a2a-protocol-core[algorand,verify]"
10
+
11
+ Env:
12
+ FAS_API_KEY tenant API key (the settle leg requires X-API-Key)
13
+ ALGORAND_MNEMONIC your 25-word payer mnemonic — signs locally, never sent
14
+ SCREEN_TARGET pay: alias or raw address to screen (default below)
15
+ FAS_BASE_URL default https://api.dnsofmoney.com
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+
23
+ from a2a_protocol_core import screen
24
+
25
+
26
+ def main() -> int:
27
+ api_key = os.getenv("FAS_API_KEY", "").strip()
28
+ mnemonic = os.getenv("ALGORAND_MNEMONIC", "").strip()
29
+ if not api_key or not mnemonic:
30
+ print("set FAS_API_KEY and ALGORAND_MNEMONIC")
31
+ return 2
32
+
33
+ result = screen(
34
+ base_url=os.getenv("FAS_BASE_URL", "https://api.dnsofmoney.com"),
35
+ target=os.getenv("SCREEN_TARGET", "pay:vendor.alpha"),
36
+ api_key=api_key,
37
+ algorand_mnemonic=mnemonic,
38
+ verify=True, # don't just trust TLS — check the issuer's signature
39
+ expected_issuer="did:web:dnsofmoney.com",
40
+ )
41
+
42
+ print(f"target : {result.target}")
43
+ print(f"verdict : {result.verdict}")
44
+ print(f"fee txid : {result.proof.get('transaction')}")
45
+ print(f"signed : {result.summary.signed}")
46
+ if result.verification:
47
+ print(f"verified : {result.verification.verified} by {result.verification.issuer}")
48
+ return 0 if result.verdict == "CLEAR" else 1
49
+
50
+
51
+ if __name__ == "__main__":
52
+ sys.exit(main())
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "a2a-protocol-core"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Open, deterministic protocol core for the DNS of Money A2A payment surface (FAS-1 pay: addressing, canonical hashing, payment-hook client)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -36,9 +36,22 @@ Source = "https://github.com/dnsofmoney/a2a-protocol-core"
36
36
  xrpl = [
37
37
  "xrpl-py>=2.5.0",
38
38
  ]
39
+ # USDC-on-Algorand pay path (pay_alias_usdc_algorand / screen fee): drives the
40
+ # OFFICIAL x402 client mechanisms to build the atomic group — never hand-rolled.
41
+ # x402-avm is GoPlausible's distribution of the x402 client WITH the AVM
42
+ # mechanisms registered (it provides the `x402` import namespace).
43
+ algorand = [
44
+ "x402-avm[avm]>=2.0.2",
45
+ ]
46
+ # Attestation signature verification (verify_attestation): Ed25519 only — JCS,
47
+ # base58btc, and did:web resolution are pure Python.
48
+ verify = [
49
+ "cryptography>=42.0.0",
50
+ ]
39
51
  dev = [
40
52
  "pytest>=8.3.0",
41
53
  "ruff>=0.8.0",
54
+ "cryptography>=42.0.0",
42
55
  ]
43
56
 
44
57
  [tool.hatch.build.targets.wheel]
@@ -19,7 +19,7 @@ from a2a_protocol_core.addressing import (
19
19
  is_valid_pay_uri,
20
20
  )
21
21
  from a2a_protocol_core.canonical_hash import compute_canonical_hash
22
- from a2a_protocol_core.client import A2APaymentHookClient
22
+ from a2a_protocol_core.client import A2AClientError, A2APaymentHookClient
23
23
  from a2a_protocol_core.schemas import (
24
24
  A2ACapabilities,
25
25
  A2APaymentHookRequest,
@@ -34,19 +34,36 @@ from a2a_protocol_core.semantic_normalizer import (
34
34
  normalize_action,
35
35
  normalize_message,
36
36
  )
37
+ from a2a_protocol_core.attestation_verify import (
38
+ AttestationVerification,
39
+ AttestationVerificationError,
40
+ did_web_document_url,
41
+ fetch_did_document,
42
+ verify_attestation,
43
+ )
44
+ from a2a_protocol_core.screen import (
45
+ ScreenResult,
46
+ fetch_screen_requirement_header,
47
+ screen,
48
+ screen_with_payment_header,
49
+ )
37
50
  from a2a_protocol_core.x402_pay import (
38
51
  AttestationSummary,
39
52
  X402PaymentResult,
40
53
  X402PayError,
41
54
  attest_settled_payment,
55
+ build_avm_payment_header,
42
56
  build_x_payment_header,
43
57
  decode_payment_required,
44
58
  fetch_requirement,
59
+ fetch_requirement_header,
60
+ pay_alias,
61
+ pay_alias_usdc_algorand,
45
62
  pay_alias_xrp,
46
63
  summarize_attestation,
47
64
  )
48
65
 
49
- __version__ = "0.2.1"
66
+ __version__ = "0.3.0"
50
67
 
51
68
  __all__ = [
52
69
  "__version__",
@@ -70,14 +87,30 @@ __all__ = [
70
87
  "A2ACapabilities",
71
88
  # client
72
89
  "A2APaymentHookClient",
73
- # x402 pay-path (one-call; signing needs the [xrpl] extra)
90
+ "A2AClientError",
91
+ # x402 pay-path (one-call; XRP signing needs [xrpl], USDC needs [algorand])
92
+ "pay_alias",
74
93
  "pay_alias_xrp",
94
+ "pay_alias_usdc_algorand",
75
95
  "attest_settled_payment",
76
96
  "fetch_requirement",
97
+ "fetch_requirement_header",
77
98
  "decode_payment_required",
78
99
  "build_x_payment_header",
100
+ "build_avm_payment_header",
79
101
  "summarize_attestation",
80
102
  "AttestationSummary",
81
103
  "X402PaymentResult",
82
104
  "X402PayError",
105
+ # paid counterparty screen ("screen before you pay")
106
+ "screen",
107
+ "screen_with_payment_header",
108
+ "fetch_screen_requirement_header",
109
+ "ScreenResult",
110
+ # attestation verification (signature checking needs the [verify] extra)
111
+ "verify_attestation",
112
+ "fetch_did_document",
113
+ "did_web_document_url",
114
+ "AttestationVerification",
115
+ "AttestationVerificationError",
83
116
  ]
@@ -0,0 +1,55 @@
1
+ """
2
+ Bounded, deterministic retry for idempotent HTTP legs.
3
+
4
+ Agent runtimes call these endpoints over flaky networks; a transient connection
5
+ reset or a 502 from a proxy should not fail a whole pay/screen flow. Retries
6
+ are DELIBERATELY narrow:
7
+
8
+ - GET only, and only legs that are idempotent by contract. The x402 settle leg
9
+ qualifies: the server checks idempotency BEFORE verification, so re-sending
10
+ the same settled tx proof returns the recorded outcome rather than
11
+ double-settling.
12
+ - Connection errors / timeouts and 502/503/504 only. A 4xx or a 500 is a real
13
+ answer and is returned/raised immediately — retrying it would just repeat it.
14
+ - Fixed attempt count, exponential backoff, no jitter — deterministic, like
15
+ everything else in this package.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import time
21
+ from typing import Optional
22
+
23
+ import requests
24
+
25
+ RETRYABLE_STATUS = frozenset({502, 503, 504})
26
+ DEFAULT_RETRIES = 2 # total attempts = retries + 1
27
+ DEFAULT_BACKOFF = 0.5 # seconds; doubles per attempt
28
+
29
+
30
+ def get_with_retries(
31
+ session: requests.Session,
32
+ url: str,
33
+ *,
34
+ params: Optional[dict] = None,
35
+ headers: Optional[dict] = None,
36
+ timeout: int,
37
+ retries: int = DEFAULT_RETRIES,
38
+ backoff: float = DEFAULT_BACKOFF,
39
+ ) -> requests.Response:
40
+ """GET with bounded retries on transient failures (see module docstring)."""
41
+ attempt = 0
42
+ while True:
43
+ try:
44
+ resp = session.get(url, params=params, headers=headers, timeout=timeout)
45
+ except (requests.ConnectionError, requests.Timeout):
46
+ if attempt >= retries:
47
+ raise
48
+ time.sleep(backoff * (2**attempt))
49
+ attempt += 1
50
+ continue
51
+ if resp.status_code in RETRYABLE_STATUS and attempt < retries:
52
+ time.sleep(backoff * (2**attempt))
53
+ attempt += 1
54
+ continue
55
+ return resp