bridgenode 0.1.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.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: bridgenode
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for BridgeNode — automated x402 payment auth for AI agents
5
+ Author-email: BridgeNode <eli.BNx@proton.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://bridgenode.cc
8
+ Project-URL: Source, https://github.com/BridgeNode/x402-client
9
+ Project-URL: Tracker, https://github.com/BridgeNode/x402-client/issues
10
+ Keywords: x402,payment,solana,ed25519,ai,agent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.28.0
23
+ Requires-Dist: pynacl>=1.6.0
24
+ Requires-Dist: base58>=2.1.0
25
+
26
+ # bridgenode
27
+
28
+ Official Python SDK for **BridgeNode** — automated x402 payment auth for AI agents.
29
+
30
+ `pip install bridgenode`
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from bridgenode import X402Client
36
+
37
+ client = X402Client()
38
+ print(f"My address: {client.address}")
39
+
40
+ response = await client.request(messages=[
41
+ {"role": "user", "content": "Hello, who are you?"}
42
+ ])
43
+ print(response["choices"][0]["message"]["content"])
44
+ ```
45
+
46
+ ## How it works
47
+
48
+ 1. Generates an ed25519 keypair (if none provided)
49
+ 2. Signs each request with a UUID nonce
50
+ 3. Sends X-Address / X-Signature / X-Nonce headers
51
+ 4. Handles 402, 401, 409 responses automatically
52
+
53
+ ## Install from source
54
+
55
+ ```bash
56
+ cd bridgenode
57
+ pip install -e .
58
+ ```
59
+
60
+ ## Requirements
61
+
62
+ - Python ≥ 3.10
63
+ - httpx, pynacl, base58
64
+
65
+ ## License
66
+
67
+ MIT — BridgeNode (https://bridgenode.cc)
@@ -0,0 +1,42 @@
1
+ # bridgenode
2
+
3
+ Official Python SDK for **BridgeNode** — automated x402 payment auth for AI agents.
4
+
5
+ `pip install bridgenode`
6
+
7
+ ## Quick start
8
+
9
+ ```python
10
+ from bridgenode import X402Client
11
+
12
+ client = X402Client()
13
+ print(f"My address: {client.address}")
14
+
15
+ response = await client.request(messages=[
16
+ {"role": "user", "content": "Hello, who are you?"}
17
+ ])
18
+ print(response["choices"][0]["message"]["content"])
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ 1. Generates an ed25519 keypair (if none provided)
24
+ 2. Signs each request with a UUID nonce
25
+ 3. Sends X-Address / X-Signature / X-Nonce headers
26
+ 4. Handles 402, 401, 409 responses automatically
27
+
28
+ ## Install from source
29
+
30
+ ```bash
31
+ cd bridgenode
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python ≥ 3.10
38
+ - httpx, pynacl, base58
39
+
40
+ ## License
41
+
42
+ MIT — BridgeNode (https://bridgenode.cc)
@@ -0,0 +1,10 @@
1
+ """x402-client — Python SDK for x402 Payment Required protocol.
2
+
3
+ Automatically handles HTTP 402 responses: detects payment challenges,
4
+ generates ed25519 keys, signs nonces, and retries with proper auth headers.
5
+ """
6
+
7
+ from .client import X402Client
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["X402Client"]
@@ -0,0 +1,61 @@
1
+ """Ed25519 key generation and signing for x402 authentication."""
2
+
3
+ import uuid
4
+ from typing import Tuple
5
+
6
+ import base58
7
+ import nacl.encoding
8
+ import nacl.signing
9
+
10
+
11
+ def generate_keypair() -> Tuple[str, str]:
12
+ """Generate a new ed25519 keypair.
13
+
14
+ Returns:
15
+ Tuple of (private_key_base58, public_key_base58)
16
+ """
17
+ sk = nacl.signing.SigningKey.generate()
18
+ private_key = base58.b58encode(sk.encode() + sk.verify_key.encode()).decode()
19
+ public_key = base58.b58encode(sk.verify_key.encode()).decode()
20
+ return private_key, public_key
21
+
22
+
23
+ def sign_nonce(nonce: str, private_key_base58: str) -> str:
24
+ """Sign a nonce with the given ed25519 private key.
25
+
26
+ Args:
27
+ nonce: The nonce string to sign (usually a UUID).
28
+ private_key_base58: Base58-encoded ed25519 private key.
29
+
30
+ Returns:
31
+ Base58-encoded 64-byte ed25519 signature.
32
+ """
33
+ priv_bytes = base58.b58decode(private_key_base58)
34
+ seed = priv_bytes[:32] if len(priv_bytes) > 32 else priv_bytes
35
+ sk = nacl.signing.SigningKey(seed)
36
+ signed = sk.sign(nonce.encode("utf-8"))
37
+ return base58.b58encode(signed.signature).decode()
38
+
39
+
40
+ def generate_nonce() -> str:
41
+ """Generate a UUID v4 nonce for x402 authentication.
42
+
43
+ Returns:
44
+ UUID string (e.g., "123e4567-e89b-12d3-a456-426614174000")
45
+ """
46
+ return str(uuid.uuid4())
47
+
48
+
49
+ def get_public_key(private_key_base58: str) -> str:
50
+ """Derive public key from private key.
51
+
52
+ Args:
53
+ private_key_base58: Base58-encoded ed25519 private key.
54
+
55
+ Returns:
56
+ Base58-encoded public key.
57
+ """
58
+ priv_bytes = base58.b58decode(private_key_base58)
59
+ seed = priv_bytes[:32] if len(priv_bytes) > 32 else priv_bytes
60
+ sk = nacl.signing.SigningKey(seed)
61
+ return base58.b58encode(sk.verify_key.encode()).decode()
@@ -0,0 +1,245 @@
1
+ """X402Client — automated x402 payment handler for BridgeNode and compatible endpoints.
2
+
3
+ Usage:
4
+ client = X402Client(private_key="...")
5
+ response = client.request("https://bridgenode.cc/v1/inference", {
6
+ "messages": [{"role": "user", "content": "Hello"}]
7
+ })
8
+ print(response["choices"][0]["message"]["content"])
9
+ """
10
+
11
+ import base64
12
+ import json
13
+ import logging
14
+ from typing import Any, Dict, Optional
15
+
16
+ import httpx
17
+
18
+ from .auth import generate_keypair, generate_nonce, get_public_key, sign_nonce
19
+ from .exceptions import X402AuthError, X402Error, X402InsufficientBalance, X402PaymentRequired
20
+
21
+ logger = logging.getLogger("bridgenode")
22
+
23
+ # Default BridgeNode URL
24
+ BRIDGENODE_URL = "https://bridgenode.cc/v1/inference"
25
+ # Default payment address (BridgeNode main wallet)
26
+ BRIDGENODE_PAYMENT_ADDRESS = "BHMDv3ri3LBEZjEzJgDZeUiguVX7LmsCstTXbM3dL8rN"
27
+
28
+
29
+ class X402Client:
30
+ """HTTP client that handles 402 Payment Required automatically.
31
+
32
+ On receiving a 402 response:
33
+ 1. Parses the PAYMENT-REQUIRED header for payment terms
34
+ 2. Generates a UUID nonce and signs it with ed25519
35
+ 3. Retries the request with X-Address, X-Signature, X-Nonce headers
36
+
37
+ If no private_key is provided, a new keypair is generated automatically.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ private_key: Optional[str] = None,
43
+ http_client: Optional[httpx.AsyncClient] = None,
44
+ ):
45
+ """Initialize the x402 client.
46
+
47
+ Args:
48
+ private_key: Base58-encoded ed25519 private key. If None, generates a new one.
49
+ http_client: Optional httpx.AsyncClient for custom configuration.
50
+ A new client is created if not provided.
51
+ """
52
+ if private_key:
53
+ self._private_key = private_key
54
+ self._public_key = get_public_key(private_key)
55
+ else:
56
+ self._private_key, self._public_key = generate_keypair()
57
+ logger.info(
58
+ "Generated new keypair. Address: %s\n"
59
+ "Keep this private key safe for future sessions:\n%s",
60
+ self._public_key, self._private_key,
61
+ )
62
+
63
+ self._http = http_client or httpx.AsyncClient(timeout=120.0)
64
+
65
+ @property
66
+ def address(self) -> str:
67
+ """Solana wallet address (ed25519 public key, base58)."""
68
+ return self._public_key
69
+
70
+ async def request(
71
+ self,
72
+ url: str = BRIDGENODE_URL,
73
+ messages: Optional[list] = None,
74
+ max_tokens: int = 256,
75
+ stream: bool = False,
76
+ **kwargs,
77
+ ) -> Dict[str, Any]:
78
+ """Send a request with automatic x402 payment handling.
79
+
80
+ First tries with auth headers. If server returns 402,
81
+ generates nonce, signs it, and retries.
82
+
83
+ Args:
84
+ url: The x402-protected endpoint URL.
85
+ messages: List of message dicts [{"role": "...", "content": "..."}].
86
+ max_tokens: Maximum output tokens (default 256).
87
+ stream: Enable SSE streaming (default False).
88
+ **kwargs: Additional JSON body fields.
89
+
90
+ Returns:
91
+ Response dict with choices and response_metadata.
92
+
93
+ Raises:
94
+ X402PaymentRequired: Server requires payment and client can't pay.
95
+ X402AuthError: Invalid signature or auth failure.
96
+ X402InsufficientBalance: Agent has insufficient balance.
97
+ X402Error: Other x402 protocol errors.
98
+ """
99
+ body = {
100
+ "messages": messages or [{"role": "user", "content": "Hello"}],
101
+ "max_tokens": max_tokens,
102
+ "stream": stream,
103
+ **kwargs,
104
+ }
105
+
106
+ # Generate nonce and sign for first attempt
107
+ nonce = generate_nonce()
108
+ signature = sign_nonce(nonce, self._private_key)
109
+
110
+ headers = {
111
+ "X-Address": self._public_key,
112
+ "X-Signature": signature,
113
+ "X-Nonce": nonce,
114
+ "Content-Type": "application/json",
115
+ }
116
+
117
+ logger.debug(
118
+ "Sending request to %s (address: %s...)", url, self._public_key[:12]
119
+ )
120
+
121
+ resp = await self._http.post(url, json=body, headers=headers)
122
+
123
+ if resp.status_code == 200:
124
+ return resp.json()
125
+
126
+ if resp.status_code == 402:
127
+ return await self._handle_402(resp, url, body)
128
+
129
+ if resp.status_code == 401:
130
+ raise X402AuthError(f"Invalid signature: {resp.text[:200]}")
131
+
132
+ if resp.status_code == 409:
133
+ # Nonce collision — retry with new nonce
134
+ logger.warning("Nonce conflict, retrying with new nonce")
135
+ new_nonce = generate_nonce()
136
+ new_sig = sign_nonce(new_nonce, self._private_key)
137
+ headers["X-Nonce"] = new_nonce
138
+ headers["X-Signature"] = new_sig
139
+ resp = await self._http.post(url, json=body, headers=headers)
140
+ if resp.status_code == 200:
141
+ return resp.json()
142
+ raise X402Error(f"Nonce conflict on retry: {resp.status_code} {resp.text[:200]}")
143
+
144
+ raise X402Error(f"Unexpected HTTP {resp.status_code}: {resp.text[:500]}")
145
+
146
+ async def _handle_402(
147
+ self,
148
+ resp: httpx.Response,
149
+ url: str,
150
+ body: dict,
151
+ ) -> Dict[str, Any]:
152
+ """Handle a 402 Payment Required response.
153
+
154
+ Parses the PAYMENT-REQUIRED header, extracts payment terms,
155
+ and retries with a fresh nonce and signature.
156
+
157
+ If the 402 is due to insufficient balance, raises X402InsufficientBalance.
158
+ """
159
+ payment_header = resp.headers.get("PAYMENT-REQUIRED", "")
160
+
161
+ if payment_header:
162
+ try:
163
+ payment_req = json.loads(base64.b64decode(payment_header).decode())
164
+ accepts = payment_req.get("accepts", [])
165
+ if accepts:
166
+ terms = accepts[0]
167
+ required_amount = terms.get("amount", "?")
168
+ pay_to = terms.get("payTo", "?")
169
+ network = terms.get("network", "?")
170
+
171
+ logger.info(
172
+ "Payment required: %s %s tokens to %s (network: %s)",
173
+ required_amount, terms.get("asset", "USDC"), pay_to, network,
174
+ )
175
+
176
+ # Check if this is an insufficient balance response
177
+ detail = resp.json() if resp.text else {}
178
+ if isinstance(detail, dict) and detail.get("error") == "Insufficient balance":
179
+ raise X402InsufficientBalance(
180
+ f"Insufficient balance. Required: ${detail.get('required', '?')}, "
181
+ f"Balance: ${detail.get('balance', '?')}, "
182
+ f"Pay to: {detail.get('payTo', pay_to)}"
183
+ )
184
+ except (json.JSONDecodeError, base64.binascii.Error, UnicodeDecodeError) as e:
185
+ logger.warning("Failed to parse PAYMENT-REQUIRED header: %s", e)
186
+
187
+ # Retry with fresh nonce (the previous nonce was never recorded)
188
+ new_nonce = generate_nonce()
189
+ new_sig = sign_nonce(new_nonce, self._private_key)
190
+ headers = {
191
+ "X-Address": self._public_key,
192
+ "X-Signature": new_sig,
193
+ "X-Nonce": new_nonce,
194
+ "Content-Type": "application/json",
195
+ }
196
+
197
+ retry_resp = await self._http.post(url, json=body, headers=headers)
198
+
199
+ if retry_resp.status_code == 200:
200
+ return retry_resp.json()
201
+
202
+ # Still failing — check again for insufficient balance
203
+ if retry_resp.status_code == 402:
204
+ detail = retry_resp.json() if retry_resp.text else {}
205
+ if isinstance(detail, dict) and isinstance(detail.get("detail"), dict):
206
+ err = detail["detail"]
207
+ if err.get("error") == "Insufficient balance":
208
+ raise X402InsufficientBalance(
209
+ f"Insufficient balance. Required: ${err.get('required', '?')}, "
210
+ f"Balance: ${err.get('balance', '?')}, "
211
+ f"Pay to: {err.get('payTo', BRIDGENODE_PAYMENT_ADDRESS)}"
212
+ )
213
+
214
+ raise X402PaymentRequired(
215
+ f"Payment required. "
216
+ f"Deposit USDC to {BRIDGENODE_PAYMENT_ADDRESS} (Solana mainnet). "
217
+ f"Check balance at https://bridgenode.cc/v1/balance/{self._public_key}"
218
+ )
219
+
220
+ if retry_resp.status_code == 401:
221
+ raise X402AuthError(f"Signature rejected on retry: {retry_resp.text[:200]}")
222
+
223
+ raise X402Error(
224
+ f"Payment retry failed (HTTP {retry_resp.status_code}): {retry_resp.text[:500]}"
225
+ )
226
+
227
+ async def check_balance(self, address: Optional[str] = None) -> float:
228
+ """Check off-chain USDC balance on BridgeNode.
229
+
230
+ Args:
231
+ address: Solana wallet address. Uses client address if not provided.
232
+
233
+ Returns:
234
+ Balance in USDC.
235
+ """
236
+ addr = address or self._public_key
237
+ resp = await self._http.get(f"https://bridgenode.cc/v1/balance/{addr}")
238
+ if resp.status_code == 200:
239
+ data = resp.json()
240
+ return float(data.get("balance", 0.0))
241
+ raise X402Error(f"Balance check failed: HTTP {resp.status_code}")
242
+
243
+ async def close(self):
244
+ """Close the underlying HTTP client."""
245
+ await self._http.aclose()
@@ -0,0 +1,20 @@
1
+ """Custom exceptions for x402-client."""
2
+
3
+
4
+ class X402Error(Exception):
5
+ """Base exception for all x402-client errors."""
6
+
7
+
8
+ class X402PaymentRequired(X402Error):
9
+ """Server returned 402 Payment Required and client could not pay.
10
+
11
+ This happens when no private key is configured or payment failed.
12
+ """
13
+
14
+
15
+ class X402AuthError(X402Error):
16
+ """Authentication error - invalid signature or address mismatch."""
17
+
18
+
19
+ class X402InsufficientBalance(X402Error):
20
+ """Server returned 402 with insufficient balance."""
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: bridgenode
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for BridgeNode — automated x402 payment auth for AI agents
5
+ Author-email: BridgeNode <eli.BNx@proton.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://bridgenode.cc
8
+ Project-URL: Source, https://github.com/BridgeNode/x402-client
9
+ Project-URL: Tracker, https://github.com/BridgeNode/x402-client/issues
10
+ Keywords: x402,payment,solana,ed25519,ai,agent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.28.0
23
+ Requires-Dist: pynacl>=1.6.0
24
+ Requires-Dist: base58>=2.1.0
25
+
26
+ # bridgenode
27
+
28
+ Official Python SDK for **BridgeNode** — automated x402 payment auth for AI agents.
29
+
30
+ `pip install bridgenode`
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from bridgenode import X402Client
36
+
37
+ client = X402Client()
38
+ print(f"My address: {client.address}")
39
+
40
+ response = await client.request(messages=[
41
+ {"role": "user", "content": "Hello, who are you?"}
42
+ ])
43
+ print(response["choices"][0]["message"]["content"])
44
+ ```
45
+
46
+ ## How it works
47
+
48
+ 1. Generates an ed25519 keypair (if none provided)
49
+ 2. Signs each request with a UUID nonce
50
+ 3. Sends X-Address / X-Signature / X-Nonce headers
51
+ 4. Handles 402, 401, 409 responses automatically
52
+
53
+ ## Install from source
54
+
55
+ ```bash
56
+ cd bridgenode
57
+ pip install -e .
58
+ ```
59
+
60
+ ## Requirements
61
+
62
+ - Python ≥ 3.10
63
+ - httpx, pynacl, base58
64
+
65
+ ## License
66
+
67
+ MIT — BridgeNode (https://bridgenode.cc)
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ bridgenode/__init__.py
4
+ bridgenode/auth.py
5
+ bridgenode/client.py
6
+ bridgenode/exceptions.py
7
+ bridgenode.egg-info/PKG-INFO
8
+ bridgenode.egg-info/SOURCES.txt
9
+ bridgenode.egg-info/dependency_links.txt
10
+ bridgenode.egg-info/entry_points.txt
11
+ bridgenode.egg-info/requires.txt
12
+ bridgenode.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ x402-demo = bridgenode.__main__:main
@@ -0,0 +1,3 @@
1
+ httpx>=0.28.0
2
+ pynacl>=1.6.0
3
+ base58>=2.1.0
@@ -0,0 +1 @@
1
+ bridgenode
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bridgenode"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for BridgeNode — automated x402 payment auth for AI agents"
9
+ authors = [
10
+ {name = "BridgeNode", email = "eli.BNx@proton.me"},
11
+ ]
12
+ readme = "README.md"
13
+ license = {text = "MIT"}
14
+ requires-python = ">=3.10"
15
+ keywords = ["x402", "payment", "solana", "ed25519", "ai", "agent"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Internet :: WWW/HTTP",
25
+ "Topic :: Security :: Cryptography",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.28.0",
29
+ "pynacl>=1.6.0",
30
+ "base58>=2.1.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://bridgenode.cc"
35
+ Source = "https://github.com/BridgeNode/x402-client"
36
+ Tracker = "https://github.com/BridgeNode/x402-client/issues"
37
+
38
+ [project.scripts]
39
+ x402-demo = "bridgenode.__main__:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+