m2mcent-sdk 1.0.3__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.
m2mcent/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .client import M2MClient, M2MCentClient
2
+ from .langchain_toolkit import M2MCentToolkit
3
+
4
+ __all__ = ["M2MClient", "M2MCentClient", "M2MCentToolkit"]
m2mcent/client.py ADDED
@@ -0,0 +1,164 @@
1
+ import requests
2
+ import base64
3
+ import json
4
+ import time
5
+ import secrets
6
+ from eth_account import Account
7
+ from eth_account.messages import encode_typed_data
8
+
9
+
10
+ class M2MClient:
11
+ """
12
+ M2MCent SDK Client (Python v2.0 - x402 V2 Compliant)
13
+ Implements automated x402 V2 EIP-3009 payment protocol on Base Mainnet (8453).
14
+ """
15
+
16
+ ESCROW_ADDRESS = "0x5d47070A36A37DAf417e64FF27a1e5B3F1c84efa"
17
+ TREASURY_ADDRESS = "0x8aaBAB75bE8825d0f5D514a9a5cBa04B7bF84920"
18
+ USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
19
+ CHAIN_ID = 8453 # Base Mainnet
20
+
21
+ USDC_DOMAIN_NAME = "USD Coin"
22
+ USDC_VERSION = "2"
23
+
24
+ def __init__(self, private_key: str, rpc_url: str = "https://mainnet.base.org", gateway_url: str = "https://api.m2mcent.com"):
25
+ clean_key = private_key if private_key.startswith("0x") else f"0x{private_key}"
26
+ self.account = Account.from_key(clean_key)
27
+ self.rpc_url = rpc_url
28
+ self.gateway_url = gateway_url.rstrip("/")
29
+
30
+ def _parse_402_challenge(self, response: requests.Response) -> dict:
31
+ header_b64 = response.headers.get("Payment-Required") or response.headers.get("payment-required")
32
+ if header_b64:
33
+ try:
34
+ decoded = json.loads(base64.b64decode(header_b64).decode("utf-8"))
35
+ accepts = decoded.get("accepts") or []
36
+ if accepts:
37
+ acc = accepts[0]
38
+ amount = acc.get("amount") or acc.get("maxAmountRequired")
39
+ if amount:
40
+ return {
41
+ "amountRaw": int(amount),
42
+ "escrow": acc.get("escrow") or (acc.get("extra") or {}).get("escrow") or acc.get("payTo") or self.ESCROW_ADDRESS,
43
+ "asset": acc.get("asset") or self.USDC_ADDRESS,
44
+ }
45
+ if decoded.get("amountRaw"):
46
+ return {
47
+ "amountRaw": int(decoded["amountRaw"]),
48
+ "escrow": decoded.get("escrow") or self.ESCROW_ADDRESS,
49
+ "asset": decoded.get("contract") or self.USDC_ADDRESS,
50
+ }
51
+ except Exception:
52
+ pass
53
+
54
+ try:
55
+ body = response.json()
56
+ except Exception:
57
+ body = {}
58
+
59
+ challenge = body.get("challenge") or {}
60
+ if challenge.get("amount"):
61
+ return {
62
+ "amountRaw": int(challenge["amount"]),
63
+ "escrow": challenge.get("escrow") or self.ESCROW_ADDRESS,
64
+ "asset": challenge.get("asset") or self.USDC_ADDRESS,
65
+ }
66
+
67
+ price_hdr = response.headers.get("x-402-price")
68
+ if price_hdr:
69
+ return {
70
+ "amountRaw": int(round(float(price_hdr) * 1_000_000)),
71
+ "escrow": response.headers.get("x-402-contract") or self.ESCROW_ADDRESS,
72
+ "asset": self.USDC_ADDRESS,
73
+ }
74
+
75
+ raise ValueError("Unable to parse HTTP 402 x402 V2 challenge from response.")
76
+
77
+ def _sign_authorization(self, challenge_meta: dict) -> str:
78
+ amount_raw = int(challenge_meta["amountRaw"])
79
+ escrow_addr = challenge_meta.get("escrow") or self.ESCROW_ADDRESS
80
+ usdc_addr = challenge_meta.get("asset") or self.USDC_ADDRESS
81
+
82
+ domain = {
83
+ "name": self.USDC_DOMAIN_NAME,
84
+ "version": self.USDC_VERSION,
85
+ "chainId": self.CHAIN_ID,
86
+ "verifyingContract": usdc_addr,
87
+ }
88
+
89
+ types = {
90
+ "TransferWithAuthorization": [
91
+ {"name": "from", "type": "address"},
92
+ {"name": "to", "type": "address"},
93
+ {"name": "value", "type": "uint256"},
94
+ {"name": "validAfter", "type": "uint256"},
95
+ {"name": "validBefore", "type": "uint256"},
96
+ {"name": "nonce", "type": "bytes32"},
97
+ ]
98
+ }
99
+
100
+ valid_after = 0
101
+ valid_before = int(time.time()) + 3600
102
+ nonce_hex = "0x" + secrets.token_hex(32)
103
+
104
+ message = {
105
+ "from": self.account.address,
106
+ "to": escrow_addr,
107
+ "value": amount_raw,
108
+ "validAfter": valid_after,
109
+ "validBefore": valid_before,
110
+ "nonce": nonce_hex,
111
+ }
112
+
113
+ signable_message = encode_typed_data(domain_data=domain, message_types=types, message_data=message)
114
+ signed = self.account.sign_message(signable_message)
115
+ sig_hex = signed.signature.hex()
116
+ if not sig_hex.startswith("0x"):
117
+ sig_hex = "0x" + sig_hex
118
+
119
+ auth_str = {
120
+ "from": self.account.address,
121
+ "to": escrow_addr,
122
+ "value": str(amount_raw),
123
+ "validAfter": valid_after,
124
+ "validBefore": valid_before,
125
+ "nonce": nonce_hex,
126
+ }
127
+
128
+ payload = {
129
+ "x402Version": 2,
130
+ "scheme": "exact",
131
+ "network": f"eip155:{self.CHAIN_ID}",
132
+ "payload": {
133
+ "signature": sig_hex,
134
+ "authorization": auth_str,
135
+ },
136
+ **auth_str,
137
+ "signature": sig_hex,
138
+ }
139
+
140
+ return base64.b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")
141
+
142
+ def request(self, method: str, url: str, max_retries: int = 2, **kwargs) -> requests.Response:
143
+ response = requests.request(method, url, **kwargs)
144
+
145
+ if response.status_code == 402:
146
+ challenge_meta = self._parse_402_challenge(response)
147
+ sig_payload_b64 = self._sign_authorization(challenge_meta)
148
+
149
+ headers = dict(kwargs.get("headers") or {})
150
+ headers["Payment-Signature"] = sig_payload_b64
151
+ headers["x-payment"] = sig_payload_b64
152
+ kwargs["headers"] = headers
153
+
154
+ for i in range(max_retries):
155
+ retry_resp = requests.request(method, url, **kwargs)
156
+ if retry_resp.status_code != 402:
157
+ return retry_resp
158
+ time.sleep((i + 1) * 1.5)
159
+ return retry_resp
160
+
161
+ return response
162
+
163
+
164
+ M2MCentClient = M2MClient
@@ -0,0 +1,93 @@
1
+ """
2
+ M2MCent LangChain & LangGraph Toolkit (x402 V2 Compliant)
3
+ Enables autonomous LangChain agents to discover, preview, quote, and invoke 1,057+
4
+ monetized deterministic x402 tools on Base Mainnet (chainId: 8453).
5
+
6
+ Smart Contract Escrow: 0x5d47070A36A37DAf417e64FF27a1e5B3F1c84efa
7
+ Institutional Safe Treasury: 0x8aaBAB75bE8825d0f5D514a9a5cBa04B7bF84920
8
+ Vouch Protocol: Grade A (100/100) Certified | Post-Quantum ML-DSA-65 Ready
9
+ """
10
+ import requests
11
+ from typing import List, Dict, Any, Optional
12
+ from .client import M2MClient
13
+
14
+ ESCROW_CONTRACT_ADDRESS = "0x5d47070A36A37DAf417e64FF27a1e5B3F1c84efa"
15
+ TREASURY_SAFE_ADDRESS = "0x8aaBAB75bE8825d0f5D514a9a5cBa04B7bF84920"
16
+
17
+
18
+ class M2MCentToolkit:
19
+ def __init__(
20
+ self,
21
+ gateway_url: str = "https://api.m2mcent.com",
22
+ private_key: Optional[str] = None,
23
+ rpc_url: str = "https://mainnet.base.org",
24
+ ):
25
+ self.gateway_url = gateway_url.rstrip("/")
26
+ self.private_key = private_key
27
+ self.client = M2MClient(private_key, rpc_url=rpc_url, gateway_url=self.gateway_url) if private_key else None
28
+ self._tools_cache: List[Dict[str, Any]] = []
29
+
30
+ def _normalize_slug(self, tool_name_or_slug: str) -> str:
31
+ # Support both underscore function names (autonomous_trading_bot_mcp) and slugs (autonomous-trading-bot-mcp)
32
+ if self._tools_cache:
33
+ for t in self._tools_cache:
34
+ if t.get("id") == tool_name_or_slug or t.get("name") == tool_name_or_slug:
35
+ return t.get("id") or tool_name_or_slug.replace("_", "-")
36
+ return tool_name_or_slug.replace("_", "-")
37
+
38
+ def fetch_tools(self, limit: int = 1062) -> List[Dict[str, Any]]:
39
+ """Fetch available tools from M2MCent Gateway (/api/v1/tools)."""
40
+ resp = requests.get(f"{self.gateway_url}/api/v1/tools", params={"limit": limit}, timeout=10)
41
+ if resp.status_code == 200:
42
+ self._tools_cache = resp.json().get("tools", [])
43
+ return self._tools_cache
44
+ return []
45
+
46
+ def quote_tool(self, tool_name: str) -> Dict[str, Any]:
47
+ """Free pre-flight pricing quote (/:id/quote)."""
48
+ slug = self._normalize_slug(tool_name)
49
+ resp = requests.get(f"{self.gateway_url}/{slug}/quote", timeout=10)
50
+ return resp.json()
51
+
52
+ def preview_tool(self, tool_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
53
+ """Free syntactic schema validation & dry-run preview (/:id/api/preview)."""
54
+ slug = self._normalize_slug(tool_name)
55
+ resp = requests.post(f"{self.gateway_url}/{slug}/api/preview", json=payload, timeout=10)
56
+ return resp.json()
57
+
58
+ def call_tool(
59
+ self,
60
+ tool_name: str,
61
+ payload: Dict[str, Any],
62
+ payment_signature: Optional[str] = None,
63
+ validate_preview: bool = False,
64
+ ) -> Dict[str, Any]:
65
+ """Invoke an M2MCent deterministic tool on /{slug}/api/process with automatic EIP-3009 signing."""
66
+ slug = self._normalize_slug(tool_name)
67
+ if validate_preview:
68
+ prev = self.preview_tool(slug, payload)
69
+ if prev.get("status") == "VALIDATION_FAILED":
70
+ return prev
71
+
72
+ endpoint = f"{self.gateway_url}/{slug}/api/process"
73
+ headers = {"Content-Type": "application/json"}
74
+
75
+ if payment_signature:
76
+ headers["Payment-Signature"] = payment_signature
77
+ headers["x-payment"] = payment_signature
78
+ response = requests.post(endpoint, json=payload, headers=headers, timeout=20)
79
+ return response.json()
80
+
81
+ if self.client:
82
+ response = self.client.request("POST", endpoint, json=payload, headers=headers, timeout=20)
83
+ return response.json()
84
+
85
+ response = requests.post(endpoint, json=payload, headers=headers, timeout=20)
86
+ if response.status_code == 402:
87
+ return {
88
+ "status": "Payment Required",
89
+ "code": 402,
90
+ "challenge": response.headers.get("Payment-Required") or response.headers.get("payment-required"),
91
+ "details": response.json(),
92
+ }
93
+ return response.json()
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: m2mcent-sdk
3
+ Version: 1.0.3
4
+ Summary: Official Python SDK & LangChain Toolkit for M2MCent x402 V2 Protocol on Base Mainnet (1,057 Deterministic Nodes, Vouch Grade A)
5
+ Author: M2MCent Protocol
6
+ Author-email: hello@m2mcent.com
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28.0
10
+ Requires-Dist: eth-account>=0.10.0
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: description
14
+ Dynamic: description-content-type
15
+ Dynamic: requires-dist
16
+ Dynamic: requires-python
17
+ Dynamic: summary
18
+
19
+ # 🐍 m2mcent-sdk (v1.0.3)
20
+
21
+ Official Python SDK (`M2MClient`, `M2MCentToolkit`) for the **M2MCent x402 V2 Protocol** on **Base Mainnet (`eip155:8453`)**.
22
+
23
+ - **1,057 Deterministic Micro-Services** (`$0.010`–`$0.250` USDC, `<250ms` compute SLA)
24
+ - **Vouch Trust Index:** Grade A (`100/100`) · Post-Quantum `ML-DSA-65` (FIPS 204)
25
+ - **Smart Contract Escrow:** `0x5d47070A36A37DAf417e64FF27a1e5B3F1c84efa`
26
+ - **Institutional Safe Treasury:** `0x8aaBAB75bE8825d0f5D514a9a5cBa04B7bF84920`
27
+
28
+ ## Installation
29
+ ```bash
30
+ pip install m2mcent-sdk
31
+ ```
32
+
33
+ ## Quickstart
34
+ ```python
35
+ from m2mcent import M2MCentToolkit
36
+
37
+ tk = M2MCentToolkit(private_key="0xYOUR_BASE_WALLET_KEY")
38
+ preview = tk.preview_tool("autonomous-trading-bot-mcp", {"symbol": "ETH"})
39
+ result = tk.call_tool("autonomous-trading-bot-mcp", {"symbol": "ETH"}, validate_preview=True)
40
+ ```
@@ -0,0 +1,7 @@
1
+ m2mcent/__init__.py,sha256=PXwYzEciB4JNPHqXogEOnT5OaIee6iNHJGkqAS1Vo-8,151
2
+ m2mcent/client.py,sha256=tpTIIWEVX5zcANc0VKmnbCcDVokpZ0DNwZuuYcxBSbo,6092
3
+ m2mcent/langchain_toolkit.py,sha256=rrdzqRjM5eAHKumOX4p2v98xSdQYWL7mfYEIn808l9I,4118
4
+ m2mcent_sdk-1.0.3.dist-info/METADATA,sha256=KDEC45KWCq9H7zRPX4Jda_6W6FJWLJtp6bIn6z9Clkw,1407
5
+ m2mcent_sdk-1.0.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ m2mcent_sdk-1.0.3.dist-info/top_level.txt,sha256=GyzUN_y3yc2Zlr-FBRQ0StpaNTqUVrBDKXol6G-u8pw,8
7
+ m2mcent_sdk-1.0.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ m2mcent