phion-client 1.52.9__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.
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: phion-client
3
+ Version: 1.52.9
4
+ Summary: Budget-safe x402 client for PHION agent services
5
+ Author: PHION Systems
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://phion.systems
8
+ Project-URL: Documentation, https://phion.systems/docs
9
+ Project-URL: Source, https://github.com/NeoNine0/phion-agent-connector
10
+ Project-URL: Issues, https://github.com/NeoNine0/phion-agent-connector/issues
11
+ Keywords: agents,mcp,x402,usdc,phion
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: x402<3,>=2.22
21
+ Requires-Dist: eth-account<1,>=0.13
22
+ Requires-Dist: requests<3,>=2.32
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest<9,>=8; extra == "test"
25
+
26
+ # PHION Python client
27
+
28
+ Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
29
+ origin, Base network, official USDC contract, PHION recipient and maximum amount,
30
+ then delegates local signing and retry to the official `x402` SDK.
31
+
32
+ Installing this package never authorizes a payment. A paid call requires an
33
+ explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
34
+ `SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
35
+ expiry and service-path limits through `SpendingMandate`.
36
+
37
+ ```bash
38
+ python -m venv .venv
39
+ . .venv/bin/activate
40
+ pip install -e .
41
+ export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
42
+ python example.py
43
+ ```
44
+
45
+ The example has a hard limit of `0.001 USDC` and verifies the signed result.
46
+
47
+ The public MCP decision surface remains available without this package at
48
+ `https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
49
+ mode when payment is not yet authorized.
@@ -0,0 +1,24 @@
1
+ # PHION Python client
2
+
3
+ Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
4
+ origin, Base network, official USDC contract, PHION recipient and maximum amount,
5
+ then delegates local signing and retry to the official `x402` SDK.
6
+
7
+ Installing this package never authorizes a payment. A paid call requires an
8
+ explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
9
+ `SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
10
+ expiry and service-path limits through `SpendingMandate`.
11
+
12
+ ```bash
13
+ python -m venv .venv
14
+ . .venv/bin/activate
15
+ pip install -e .
16
+ export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
17
+ python example.py
18
+ ```
19
+
20
+ The example has a hard limit of `0.001 USDC` and verifies the signed result.
21
+
22
+ The public MCP decision surface remains available without this package at
23
+ `https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
24
+ mode when payment is not yet authorized.
@@ -0,0 +1,26 @@
1
+ from .client import (
2
+ BASE_MAINNET,
3
+ BASE_USDC,
4
+ PHION_ORIGIN,
5
+ PHION_RECEIVER,
6
+ PhionBuyer,
7
+ SpendingPolicy,
8
+ SpendingMandate,
9
+ authorize_requirement,
10
+ decode_base64_json,
11
+ )
12
+
13
+ __version__ = "1.52.9"
14
+
15
+ __all__ = [
16
+ "BASE_MAINNET",
17
+ "BASE_USDC",
18
+ "PHION_ORIGIN",
19
+ "PHION_RECEIVER",
20
+ "PhionBuyer",
21
+ "SpendingPolicy",
22
+ "SpendingMandate",
23
+ "authorize_requirement",
24
+ "decode_base64_json",
25
+ "__version__",
26
+ ]
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import threading
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+ from urllib.parse import urljoin, urlparse
10
+
11
+ import requests
12
+
13
+ PHION_ORIGIN = "https://phion.systems"
14
+ BASE_MAINNET = "eip155:8453"
15
+ BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
16
+ PHION_RECEIVER = "0x7BEcA68DD9aC733D4de6b16d16B85Dc391c071e2"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SpendingPolicy:
21
+ """All amounts use atomic USDC units: 1,000 = 0.001 USDC."""
22
+
23
+ max_atomic: int
24
+ allowed_origins: tuple[str, ...] = (PHION_ORIGIN,)
25
+ allowed_networks: tuple[str, ...] = (BASE_MAINNET,)
26
+ allowed_assets: tuple[str, ...] = (BASE_USDC,)
27
+ allowed_recipients: tuple[str, ...] = (PHION_RECEIVER,)
28
+ allowed_paths: tuple[str, ...] | None = None
29
+ max_total_atomic: int | None = None
30
+ max_calls: int | None = None
31
+ expires_at: int | None = None
32
+
33
+
34
+ class SpendingMandate:
35
+ """Thread-safe, conservative in-process authorization budget."""
36
+
37
+ def __init__(
38
+ self,
39
+ policy: SpendingPolicy,
40
+ *,
41
+ authorized_atomic: int = 0,
42
+ calls: int = 0,
43
+ ):
44
+ if authorized_atomic < 0 or calls < 0:
45
+ raise ValueError("Invalid mandate snapshot")
46
+ if policy.max_total_atomic is not None and authorized_atomic > policy.max_total_atomic:
47
+ raise ValueError("Mandate snapshot exceeds cumulative limit")
48
+ self.policy = policy
49
+ self._authorized_atomic = authorized_atomic
50
+ self._calls = calls
51
+ self._lock = threading.Lock()
52
+
53
+ def authorize(self, requirement: dict[str, Any], url: str) -> None:
54
+ path = urlparse(url).path
55
+ with self._lock:
56
+ if self.policy.allowed_paths is not None and path not in self.policy.allowed_paths:
57
+ raise ValueError(f"Service path is not allowed: {path}")
58
+ if self.policy.expires_at is not None and int(time.time()) >= self.policy.expires_at:
59
+ raise ValueError("Spending mandate has expired")
60
+ if self.policy.max_calls is not None and self._calls >= self.policy.max_calls:
61
+ raise ValueError("Spending mandate call limit reached")
62
+ amount = int(requirement["amount"])
63
+ if (
64
+ self.policy.max_total_atomic is not None
65
+ and self._authorized_atomic + amount > self.policy.max_total_atomic
66
+ ):
67
+ raise ValueError("Spending mandate cumulative limit exceeded")
68
+ # Reserve before signing. Unknown settlements remain counted to avoid overspend.
69
+ self._authorized_atomic += amount
70
+ self._calls += 1
71
+
72
+ def snapshot(self) -> dict[str, int | None]:
73
+ with self._lock:
74
+ total = self.policy.max_total_atomic
75
+ return {
76
+ "authorized_atomic": self._authorized_atomic,
77
+ "calls": self._calls,
78
+ "remaining_atomic": None if total is None else total - self._authorized_atomic,
79
+ }
80
+
81
+
82
+ def decode_base64_json(value: str) -> dict[str, Any]:
83
+ padding = "=" * (-len(value) % 4)
84
+ return json.loads(base64.urlsafe_b64decode(value + padding))
85
+
86
+
87
+ def _contains(values: tuple[str, ...], candidate: str) -> bool:
88
+ return candidate.casefold() in {value.casefold() for value in values}
89
+
90
+
91
+ def authorize_requirement(
92
+ challenge: dict[str, Any], url: str, policy: SpendingPolicy
93
+ ) -> dict[str, Any]:
94
+ if challenge.get("x402Version") != 2:
95
+ raise ValueError("PHION returned an invalid x402 v2 challenge")
96
+ requirement = next(
97
+ (item for item in challenge.get("accepts", []) if item.get("scheme") == "exact"),
98
+ None,
99
+ )
100
+ if requirement is None:
101
+ raise ValueError("No supported exact payment requirement")
102
+
103
+ parsed = urlparse(url)
104
+ origin = f"{parsed.scheme}://{parsed.netloc}"
105
+ checks = (
106
+ (_contains(policy.allowed_origins, origin), f"Origin is not allowed: {origin}"),
107
+ (_contains(policy.allowed_networks, requirement.get("network", "")), "Network is not allowed"),
108
+ (_contains(policy.allowed_assets, requirement.get("asset", "")), "Asset is not allowed"),
109
+ (_contains(policy.allowed_recipients, requirement.get("payTo", "")), "Recipient is not allowed"),
110
+ )
111
+ for accepted, message in checks:
112
+ if not accepted:
113
+ raise ValueError(message)
114
+ amount_text = requirement.get("amount", "")
115
+ if not isinstance(amount_text, str) or not amount_text.isdigit():
116
+ raise ValueError("Payment amount must be an atomic-unit integer")
117
+ if int(amount_text) > policy.max_atomic:
118
+ raise ValueError(f"Price {amount_text} exceeds limit {policy.max_atomic}")
119
+ return requirement
120
+
121
+
122
+ class PhionBuyer:
123
+ def __init__(self, private_key: str):
124
+ # Imports stay here so policy validation can be tested without wallet packages.
125
+ from eth_account import Account
126
+ from x402 import x402ClientSync
127
+ from x402.mechanisms.evm import EthAccountSigner
128
+ from x402.mechanisms.evm.exact.register import register_exact_evm_client
129
+
130
+ self._client = x402ClientSync()
131
+ register_exact_evm_client(
132
+ self._client, EthAccountSigner(Account.from_key(private_key))
133
+ )
134
+
135
+ def call(
136
+ self, path: str, body: dict[str, Any], policy: SpendingPolicy | SpendingMandate
137
+ ) -> dict[str, Any]:
138
+ from x402.http.clients import x402_requests
139
+
140
+ url = urljoin(f"{PHION_ORIGIN}/", path.lstrip("/"))
141
+ # Quote first without a signer. This request cannot charge.
142
+ quote = requests.post(url, json=body, timeout=30)
143
+ if quote.status_code != 402:
144
+ raise RuntimeError(f"Expected an x402 quote, received HTTP {quote.status_code}")
145
+ encoded = quote.headers.get("PAYMENT-REQUIRED")
146
+ if not encoded:
147
+ raise RuntimeError("Missing PAYMENT-REQUIRED header")
148
+ effective_policy = policy.policy if isinstance(policy, SpendingMandate) else policy
149
+ requirement = authorize_requirement(decode_base64_json(encoded), url, effective_policy)
150
+ if isinstance(policy, SpendingMandate):
151
+ policy.authorize(requirement, url)
152
+
153
+ with x402_requests(self._client) as session:
154
+ response = session.post(url, json=body, timeout=60)
155
+ if not response.ok:
156
+ raise RuntimeError(
157
+ f"Paid PHION call failed with HTTP {response.status_code}: {response.text}"
158
+ )
159
+ settlement_header = response.headers.get("PAYMENT-RESPONSE")
160
+ return {
161
+ "data": response.json(),
162
+ "requirement": requirement,
163
+ "settlement": decode_base64_json(settlement_header)
164
+ if settlement_header
165
+ else None,
166
+ }
167
+
168
+ @staticmethod
169
+ def verify_receipt(
170
+ receipt: dict[str, Any], content: Any, sources: list[dict[str, Any]] | None = None
171
+ ) -> bool:
172
+ response = requests.post(
173
+ f"{PHION_ORIGIN}/v1/verify",
174
+ json={"receipt": receipt, "content": content, "sources": sources or []},
175
+ timeout=30,
176
+ )
177
+ response.raise_for_status()
178
+ return response.json().get("valid") is True
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: phion-client
3
+ Version: 1.52.9
4
+ Summary: Budget-safe x402 client for PHION agent services
5
+ Author: PHION Systems
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://phion.systems
8
+ Project-URL: Documentation, https://phion.systems/docs
9
+ Project-URL: Source, https://github.com/NeoNine0/phion-agent-connector
10
+ Project-URL: Issues, https://github.com/NeoNine0/phion-agent-connector/issues
11
+ Keywords: agents,mcp,x402,usdc,phion
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: x402<3,>=2.22
21
+ Requires-Dist: eth-account<1,>=0.13
22
+ Requires-Dist: requests<3,>=2.32
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest<9,>=8; extra == "test"
25
+
26
+ # PHION Python client
27
+
28
+ Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
29
+ origin, Base network, official USDC contract, PHION recipient and maximum amount,
30
+ then delegates local signing and retry to the official `x402` SDK.
31
+
32
+ Installing this package never authorizes a payment. A paid call requires an
33
+ explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
34
+ `SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
35
+ expiry and service-path limits through `SpendingMandate`.
36
+
37
+ ```bash
38
+ python -m venv .venv
39
+ . .venv/bin/activate
40
+ pip install -e .
41
+ export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
42
+ python example.py
43
+ ```
44
+
45
+ The example has a hard limit of `0.001 USDC` and verifies the signed result.
46
+
47
+ The public MCP decision surface remains available without this package at
48
+ `https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
49
+ mode when payment is not yet authorized.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ phion_client/__init__.py
4
+ phion_client/client.py
5
+ phion_client.egg-info/PKG-INFO
6
+ phion_client.egg-info/SOURCES.txt
7
+ phion_client.egg-info/dependency_links.txt
8
+ phion_client.egg-info/requires.txt
9
+ phion_client.egg-info/top_level.txt
10
+ tests/test_policy.py
@@ -0,0 +1,6 @@
1
+ x402<3,>=2.22
2
+ eth-account<1,>=0.13
3
+ requests<3,>=2.32
4
+
5
+ [test]
6
+ pytest<9,>=8
@@ -0,0 +1 @@
1
+ phion_client
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "phion-client"
7
+ version = "1.52.9"
8
+ description = "Budget-safe x402 client for PHION agent services"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ authors = [{ name = "PHION Systems" }]
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "x402>=2.22,<3",
15
+ "eth-account>=0.13,<1",
16
+ "requests>=2.32,<3"
17
+ ]
18
+
19
+ keywords = ["agents", "mcp", "x402", "usdc", "phion"]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://phion.systems"
31
+ Documentation = "https://phion.systems/docs"
32
+ Source = "https://github.com/NeoNine0/phion-agent-connector"
33
+ Issues = "https://github.com/NeoNine0/phion-agent-connector/issues"
34
+
35
+ [project.optional-dependencies]
36
+ test = ["pytest>=8,<9"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,79 @@
1
+ import pytest
2
+
3
+ from phion_client import (
4
+ BASE_MAINNET,
5
+ BASE_USDC,
6
+ PHION_RECEIVER,
7
+ SpendingPolicy,
8
+ SpendingMandate,
9
+ authorize_requirement,
10
+ __version__,
11
+ )
12
+
13
+
14
+ def test_package_version_matches_phion_release():
15
+ assert __version__ == "1.52.9"
16
+
17
+
18
+ def challenge():
19
+ return {
20
+ "x402Version": 2,
21
+ "accepts": [{
22
+ "scheme": "exact",
23
+ "network": BASE_MAINNET,
24
+ "asset": BASE_USDC,
25
+ "amount": "1000",
26
+ "payTo": PHION_RECEIVER,
27
+ }],
28
+ }
29
+
30
+
31
+ def test_accepts_quote_within_budget():
32
+ result = authorize_requirement(
33
+ challenge(), "https://phion.systems/v1/paid/attest", SpendingPolicy(1000)
34
+ )
35
+ assert result["amount"] == "1000"
36
+
37
+
38
+ def test_rejects_quote_above_budget():
39
+ with pytest.raises(ValueError, match="exceeds limit"):
40
+ authorize_requirement(
41
+ challenge(), "https://phion.systems/v1/paid/attest", SpendingPolicy(999)
42
+ )
43
+
44
+
45
+ def test_rejects_recipient_substitution():
46
+ value = challenge()
47
+ value["accepts"][0]["payTo"] = "0x0000000000000000000000000000000000000001"
48
+ with pytest.raises(ValueError, match="Recipient"):
49
+ authorize_requirement(
50
+ value, "https://phion.systems/v1/paid/attest", SpendingPolicy(1000)
51
+ )
52
+
53
+
54
+ def test_rejects_untrusted_origin():
55
+ with pytest.raises(ValueError, match="Origin"):
56
+ authorize_requirement(challenge(), "https://evil.example/attest", SpendingPolicy(1000))
57
+
58
+
59
+ def test_mandate_enforces_cumulative_and_service_limits():
60
+ mandate = SpendingMandate(
61
+ SpendingPolicy(
62
+ max_atomic=1_000,
63
+ max_total_atomic=2_000,
64
+ max_calls=2,
65
+ allowed_paths=("/v1/paid/attest",),
66
+ )
67
+ )
68
+ requirement = authorize_requirement(
69
+ challenge(), "https://phion.systems/v1/paid/attest", mandate.policy
70
+ )
71
+ mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")
72
+ mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")
73
+ assert mandate.snapshot() == {
74
+ "authorized_atomic": 2_000,
75
+ "calls": 2,
76
+ "remaining_atomic": 0,
77
+ }
78
+ with pytest.raises(ValueError, match="call limit"):
79
+ mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")