toll-sdk 0.1.0__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.
toll_sdk/__init__.py ADDED
@@ -0,0 +1,397 @@
1
+ """toll-sdk (import toll_sdk): client for Toll's remote MCP server (https://t0ll.xyz/mcp).
2
+
3
+ Information only, not financial advice.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ import json
9
+ import re
10
+ import socket
11
+ import time
12
+ import urllib.error
13
+ import urllib.request
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, Dict, List, Optional
16
+
17
+ __all__ = [
18
+ "PaymentRequirements",
19
+ "SignPayment",
20
+ "Toll",
21
+ "TollError",
22
+ "TollResult",
23
+ "is_valid_address",
24
+ "is_valid_wallet",
25
+ "parse_paid_url",
26
+ "parse_pending",
27
+ "read_requirements",
28
+ ]
29
+
30
+ _EVM = re.compile(r"0x[0-9a-fA-F]{40}")
31
+ _BASE58 = re.compile(r"[1-9A-HJ-NP-Za-km-z]{32,44}")
32
+ # Only the upsell line counts: a finished LARP answer also lists Toll's own x402 endpoint as a "working part".
33
+ _PAID_URL = re.compile(r"paid (?:LARP )?check of this coin \(x402\):\s*(https://t0ll\.xyz/api/x402/[^\s)\"'<>]+)", re.I)
34
+ _AGAIN = re.compile(r"again in (?:about )?(\d+)\s*(?:s\b|sec)", re.I)
35
+ _PAID_PREFIX = "https://t0ll.xyz/api/x402/"
36
+ _TIMEOUTS = (TimeoutError, socket.timeout)
37
+ _SETTLE_NOTE = (
38
+ " The payment may have settled: do not retry this call with a new signature;"
39
+ " resend the same payment header to get the answer."
40
+ )
41
+
42
+
43
+ class TollError(Exception):
44
+ """Any failure: HTTP, JSON-RPC, timeout, server-side input error, bad input."""
45
+
46
+ def __init__(
47
+ self,
48
+ message: str,
49
+ status: Optional[int] = None,
50
+ code: Optional[int] = None,
51
+ retry_after_s: Optional[int] = None,
52
+ ) -> None:
53
+ super().__init__(message)
54
+ self.status = status
55
+ self.code = code
56
+ self.retry_after_s = retry_after_s
57
+
58
+
59
+ @dataclass
60
+ class TollResult:
61
+ text: str
62
+ paid_url: Optional[str]
63
+ pending: Optional[Dict[str, int]] # {"retry_after_s": n}: part of the answer is still being read, ask again then
64
+
65
+
66
+ @dataclass
67
+ class PaymentRequirements:
68
+ """What a paid URL asks for, read from its 402 answer."""
69
+
70
+ x402_version: int
71
+ """2 when the 402 carried a PAYMENT-REQUIRED header (sent back as PAYMENT-SIGNATURE), else 1 (X-PAYMENT)."""
72
+ payment_required: Dict[str, Any]
73
+ """The decoded 402 object: base64 JSON of the PAYMENT-REQUIRED header (v2) or the JSON body (v1)."""
74
+ accepts: List[Dict[str, Any]]
75
+ """payment_required["accepts"]: scheme, network, asset, amount (v2) or maxAmountRequired (v1), payTo, ..."""
76
+
77
+
78
+ SignPayment = Callable[[PaymentRequirements, str], str]
79
+ """sign(requirements, url) -> the payment header value. pay() calls it at most once per call."""
80
+
81
+
82
+ def _clamp_seconds(value: Any, fallback: int) -> int:
83
+ """Seconds to wait, never below 1 (a 0 or junk value must not become a tight loop)."""
84
+ try:
85
+ n = float(value)
86
+ except (TypeError, ValueError):
87
+ return fallback
88
+ if n != n or n in (float("inf"), float("-inf")):
89
+ return fallback
90
+ return max(1, int(-(-n // 1)))
91
+
92
+
93
+ def is_valid_address(value: object) -> bool:
94
+ """Token address: 0x + 40 hex, or a Solana base58 mint (32-44 chars)."""
95
+ return isinstance(value, str) and bool(_EVM.fullmatch(value) or _BASE58.fullmatch(value))
96
+
97
+
98
+ def is_valid_wallet(value: object) -> bool:
99
+ """Wallet address: 0x + 40 hex."""
100
+ return isinstance(value, str) and bool(_EVM.fullmatch(value))
101
+
102
+
103
+ def parse_paid_url(text: str) -> Optional[str]:
104
+ m = _PAID_URL.search(text)
105
+ return m.group(1).rstrip(".,;:") if m else None
106
+
107
+
108
+ def parse_pending(text: str) -> Optional[Dict[str, int]]:
109
+ m = _AGAIN.search(text)
110
+ return {"retry_after_s": _clamp_seconds(m.group(1), 5)} if m else None
111
+
112
+
113
+ def _check_address(value: object, what: str = "address") -> None:
114
+ if not is_valid_address(value):
115
+ raise TollError(f"Invalid {what}: expected 0x + 40 hex characters or a Solana base58 mint (32-44 characters).")
116
+
117
+
118
+ def _check_wallet(value: object) -> None:
119
+ if not is_valid_wallet(value):
120
+ raise TollError("Invalid wallet: expected 0x + 40 hex characters.")
121
+
122
+
123
+ def _check_chain(chain: Optional[str], allowed: tuple) -> None:
124
+ if chain is not None and chain not in allowed:
125
+ raise TollError(f'Invalid chain "{chain}": use {", ".join(allowed)}.')
126
+
127
+
128
+ def _parse_body(raw: str) -> Any:
129
+ t = raw.strip()
130
+ if t.startswith("{"):
131
+ return json.loads(t)
132
+ for line in t.splitlines():
133
+ if line.startswith("data:"):
134
+ return json.loads(line[5:].strip())
135
+ raise ValueError("no data")
136
+
137
+
138
+ class Toll:
139
+ """Client. `opener` is a urllib OpenerDirector (or anything with .open(request, timeout=...))."""
140
+
141
+ def __init__(
142
+ self,
143
+ base_url: str = "https://t0ll.xyz",
144
+ opener: Any = None,
145
+ timeout: float = 30.0,
146
+ sleep: Callable[[float], None] = time.sleep,
147
+ sign: Optional[SignPayment] = None,
148
+ pay_timeout: float = 300.0,
149
+ ) -> None:
150
+ self.base_url = base_url.rstrip("/")
151
+ self._opener = opener or urllib.request.build_opener()
152
+ self.timeout = timeout
153
+ self.pay_timeout = pay_timeout
154
+ self._sign = sign
155
+ self._sleep = sleep
156
+ self._next_id = 1
157
+
158
+ # -- transport ---------------------------------------------------------
159
+ def _http(self, req: urllib.request.Request, timeout: float, note: str = "") -> tuple:
160
+ """One request, body read inside the timeout. Returns (status, headers, text); HTTP errors are returned too."""
161
+ try:
162
+ with self._opener.open(req, timeout=timeout) as res:
163
+ raw = res.read().decode("utf-8", "replace")
164
+ return int(getattr(res, "status", 200) or 200), getattr(res, "headers", None) or {}, raw
165
+ except urllib.error.HTTPError as e:
166
+ try:
167
+ raw = e.read().decode("utf-8", "replace") if hasattr(e, "read") else ""
168
+ except Exception:
169
+ raw = ""
170
+ return int(e.code), e.headers or {}, raw
171
+ except _TIMEOUTS:
172
+ raise TollError(f"Request to {req.full_url} timed out after {timeout:g} s.{note}") from None
173
+ except urllib.error.URLError as e:
174
+ if isinstance(e.reason, _TIMEOUTS):
175
+ raise TollError(f"Request to {req.full_url} timed out after {timeout:g} s.{note}") from None
176
+ raise TollError(f"Request to {req.full_url} failed: {e}.{note}") from None
177
+ except OSError as e:
178
+ raise TollError(f"Request to {req.full_url} failed: {e}.{note}") from None
179
+
180
+ def _rpc(self, method: str, params: Dict[str, Any]) -> Any:
181
+ payload = json.dumps({"jsonrpc": "2.0", "id": self._next_id, "method": method, "params": params}).encode()
182
+ self._next_id += 1
183
+ req = urllib.request.Request(
184
+ f"{self.base_url}/mcp",
185
+ data=payload,
186
+ method="POST",
187
+ headers={"content-type": "application/json", "accept": "application/json, text/event-stream"},
188
+ )
189
+ status, _headers, raw = self._http(req, self.timeout)
190
+ if not 200 <= status < 300:
191
+ raise TollError(f"Toll answered HTTP {status}" + (f": {raw[:200]}" if raw else ""), status=status)
192
+ try:
193
+ body = _parse_body(raw)
194
+ except ValueError:
195
+ raise TollError("Toll sent an answer that is not JSON-RPC.") from None
196
+ if isinstance(body, dict) and body.get("error"):
197
+ err = body["error"]
198
+ raise TollError(f"Toll JSON-RPC error {err.get('code')}: {err.get('message')}", code=err.get("code"))
199
+ return body.get("result") if isinstance(body, dict) else None
200
+
201
+ # -- tools -------------------------------------------------------------
202
+ def tools(self) -> List[Dict[str, Any]]:
203
+ r = self._rpc("tools/list", {})
204
+ return (r or {}).get("tools", [])
205
+
206
+ def call(self, name: str, args: Optional[Dict[str, Any]] = None) -> TollResult:
207
+ """Call any tool by its server name. isError answers raise TollError."""
208
+ r = self._rpc("tools/call", {"name": name, "arguments": args or {}}) or {}
209
+ content = r.get("content")
210
+ text = "\n".join(str(c.get("text", "")) for c in content if isinstance(c, dict) and c.get("type") == "text") if isinstance(content, list) else ""
211
+ if r.get("isError"):
212
+ p = parse_pending(text)
213
+ raise TollError(text.split("\n\n")[0] or "Toll reported an error.", retry_after_s=p["retry_after_s"] if p else None)
214
+ pending = parse_pending(text) if re.search(r"still reading", text, re.I) else None
215
+ return TollResult(text=text, paid_url=parse_paid_url(text), pending=pending)
216
+
217
+ def check_coin(self, address: str, chain: Optional[str] = None) -> TollResult:
218
+ _check_address(address)
219
+ _check_chain(chain, ("rh", "sol", "bsc"))
220
+ return self.call("check_coin", {"address": address, **({"chain": chain} if chain else {})})
221
+
222
+ def larp_check(self, token: str, chain: Optional[str] = None, wait: bool = False) -> TollResult:
223
+ """LARP check.
224
+
225
+ wait=True asks again while part of the answer is still being read, or while Toll answers that it is
226
+ busy with many new LARP checks (a TollError with retry_after_s): max 3 tries, max 120 s waiting.
227
+ """
228
+ _check_address(token, "token")
229
+ _check_chain(chain, ("rh", "sol"))
230
+ args = {"token": token, **({"chain": chain} if chain else {})}
231
+ if not wait:
232
+ return self.call("larp_check", args)
233
+ waited = 0
234
+ tries = 0
235
+ while True:
236
+ tries += 1
237
+ res: Optional[TollResult] = None
238
+ busy: Optional[TollError] = None
239
+ try:
240
+ res = self.call("larp_check", args)
241
+ except TollError as e:
242
+ if e.retry_after_s is None:
243
+ raise
244
+ busy = e
245
+ if res is not None:
246
+ if not res.pending:
247
+ return res
248
+ s = res.pending["retry_after_s"]
249
+ else:
250
+ assert busy is not None and busy.retry_after_s is not None
251
+ s = busy.retry_after_s
252
+ if tries >= 3 or waited + s > 120:
253
+ if busy is not None:
254
+ raise busy
255
+ assert res is not None
256
+ return res
257
+ self._sleep(s)
258
+ waited += s
259
+
260
+ def dev_record(self, wallet: str, chain: Optional[str] = None) -> TollResult:
261
+ _check_wallet(wallet)
262
+ _check_chain(chain, ("rh", "bsc"))
263
+ return self.call("dev_record", {"wallet": wallet, **({"chain": chain} if chain else {})})
264
+
265
+ def launch_record(self, address: str) -> TollResult:
266
+ _check_wallet(address)
267
+ return self.call("launch_record", {"address": address})
268
+
269
+ def wallet_privacy(self, wallet: str) -> TollResult:
270
+ _check_wallet(wallet)
271
+ return self.call("wallet_privacy", {"wallet": wallet})
272
+
273
+ def fresh_launches(self, limit: Optional[int] = None) -> TollResult:
274
+ if limit is not None and (isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 20):
275
+ raise TollError("Invalid limit: use a whole number from 1 to 20.")
276
+ return self.call("fresh_launches", {} if limit is None else {"limit": limit})
277
+
278
+ def chain_pulse(self) -> TollResult:
279
+ return self.call("chain_pulse", {})
280
+
281
+ # -- paying ------------------------------------------------------------
282
+ def pay(self, paid_url: str, sign: Optional[SignPayment] = None, timeout: Optional[float] = None) -> Any:
283
+ """Pay a paid x402 URL (the paid_url of a result) and return its JSON.
284
+
285
+ 1. GET without payment: 402 with the requirements (v2 PAYMENT-REQUIRED header, else the v1 body).
286
+ 2. Call sign(requirements, url) ONCE for the payment header value.
287
+ 3. GET with it (v2: PAYMENT-SIGNATURE, v1: X-PAYMENT). On 402 {"pending": true} resend EXACTLY the
288
+ same header after Retry-After (at least 1 s), max 5 tries and 300 s of waiting. 200 JSON is the
289
+ answer; any other 402 after paying raises TollError "payment not accepted". It never signs twice.
290
+ The whole call is bounded by `timeout` (default pay_timeout, 300 s).
291
+ """
292
+ if not isinstance(paid_url, str) or not paid_url.startswith(_PAID_PREFIX):
293
+ raise TollError("pay() only takes https://t0ll.xyz/api/x402/... URLs.")
294
+ signer = sign or self._sign
295
+ if signer is None:
296
+ raise TollError("pay() needs a sign function: Toll(sign=...) or pay(url, sign=...). See the README (paying).")
297
+ total = self.pay_timeout if timeout is None else timeout
298
+ deadline = time.monotonic() + total
299
+
300
+ def left() -> float:
301
+ return max(0.001, deadline - time.monotonic())
302
+
303
+ def get(headers: Dict[str, str], note: str = "") -> tuple:
304
+ req = urllib.request.Request(paid_url, method="GET", headers={"accept": "application/json", **headers})
305
+ return self._http(req, left(), note)
306
+
307
+ # 1. Unpaid GET: read what to pay.
308
+ status, headers, raw = get({})
309
+ if 200 <= status < 300:
310
+ return _json_answer(raw, status)
311
+ if status != 402:
312
+ raise _http_error(status, raw)
313
+ requirements = read_requirements(headers, raw)
314
+
315
+ # 2. One signature for this call.
316
+ try:
317
+ header = signer(requirements, paid_url)
318
+ except Exception as e:
319
+ raise TollError(f"sign() failed, nothing was sent: {e}") from None
320
+ if not isinstance(header, str) or not header.strip() or "\r" in header or "\n" in header:
321
+ raise TollError("sign() must return the payment header value as one non-empty line; nothing was sent.")
322
+ name = "PAYMENT-SIGNATURE" if requirements.x402_version == 2 else "X-PAYMENT"
323
+
324
+ # 3. Paid GET, resending the same header while the answer is pending.
325
+ waited = 0
326
+ tries = 0
327
+ while True:
328
+ tries += 1
329
+ if time.monotonic() >= deadline:
330
+ raise TollError(f"pay() timed out after {total:g} s.{_SETTLE_NOTE}")
331
+ status, headers, raw = get({name: header}, _SETTLE_NOTE)
332
+ data = _try_json(raw)
333
+ if status == 402 and isinstance(data, dict) and data.get("pending") is True:
334
+ s = _clamp_seconds(_header(headers, "retry-after"), 5)
335
+ if tries >= 5 or waited + s > 300 or time.monotonic() + s > deadline:
336
+ raise TollError(
337
+ f"The paid answer is still being read after {tries} tries (nothing is charged while it is pending)."
338
+ " Resend the same payment header later; do not sign a new one for this call.",
339
+ status=402,
340
+ retry_after_s=s,
341
+ )
342
+ self._sleep(s)
343
+ waited += s
344
+ continue
345
+ if status == 402:
346
+ why = f" ({data['error']})" if isinstance(data, dict) and isinstance(data.get("error"), str) else ""
347
+ raise TollError(f"payment not accepted: Toll answered 402 to the signed payment{why}. pay() does not sign again.", status=402)
348
+ if not 200 <= status < 300:
349
+ raise _http_error(status, raw, _SETTLE_NOTE)
350
+ return _json_answer(raw, status)
351
+
352
+
353
+ def read_requirements(headers: Any, raw: str) -> PaymentRequirements:
354
+ """Read a 402: v2 from the PAYMENT-REQUIRED header when it decodes, else v1 from the JSON body."""
355
+ h = _header(headers, "payment-required")
356
+ if h:
357
+ try:
358
+ pr = json.loads(base64.b64decode(h.strip()).decode("utf-8"))
359
+ if isinstance(pr, dict) and isinstance(pr.get("accepts"), list) and pr["accepts"]:
360
+ return PaymentRequirements(x402_version=2, payment_required=pr, accepts=pr["accepts"])
361
+ except (ValueError, UnicodeDecodeError):
362
+ pass
363
+ body = _try_json(raw)
364
+ if isinstance(body, dict) and isinstance(body.get("accepts"), list) and body["accepts"]:
365
+ return PaymentRequirements(x402_version=1, payment_required=body, accepts=body["accepts"])
366
+ raise TollError("Toll answered 402 without x402 payment requirements; nothing was signed.", status=402)
367
+
368
+
369
+ def _try_json(raw: str) -> Any:
370
+ try:
371
+ return json.loads(raw) if raw else None
372
+ except ValueError:
373
+ return None
374
+
375
+
376
+ def _json_answer(raw: str, status: int) -> Any:
377
+ data = _try_json(raw)
378
+ if data is None:
379
+ raise TollError("The paid answer was not JSON.", status=status)
380
+ return data
381
+
382
+
383
+ def _http_error(status: int, raw: str, note: str = "") -> TollError:
384
+ return TollError(f"Toll answered HTTP {status}" + (f": {raw[:200]}" if raw else "") + "." + note, status=status)
385
+
386
+
387
+ def _header(headers: Any, name: str) -> Optional[str]:
388
+ if not headers:
389
+ return None
390
+ get = getattr(headers, "get", None)
391
+ if get is not None and hasattr(headers, "get_all"): # http.client.HTTPMessage: case-insensitive
392
+ v = get(name)
393
+ return None if v is None else str(v)
394
+ for k in headers:
395
+ if str(k).lower() == name:
396
+ return str(headers[k])
397
+ return None
toll_sdk/py.typed ADDED
File without changes
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.5
2
+ Name: toll-sdk
3
+ Version: 0.1.0
4
+ Summary: Client for Toll's coin checks (Robinhood Chain, Solana, BSC) over MCP. Standard library only.
5
+ Project-URL: Homepage, https://t0ll.xyz/agents
6
+ Project-URL: Documentation, https://t0ll.xyz/llms.txt
7
+ Project-URL: Repository, https://github.com/t0ll-xyz/toll-plugins
8
+ Author: Toll
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agent,bsc,larp,mcp,robinhood-chain,rug-check,solana,toll,x402
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # toll-sdk
26
+
27
+ Python client for [Toll](https://t0ll.xyz/agents)'s coin checks, read from the chain (Robinhood Chain, Solana, BSC), over Toll's remote MCP server. Who is behind a coin, the dev's record, and whether there is a real product behind it. Standard library only, Python >= 3.9, typed.
28
+
29
+ Information only, not financial advice.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ pip install toll-sdk # import name: toll_sdk
35
+ ```
36
+
37
+ ## Example
38
+
39
+ ```python
40
+ from toll_sdk import Toll
41
+
42
+ toll = Toll()
43
+ check = toll.check_coin("0xff9e86e6b21fd688027981bee4b64c8b6b804ed3", chain="rh")
44
+ print(check.text) # markdown facts from the chain
45
+ larp = toll.larp_check("0xff9e86e6b21fd688027981bee4b64c8b6b804ed3", wait=True)
46
+ print(larp.text) # product found / partly found / not found / not read
47
+ ```
48
+
49
+ Every method returns `TollResult(text, paid_url, pending)`. `pending` (`{"retry_after_s": n}` or None) means part of the answer is still being read: ask again after that many seconds. `paid_url` is set when the free coins are used up (see below).
50
+
51
+ ## API
52
+
53
+ `Toll(base_url="https://t0ll.xyz", opener=None, timeout=30.0, sign=None, pay_timeout=300.0)`
54
+
55
+ - `check_coin(address, chain=None)` - chain is `rh`, `sol` or `bsc`
56
+ - `larp_check(token, chain=None, wait=False)` - `rh` or `sol`; `wait=True` asks again after the seconds the server gives while part of the answer is still being read or Toll is busy with many new LARP checks (max 3 tries, max 120 s)
57
+ - `dev_record(wallet, chain=None)`, `launch_record(address)`, `wallet_privacy(wallet)`, `fresh_launches(limit=None)`, `chain_pulse()`
58
+ - `tools()` lists the server's tools, `call(name, args)` calls any of them
59
+ - `pay(paid_url, sign=None, timeout=None)` - pays a paid URL once and returns its JSON (see below)
60
+ - `is_valid_address(x)`, `is_valid_wallet(x)` - the checks run before anything is sent
61
+ - Failures (HTTP, JSON-RPC, timeouts, server-side input errors) raise `TollError`.
62
+
63
+ ## Free limit and the paid path
64
+
65
+ 10 new coins a day per caller are free (coins already read stay free). Past that, the result carries `paid_url`, the x402 endpoint for that coin (0.05 USDG on Robinhood Chain, charged only for a whole answer).
66
+
67
+ `pay(paid_url)` runs the x402 flow itself and asks your `sign` function for one payment:
68
+
69
+ 1. GET the URL without payment. Toll answers 402 with what to pay: x402 v2 in the `PAYMENT-REQUIRED` header, v1 in the body.
70
+ 2. Call `sign(requirements, url)` **once**. It returns the payment header value.
71
+ 3. GET again with it (`PAYMENT-SIGNATURE` for v2, `X-PAYMENT` for v1, following what the 402 offered). While Toll is still reading the coin it answers 402 `{"pending": true}` (nothing charged): `pay` resends **exactly the same header** after `Retry-After` (at least 1 s), up to 5 tries and 300 s. A 200 is the answer. Any other 402 after paying raises `TollError` "payment not accepted"; `pay` never signs a second payment for one call.
72
+
73
+ The whole call has its own timeout (`pay_timeout`, default 300 s). If it times out or the connection drops after the payment was sent, the error says the payment may have settled: do not retry that call with a new signature; resend the same header.
74
+
75
+ ### The `sign` contract
76
+
77
+ ```python
78
+ from toll_sdk import PaymentRequirements, Toll
79
+
80
+ def sign(req: PaymentRequirements, url: str) -> str:
81
+ # req.x402_version: 2 when the 402 had a PAYMENT-REQUIRED header, else 1
82
+ # req.payment_required: the decoded 402 object; req.accepts: scheme, network "eip155:4663",
83
+ # asset (USDG), amount (v2) / maxAmountRequired (v1), payTo, maxTimeoutSeconds, extra
84
+ # Check the amount and payTo, sign an EIP-3009 USDG transfer authorization with your x402 client,
85
+ # and return the header value for req.x402_version (the base64 payment payload).
86
+ ...
87
+
88
+ toll = Toll(sign=sign)
89
+ r = toll.check_coin(address)
90
+ if r.paid_url:
91
+ print(toll.pay(r.paid_url))
92
+ ```
93
+
94
+ Your key never touches this SDK. Without a `sign`, `pay` raises before any request. The JavaScript SDK's README has a worked `sign` built on `@priors/x402`. Docs: <https://t0ll.xyz/agents>, <https://t0ll.xyz/llms.txt>, <https://t0ll.xyz/agents/x402>.
95
+
96
+ MIT.
@@ -0,0 +1,6 @@
1
+ toll_sdk/__init__.py,sha256=mLa8Z2PiGUKB-8PZbxUKzQa5RUdCUiTXz_VewIfkM3c,16988
2
+ toll_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ toll_sdk-0.1.0.dist-info/METADATA,sha256=sBrCUNWdd3aapku54_k8QG9TQv4YapAFTbOvNsj0evA,5104
4
+ toll_sdk-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ toll_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=fYxg0YwHSQpc90DS9iZXfliQn-nejEdtdHu9448-mnc,1061
6
+ toll_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toll
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.