lordevo-x402 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,9 @@
1
+ node_modules/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .npmrc
9
+ .pypirc
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lordevo
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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: lordevo-x402
3
+ Version: 0.1.0
4
+ Summary: Client for Lordevo's pay-per-call web, DNS, email and domain checks for AI agents: 33 tools from $0.01, paid with x402 (USDC on Base). LangChain and CrewAI tools included.
5
+ Project-URL: Homepage, https://shop.lordevo.com
6
+ Project-URL: Source, https://github.com/forbot26/lordevo-x402
7
+ Project-URL: Issues, https://github.com/forbot26/lordevo-x402/issues
8
+ Project-URL: Documentation, https://shop.lordevo.com/llms.txt
9
+ Author-email: Lordevo <hello@lordevo.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: a2a,ai-agents,base,crewai,dns,langchain,mcp,robots.txt,security,seo,usdc,x402
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27
19
+ Provides-Extra: crewai
20
+ Requires-Dist: crewai>=0.80; extra == 'crewai'
21
+ Provides-Extra: langchain
22
+ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
23
+ Provides-Extra: pay
24
+ Requires-Dist: eth-account>=0.13; extra == 'pay'
25
+ Requires-Dist: x402[evm,httpx,requests]>=2.25; extra == 'pay'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # lordevo-x402 (Python)
29
+
30
+ Python client for the pay-per-call checks at **[shop.lordevo.com](https://shop.lordevo.com)**. It has 33 web, DNS, email and domain tools for AI agents, priced from $0.01 per call and paid with [x402](https://x402.org) (USDC on Base). There's no account or API key: each call is one micro-payment from the agent's wallet. Calls that fail or get refused (4xx/5xx) are never charged. LangChain and CrewAI tools are included.
31
+
32
+ ```bash
33
+ pip install lordevo-x402 # free helpers
34
+ pip install "lordevo-x402[pay]" # paid calls (x402 + eth-account)
35
+ pip install "lordevo-x402[pay,langchain]" # or [pay,crewai]
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ Free calls need no wallet:
41
+
42
+ ```python
43
+ from lordevo_x402 import ask, quote, catalog
44
+
45
+ ask("dns-lookup example.com") # A2A router: the endpoint, price and payment terms
46
+ quote("can-fetch", "https://example.com/blog") # live x402 terms from the HTTP 402, nothing is paid
47
+ len(catalog()["quick_checks"]) # 33
48
+ ```
49
+
50
+ Paid calls need a Base wallet holding a little USDC:
51
+
52
+ ```python
53
+ import os
54
+ from lordevo_x402 import LordevoSync, Lordevo
55
+
56
+ lo = LordevoSync.from_private_key(os.environ["AGENT_WALLET_KEY"], max_price_usd=0.02)
57
+ lo.can_fetch("https://example.com/blog/post") # robots.txt + AI-crawler gate, $0.01
58
+ lo.dns_lookup("example.com", type="MX") # $0.01
59
+ lo.email_validate("jane@gmial.com") # syntax + MX + disposable + typo hint, no SMTP, $0.01
60
+
61
+ # async
62
+ async with Lordevo.from_private_key(os.environ["AGENT_WALLET_KEY"]) as alo:
63
+ await alo.link_check("https://bit.ly/xyz")
64
+ ```
65
+
66
+ - `max_price_usd` (default 0.05): tools priced above it are refused before any request.
67
+ - `cache_seconds`: identical calls within the window are answered locally and not paid again.
68
+ - Errors raise `LordevoError` with `status`, `code` and `charged`.
69
+ - You can also pass your own paying client: `Lordevo(x402HttpxClient(...))` or `LordevoSync(x402_requests(...))`.
70
+
71
+ ## LangChain and CrewAI
72
+
73
+ ```python
74
+ from lordevo_x402.langchain import langchain_tools
75
+ tools = langchain_tools(sync_client=lo, only=["can-fetch", "link-check", "dns-lookup"])
76
+
77
+ from lordevo_x402.crewai import crewai_tools
78
+ agent = Agent(role="Researcher", goal="...", backstory="...", tools=crewai_tools(lo, only=["can-fetch", "link-check"]))
79
+ ```
80
+
81
+ ## Tools
82
+
83
+ These are the live prices from [catalog.json](https://shop.lordevo.com/catalog.json). Call any of them with `lo.call(slug, value, **options)` or as a method (`lo.dns_lookup`).
84
+
85
+ | Tool | Input | Price | What it does |
86
+ |---|---|---|---|
87
+ | `site-check` | `url` | $0.03 | Site check: trust + AI readiness + security headers (recommended start) |
88
+ | `agent-readiness` | `url` | $0.03 | Agent readiness / AI visibility score (GEO) |
89
+ | `url-trust` | `url` | $0.01 | URL trust check before you click or pay (safe / caution / risky) |
90
+ | `can-fetch` | `url` | $0.01 | Can I fetch this URL? robots.txt + AI-crawler permission gate |
91
+ | `link-check` | `url` | $0.01 | Link check: is it alive, where does it really go (redirects resolved) |
92
+ | `dns-lookup` | `host` (+ type) | $0.01 | DNS lookup (A, AAAA, CNAME, MX, TXT, NS, SOA, CAA) |
93
+ | `email-validate` | `email` | $0.01 | Email address validation (syntax + domain + MX, no SMTP probing) |
94
+ | `ssl-cert` | `host` | $0.01 | SSL/TLS certificate check (expiry, issuer, SANs, chain) |
95
+ | `domain-rdap` | `domain` | $0.01 | Domain age, registrar and expiry (RDAP) |
96
+ | `ip-info` | `ip` | $0.01 | IP address owner and network (RDAP + reverse DNS) |
97
+ | `links-batch` | `urls` | $0.02 | Batch link check (up to 10 URLs per call) |
98
+ | `broken-links` | `url` | $0.02 | Broken links on a page (up to 40 links checked) |
99
+ | `sitemap-urls` | `url` | $0.01 | Sitemap URL extractor (robots.txt + sitemap index) |
100
+ | `link-preview` | `url` | $0.01 | Link preview / unfurl (title, description, image, favicon, logo) |
101
+ | `readability` | `url` | $0.01 | Readability and word count (Flesch, reading time, top terms) |
102
+ | `structured-data` | `url` | $0.01 | Structured data extractor (JSON-LD, microdata, RDFa) |
103
+ | `canonical-check` | `url` | $0.01 | Canonical, hreflang and indexability check |
104
+ | `cors-check` | `url` (+ origin, method) | $0.01 | CORS check (preflight + allowed origins) |
105
+ | `http-ping` | `url` | $0.01 | HTTP ping: up or down, status and timings (DNS, TCP, TLS, TTFB) |
106
+ | `domain-lookalike` | `host` | $0.01 | Lookalike / homograph domain check (punycode, mixed scripts, brand typos) |
107
+ | `myr-rates` | `currency` | $0.01 | Malaysian ringgit exchange rates + OPR (Bank Negara Malaysia data) |
108
+ | `verdict-card` | `url` | $0.02 | Shareable site verdict card (grade + one-liner, SVG) |
109
+ | `page-markdown` | `url` | $0.01 | Page to Markdown (main content) |
110
+ | `page-meta` | `url` | $0.01 | Page metadata + Markdown |
111
+ | `email-auth` | `domain` | $0.01 | Email authentication check (SPF, DKIM, DMARC) |
112
+ | `security-headers` | `url` | $0.01 | Security headers + TLS quick grade |
113
+ | `tech-stack` | `url` | $0.02 | Tech stack detection |
114
+ | `seo-page` | `url` | $0.02 | On-page SEO check (one page) |
115
+ | `site-snapshot` | `url` | $0.03 | Site snapshot (SEO + security + tech + email) |
116
+ | `page-quality` | `url` | $0.01 | Page quality verdict (AI) |
117
+ | `site-verdict` | `url` | $0.02 | Is this a legit business site? (AI trust verdict) |
118
+ | `seo-audit-lite` | `url` | $0.03 | SEO Audit LITE (top 10 issues, one page) |
119
+ | `security-scan-lite` | `url` | $0.03 | Website Security Scan LITE (passive summary) |
120
+ ## Other ways in
121
+
122
+ | Interface | Endpoint |
123
+ |---|---|
124
+ | MCP (streamable HTTP, x402 MCP transport) | `https://shop.lordevo.com/mcp` |
125
+ | A2A JSON-RPC (`message/send`, a free router) | `https://shop.lordevo.com/a2a` |
126
+ | x402 manifest | `https://shop.lordevo.com/.well-known/x402.json` |
127
+ | OpenAPI 3.1 | `https://shop.lordevo.com/openapi.json` |
128
+
129
+ Only check sites you are allowed to check. All checks are passive and read-only.
130
+
131
+ JavaScript package: [`lordevo-x402` on npm](https://www.npmjs.com/package/lordevo-x402). Source: [github.com/forbot26/lordevo-x402](https://github.com/forbot26/lordevo-x402). MIT licence.
@@ -0,0 +1,104 @@
1
+ # lordevo-x402 (Python)
2
+
3
+ Python client for the pay-per-call checks at **[shop.lordevo.com](https://shop.lordevo.com)**. It has 33 web, DNS, email and domain tools for AI agents, priced from $0.01 per call and paid with [x402](https://x402.org) (USDC on Base). There's no account or API key: each call is one micro-payment from the agent's wallet. Calls that fail or get refused (4xx/5xx) are never charged. LangChain and CrewAI tools are included.
4
+
5
+ ```bash
6
+ pip install lordevo-x402 # free helpers
7
+ pip install "lordevo-x402[pay]" # paid calls (x402 + eth-account)
8
+ pip install "lordevo-x402[pay,langchain]" # or [pay,crewai]
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ Free calls need no wallet:
14
+
15
+ ```python
16
+ from lordevo_x402 import ask, quote, catalog
17
+
18
+ ask("dns-lookup example.com") # A2A router: the endpoint, price and payment terms
19
+ quote("can-fetch", "https://example.com/blog") # live x402 terms from the HTTP 402, nothing is paid
20
+ len(catalog()["quick_checks"]) # 33
21
+ ```
22
+
23
+ Paid calls need a Base wallet holding a little USDC:
24
+
25
+ ```python
26
+ import os
27
+ from lordevo_x402 import LordevoSync, Lordevo
28
+
29
+ lo = LordevoSync.from_private_key(os.environ["AGENT_WALLET_KEY"], max_price_usd=0.02)
30
+ lo.can_fetch("https://example.com/blog/post") # robots.txt + AI-crawler gate, $0.01
31
+ lo.dns_lookup("example.com", type="MX") # $0.01
32
+ lo.email_validate("jane@gmial.com") # syntax + MX + disposable + typo hint, no SMTP, $0.01
33
+
34
+ # async
35
+ async with Lordevo.from_private_key(os.environ["AGENT_WALLET_KEY"]) as alo:
36
+ await alo.link_check("https://bit.ly/xyz")
37
+ ```
38
+
39
+ - `max_price_usd` (default 0.05): tools priced above it are refused before any request.
40
+ - `cache_seconds`: identical calls within the window are answered locally and not paid again.
41
+ - Errors raise `LordevoError` with `status`, `code` and `charged`.
42
+ - You can also pass your own paying client: `Lordevo(x402HttpxClient(...))` or `LordevoSync(x402_requests(...))`.
43
+
44
+ ## LangChain and CrewAI
45
+
46
+ ```python
47
+ from lordevo_x402.langchain import langchain_tools
48
+ tools = langchain_tools(sync_client=lo, only=["can-fetch", "link-check", "dns-lookup"])
49
+
50
+ from lordevo_x402.crewai import crewai_tools
51
+ agent = Agent(role="Researcher", goal="...", backstory="...", tools=crewai_tools(lo, only=["can-fetch", "link-check"]))
52
+ ```
53
+
54
+ ## Tools
55
+
56
+ These are the live prices from [catalog.json](https://shop.lordevo.com/catalog.json). Call any of them with `lo.call(slug, value, **options)` or as a method (`lo.dns_lookup`).
57
+
58
+ | Tool | Input | Price | What it does |
59
+ |---|---|---|---|
60
+ | `site-check` | `url` | $0.03 | Site check: trust + AI readiness + security headers (recommended start) |
61
+ | `agent-readiness` | `url` | $0.03 | Agent readiness / AI visibility score (GEO) |
62
+ | `url-trust` | `url` | $0.01 | URL trust check before you click or pay (safe / caution / risky) |
63
+ | `can-fetch` | `url` | $0.01 | Can I fetch this URL? robots.txt + AI-crawler permission gate |
64
+ | `link-check` | `url` | $0.01 | Link check: is it alive, where does it really go (redirects resolved) |
65
+ | `dns-lookup` | `host` (+ type) | $0.01 | DNS lookup (A, AAAA, CNAME, MX, TXT, NS, SOA, CAA) |
66
+ | `email-validate` | `email` | $0.01 | Email address validation (syntax + domain + MX, no SMTP probing) |
67
+ | `ssl-cert` | `host` | $0.01 | SSL/TLS certificate check (expiry, issuer, SANs, chain) |
68
+ | `domain-rdap` | `domain` | $0.01 | Domain age, registrar and expiry (RDAP) |
69
+ | `ip-info` | `ip` | $0.01 | IP address owner and network (RDAP + reverse DNS) |
70
+ | `links-batch` | `urls` | $0.02 | Batch link check (up to 10 URLs per call) |
71
+ | `broken-links` | `url` | $0.02 | Broken links on a page (up to 40 links checked) |
72
+ | `sitemap-urls` | `url` | $0.01 | Sitemap URL extractor (robots.txt + sitemap index) |
73
+ | `link-preview` | `url` | $0.01 | Link preview / unfurl (title, description, image, favicon, logo) |
74
+ | `readability` | `url` | $0.01 | Readability and word count (Flesch, reading time, top terms) |
75
+ | `structured-data` | `url` | $0.01 | Structured data extractor (JSON-LD, microdata, RDFa) |
76
+ | `canonical-check` | `url` | $0.01 | Canonical, hreflang and indexability check |
77
+ | `cors-check` | `url` (+ origin, method) | $0.01 | CORS check (preflight + allowed origins) |
78
+ | `http-ping` | `url` | $0.01 | HTTP ping: up or down, status and timings (DNS, TCP, TLS, TTFB) |
79
+ | `domain-lookalike` | `host` | $0.01 | Lookalike / homograph domain check (punycode, mixed scripts, brand typos) |
80
+ | `myr-rates` | `currency` | $0.01 | Malaysian ringgit exchange rates + OPR (Bank Negara Malaysia data) |
81
+ | `verdict-card` | `url` | $0.02 | Shareable site verdict card (grade + one-liner, SVG) |
82
+ | `page-markdown` | `url` | $0.01 | Page to Markdown (main content) |
83
+ | `page-meta` | `url` | $0.01 | Page metadata + Markdown |
84
+ | `email-auth` | `domain` | $0.01 | Email authentication check (SPF, DKIM, DMARC) |
85
+ | `security-headers` | `url` | $0.01 | Security headers + TLS quick grade |
86
+ | `tech-stack` | `url` | $0.02 | Tech stack detection |
87
+ | `seo-page` | `url` | $0.02 | On-page SEO check (one page) |
88
+ | `site-snapshot` | `url` | $0.03 | Site snapshot (SEO + security + tech + email) |
89
+ | `page-quality` | `url` | $0.01 | Page quality verdict (AI) |
90
+ | `site-verdict` | `url` | $0.02 | Is this a legit business site? (AI trust verdict) |
91
+ | `seo-audit-lite` | `url` | $0.03 | SEO Audit LITE (top 10 issues, one page) |
92
+ | `security-scan-lite` | `url` | $0.03 | Website Security Scan LITE (passive summary) |
93
+ ## Other ways in
94
+
95
+ | Interface | Endpoint |
96
+ |---|---|
97
+ | MCP (streamable HTTP, x402 MCP transport) | `https://shop.lordevo.com/mcp` |
98
+ | A2A JSON-RPC (`message/send`, a free router) | `https://shop.lordevo.com/a2a` |
99
+ | x402 manifest | `https://shop.lordevo.com/.well-known/x402.json` |
100
+ | OpenAPI 3.1 | `https://shop.lordevo.com/openapi.json` |
101
+
102
+ Only check sites you are allowed to check. All checks are passive and read-only.
103
+
104
+ JavaScript package: [`lordevo-x402` on npm](https://www.npmjs.com/package/lordevo-x402). Source: [github.com/forbot26/lordevo-x402](https://github.com/forbot26/lordevo-x402). MIT licence.
@@ -0,0 +1,203 @@
1
+ """lordevo-x402: client for the pay-per-call checks at https://shop.lordevo.com (x402, USDC on Base).
2
+
3
+ Free (no wallet): catalog(), quote(slug, value), ask(text)
4
+ Paid, async: async with Lordevo.from_private_key(key) as lo: await lo.dns_lookup("example.com", type="MX")
5
+ Paid, sync: lo = LordevoSync.from_private_key(key); lo.can_fetch("https://example.com/page")
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+ import time
12
+ import uuid
13
+ from importlib import resources
14
+ from typing import Any
15
+ from urllib.parse import urlencode
16
+
17
+ import httpx
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = ["BASE_URL", "TOOLS", "Lordevo", "LordevoSync", "LordevoError", "tool_url", "catalog", "quote", "ask"]
21
+
22
+ BASE_URL = "https://shop.lordevo.com"
23
+ TOOLS: dict[str, dict] = {t["slug"]: t for t in json.loads(resources.files(__package__).joinpath("tools.json").read_text())["tools"]}
24
+
25
+
26
+ class LordevoError(Exception):
27
+ def __init__(self, status: int, body: Any):
28
+ msg = body.get("message") if isinstance(body, dict) and body.get("message") else f"HTTP {status}"
29
+ super().__init__(msg)
30
+ self.status, self.body = status, body
31
+ self.code = body.get("error") if isinstance(body, dict) else None
32
+ self.charged = body.get("charged") if isinstance(body, dict) else None
33
+
34
+
35
+ def _query(slug: str, value: str | None, params: dict) -> dict:
36
+ t = TOOLS.get(slug)
37
+ if not t:
38
+ raise ValueError(f"Unknown Lordevo tool: {slug}")
39
+ if value is not None:
40
+ params = {**params, t["param"]: value}
41
+ if not params.get(t["param"]):
42
+ raise ValueError(f'{slug} needs "{t["param"]}" ({t.get("param_description", "")})')
43
+ return {t["param"]: str(params[t["param"]]), **{k: str(v) for k, v in params.items() if k in (t.get("options") or {}) and v is not None}}
44
+
45
+
46
+ def tool_url(slug: str, value: str | None = None, base_url: str = BASE_URL, **params: Any) -> str:
47
+ """Endpoint URL for one call (raises before any request on unknown tool or missing input)."""
48
+ return f"{base_url}/api/quick/{slug}?{urlencode(_query(slug, value, params))}"
49
+
50
+
51
+ # ---------- free helpers (no wallet) ----------
52
+ def catalog(base_url: str = BASE_URL, client: httpx.Client | None = None) -> dict:
53
+ c = client or httpx.Client(timeout=30)
54
+ try:
55
+ r = c.get(f"{base_url}/catalog.json", headers={"accept": "application/json"})
56
+ if r.status_code >= 400:
57
+ raise LordevoError(r.status_code, None)
58
+ return r.json()
59
+ finally:
60
+ if client is None:
61
+ c.close()
62
+
63
+
64
+ def quote(slug: str, value: str | None = None, base_url: str = BASE_URL, client: httpx.Client | None = None, **params: Any) -> dict:
65
+ """Live x402 terms for one call, read from the HTTP 402. Nothing is paid."""
66
+ url = tool_url(slug, value, base_url, **params)
67
+ c = client or httpx.Client(timeout=30)
68
+ try:
69
+ r = c.get(url, headers={"accept": "application/json"})
70
+ finally:
71
+ if client is None:
72
+ c.close()
73
+ try:
74
+ body = r.json()
75
+ except ValueError:
76
+ body = None
77
+ if r.status_code != 402:
78
+ raise LordevoError(r.status_code, body)
79
+ h = r.headers.get("payment-required")
80
+ pr = json.loads(base64.b64decode(h)) if h else body
81
+ a = (pr or {}).get("accepts", [{}])[0]
82
+ amount = str(a.get("amount") or a.get("maxAmountRequired"))
83
+ return {"tool": slug, "url": url, "price_usd": int(amount) / 1e6, "amount": amount, "asset": a.get("asset"), "network": a.get("network"), "payTo": a.get("payTo"), "scheme": a.get("scheme"), "x402": pr}
84
+
85
+
86
+ def ask(text: str, base_url: str = BASE_URL, client: httpx.Client | None = None) -> dict:
87
+ """Ask the shop's free A2A router which check fits (e.g. "dns-lookup example.com")."""
88
+ msg = {"kind": "message", "role": "user", "messageId": str(uuid.uuid4()), "parts": [{"kind": "text", "text": str(text)}]}
89
+ c = client or httpx.Client(timeout=30)
90
+ try:
91
+ r = c.post(f"{base_url}/a2a", json={"jsonrpc": "2.0", "id": 1, "method": "message/send", "params": {"message": msg}})
92
+ finally:
93
+ if client is None:
94
+ c.close()
95
+ j = r.json()
96
+ if r.status_code >= 400 or "error" in j:
97
+ raise LordevoError(r.status_code, j.get("error"))
98
+ parts = j["result"]["parts"]
99
+ out = {"text": next((p["text"] for p in parts if p.get("kind") == "text"), "")}
100
+ out.update(next((p["data"] for p in parts if p.get("kind") == "data"), {}))
101
+ return out
102
+
103
+
104
+ # ---------- paid clients ----------
105
+ class _Base:
106
+ def __init__(self, client: Any, base_url: str = BASE_URL, max_price_usd: float = 0.05, cache_seconds: float = 0):
107
+ self.client, self.base_url, self.max_price_usd, self.cache_seconds = client, base_url, max_price_usd, cache_seconds
108
+ self._memo: dict[str, tuple[float, Any]] = {}
109
+ self.tools = TOOLS
110
+
111
+ def _prep(self, slug: str, value: str | None, params: dict) -> tuple[str, dict, str]:
112
+ t = TOOLS.get(slug)
113
+ if not t:
114
+ raise ValueError(f"Unknown Lordevo tool: {slug}")
115
+ if t["price_usd"] > self.max_price_usd:
116
+ raise ValueError(f"{slug} costs ${t['price_usd']}, above max_price_usd ${self.max_price_usd}")
117
+ q = _query(slug, value, params)
118
+ return f"{self.base_url}/api/quick/{slug}", q, f"{slug}?{sorted(q.items())}"
119
+
120
+ def _cached(self, key: str):
121
+ if self.cache_seconds and key in self._memo and time.time() - self._memo[key][0] < self.cache_seconds:
122
+ return True, self._memo[key][1]
123
+ return False, None
124
+
125
+ def _done(self, key: str, status: int, body: Any):
126
+ if status >= 400:
127
+ raise LordevoError(status, body)
128
+ if self.cache_seconds:
129
+ self._memo[key] = (time.time(), body)
130
+ if len(self._memo) > 1000:
131
+ self._memo.pop(next(iter(self._memo)))
132
+ return body
133
+
134
+ def __getattr__(self, name: str):
135
+ slug = name.replace("_", "-")
136
+ if slug in TOOLS:
137
+ return lambda value=None, **p: self.call(slug, value, **p)
138
+ raise AttributeError(name)
139
+
140
+
141
+ def _json(r):
142
+ try:
143
+ return r.json()
144
+ except ValueError:
145
+ return None
146
+
147
+
148
+ class Lordevo(_Base):
149
+ """Async client. Pass any httpx.AsyncClient that pays x402 (e.g. x402.http.clients.x402HttpxClient)."""
150
+
151
+ @classmethod
152
+ def from_private_key(cls, private_key: str, **kw) -> "Lordevo":
153
+ from eth_account import Account
154
+ from x402 import x402Client
155
+ from x402.http.clients import x402HttpxClient
156
+ from x402.mechanisms.evm.exact import register_exact_evm_client
157
+
158
+ x = x402Client()
159
+ register_exact_evm_client(x, Account.from_key(private_key), networks="eip155:8453")
160
+ return cls(x402HttpxClient(x, timeout=60), **kw)
161
+
162
+ async def __aenter__(self):
163
+ return self
164
+
165
+ async def __aexit__(self, *exc):
166
+ await self.client.aclose()
167
+
168
+ async def call(self, slug: str, value: str | None = None, **params: Any) -> dict:
169
+ url, q, key = self._prep(slug, value, params)
170
+ hit, out = self._cached(key)
171
+ if hit:
172
+ return out
173
+ r = await self.client.get(url, params=q, headers={"accept": "application/json"})
174
+ return self._done(key, r.status_code, _json(r))
175
+
176
+
177
+ class LordevoSync(_Base):
178
+ """Sync client. Pass a paying requests.Session (x402.http.clients.x402_requests) or httpx.Client."""
179
+
180
+ @classmethod
181
+ def from_private_key(cls, private_key: str, **kw) -> "LordevoSync":
182
+ from eth_account import Account
183
+ from x402 import x402ClientSync
184
+ from x402.http.clients import x402_requests
185
+ from x402.mechanisms.evm.exact import register_exact_evm_client
186
+
187
+ x = x402ClientSync()
188
+ register_exact_evm_client(x, Account.from_key(private_key), networks="eip155:8453")
189
+ return cls(x402_requests(x), **kw)
190
+
191
+ def __enter__(self):
192
+ return self
193
+
194
+ def __exit__(self, *exc):
195
+ self.client.close()
196
+
197
+ def call(self, slug: str, value: str | None = None, **params: Any) -> dict:
198
+ url, q, key = self._prep(slug, value, params)
199
+ hit, out = self._cached(key)
200
+ if hit:
201
+ return out
202
+ r = self.client.get(url, params=q, headers={"accept": "application/json"})
203
+ return self._done(key, r.status_code, _json(r))
@@ -0,0 +1,30 @@
1
+ """CrewAI tools, one per Lordevo check: Agent(..., tools=crewai_tools(LordevoSync.from_private_key(key), only=[...]))"""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+
6
+ from . import TOOLS
7
+
8
+
9
+ def crewai_tools(client: Any, only: Optional[list] = None):
10
+ """client: a LordevoSync (CrewAI tools run synchronously)."""
11
+ from crewai.tools import BaseTool
12
+ from pydantic import BaseModel, Field, create_model
13
+
14
+ def make(slug: str, t: dict):
15
+ fields = {t["param"]: (str, Field(..., description=t.get("param_description", t["param"])))}
16
+ for k, o in (t.get("options") or {}).items():
17
+ fields[k] = (Optional[str], Field(None, description=o.get("description", k)))
18
+ Args = create_model(f"{t['mcp_tool']}_args", **fields)
19
+
20
+ class LordevoTool(BaseTool):
21
+ name: str = t["mcp_tool"]
22
+ description: str = f"{t['title']}. Costs ${t['price_usd']:.2f} per call (x402, USDC on Base)."
23
+ args_schema: type[BaseModel] = Args
24
+
25
+ def _run(self, **kw) -> str:
26
+ return json.dumps(client.call(slug, **{k: v for k, v in kw.items() if v is not None}))
27
+
28
+ return LordevoTool()
29
+
30
+ return [make(s, t) for s, t in TOOLS.items() if not only or s in only]
@@ -0,0 +1,38 @@
1
+ """LangChain tools, one per Lordevo check: tools = langchain_tools(sync_client=LordevoSync..., async_client=Lordevo..., only=[...])"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ from . import TOOLS
8
+
9
+
10
+ def _schema(t: dict) -> dict:
11
+ props = {t["param"]: {"type": "string", "description": t.get("param_description", t["param"])}}
12
+ for k, o in (t.get("options") or {}).items():
13
+ props[k] = {"type": "string", "description": o.get("description", k), **({"enum": o["enum"]} if "enum" in o else {})}
14
+ return {"type": "object", "properties": props, "required": [t["param"]]}
15
+
16
+
17
+ def langchain_tools(sync_client: Any = None, async_client: Any = None, only: list[str] | None = None):
18
+ """sync_client: a LordevoSync; async_client: a Lordevo. Give at least one."""
19
+ from langchain_core.tools import StructuredTool
20
+
21
+ if sync_client is None and async_client is None:
22
+ raise ValueError("give sync_client (LordevoSync) and/or async_client (Lordevo)")
23
+
24
+ def make(slug: str, t: dict):
25
+ def run(**kw):
26
+ if sync_client is None:
27
+ raise RuntimeError("this tool was built with an async client only; use ainvoke")
28
+ return json.dumps(sync_client.call(slug, **kw))
29
+
30
+ async def arun(**kw):
31
+ if async_client is None:
32
+ return run(**kw)
33
+ return json.dumps(await async_client.call(slug, **kw))
34
+
35
+ return StructuredTool.from_function(func=run, coroutine=arun, name=t["mcp_tool"], args_schema=_schema(t),
36
+ description=f"{t['title']}. Costs ${t['price_usd']:.2f} per call (x402, USDC on Base); failed calls are not charged.")
37
+
38
+ return [make(s, t) for s, t in TOOLS.items() if not only or s in only]
@@ -0,0 +1,402 @@
1
+ {
2
+ "source": "https://shop.lordevo.com/catalog.json",
3
+ "generated": "2026-10-27",
4
+ "tools": [
5
+ {
6
+ "slug": "site-check",
7
+ "mcp_tool": "site_check",
8
+ "param": "url",
9
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
10
+ "options": {},
11
+ "price_usd": 0.03,
12
+ "title": "Site check: trust + AI readiness + security headers (recommended start)",
13
+ "ai": false,
14
+ "category": "website"
15
+ },
16
+ {
17
+ "slug": "agent-readiness",
18
+ "mcp_tool": "agent_readiness",
19
+ "param": "url",
20
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
21
+ "options": {},
22
+ "price_usd": 0.03,
23
+ "title": "Agent readiness / AI visibility score (GEO)",
24
+ "ai": false,
25
+ "category": "seo"
26
+ },
27
+ {
28
+ "slug": "url-trust",
29
+ "mcp_tool": "url_trust",
30
+ "param": "url",
31
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
32
+ "options": {},
33
+ "price_usd": 0.01,
34
+ "title": "URL trust check before you click or pay (safe / caution / risky)",
35
+ "ai": false,
36
+ "category": "security"
37
+ },
38
+ {
39
+ "slug": "can-fetch",
40
+ "mcp_tool": "can_fetch",
41
+ "param": "url",
42
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
43
+ "options": {},
44
+ "price_usd": 0.01,
45
+ "title": "Can I fetch this URL? robots.txt + AI-crawler permission gate",
46
+ "ai": false,
47
+ "category": "web-scraping"
48
+ },
49
+ {
50
+ "slug": "link-check",
51
+ "mcp_tool": "link_check",
52
+ "param": "url",
53
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
54
+ "options": {},
55
+ "price_usd": 0.01,
56
+ "title": "Link check: is it alive, where does it really go (redirects resolved)",
57
+ "ai": false,
58
+ "category": "web"
59
+ },
60
+ {
61
+ "slug": "dns-lookup",
62
+ "mcp_tool": "dns_lookup",
63
+ "param": "host",
64
+ "param_description": "Public host name, e.g. example.com or www.example.com (a URL also works)",
65
+ "options": {
66
+ "type": {
67
+ "enum": [
68
+ "ALL",
69
+ "A",
70
+ "AAAA",
71
+ "CNAME",
72
+ "MX",
73
+ "TXT",
74
+ "NS",
75
+ "SOA",
76
+ "CAA"
77
+ ],
78
+ "default": "ALL",
79
+ "description": "Record type (default ALL = A, AAAA, CNAME, MX, TXT, NS, SOA, CAA)"
80
+ }
81
+ },
82
+ "price_usd": 0.01,
83
+ "title": "DNS lookup (A, AAAA, CNAME, MX, TXT, NS, SOA, CAA)",
84
+ "ai": false,
85
+ "category": "dns"
86
+ },
87
+ {
88
+ "slug": "email-validate",
89
+ "mcp_tool": "email_validate",
90
+ "param": "email",
91
+ "param_description": "Email address, e.g. jane@example.com",
92
+ "options": {},
93
+ "price_usd": 0.01,
94
+ "title": "Email address validation (syntax + domain + MX, no SMTP probing)",
95
+ "ai": false,
96
+ "category": "email"
97
+ },
98
+ {
99
+ "slug": "ssl-cert",
100
+ "mcp_tool": "ssl_cert",
101
+ "param": "host",
102
+ "param_description": "Public host name, e.g. example.com or www.example.com (a URL also works)",
103
+ "options": {},
104
+ "price_usd": 0.01,
105
+ "title": "SSL/TLS certificate check (expiry, issuer, SANs, chain)",
106
+ "ai": false,
107
+ "category": "security"
108
+ },
109
+ {
110
+ "slug": "domain-rdap",
111
+ "mcp_tool": "domain_rdap",
112
+ "param": "domain",
113
+ "param_description": "Domain name, e.g. example.com (a URL or email address also works)",
114
+ "options": {},
115
+ "price_usd": 0.01,
116
+ "title": "Domain age, registrar and expiry (RDAP)",
117
+ "ai": false,
118
+ "category": "domain"
119
+ },
120
+ {
121
+ "slug": "ip-info",
122
+ "mcp_tool": "ip_info",
123
+ "param": "ip",
124
+ "param_description": "Public IPv4 or IPv6 address, e.g. 8.8.8.8",
125
+ "options": {},
126
+ "price_usd": 0.01,
127
+ "title": "IP address owner and network (RDAP + reverse DNS)",
128
+ "ai": false,
129
+ "category": "network"
130
+ },
131
+ {
132
+ "slug": "links-batch",
133
+ "mcp_tool": "links_batch",
134
+ "param": "urls",
135
+ "param_description": "1-10 public http(s) URLs, comma-separated",
136
+ "options": {},
137
+ "price_usd": 0.02,
138
+ "title": "Batch link check (up to 10 URLs per call)",
139
+ "ai": false,
140
+ "category": "web"
141
+ },
142
+ {
143
+ "slug": "broken-links",
144
+ "mcp_tool": "broken_links",
145
+ "param": "url",
146
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
147
+ "options": {},
148
+ "price_usd": 0.02,
149
+ "title": "Broken links on a page (up to 40 links checked)",
150
+ "ai": false,
151
+ "category": "seo"
152
+ },
153
+ {
154
+ "slug": "sitemap-urls",
155
+ "mcp_tool": "sitemap_urls",
156
+ "param": "url",
157
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
158
+ "options": {},
159
+ "price_usd": 0.01,
160
+ "title": "Sitemap URL extractor (robots.txt + sitemap index)",
161
+ "ai": false,
162
+ "category": "web-scraping"
163
+ },
164
+ {
165
+ "slug": "link-preview",
166
+ "mcp_tool": "link_preview",
167
+ "param": "url",
168
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
169
+ "options": {},
170
+ "price_usd": 0.01,
171
+ "title": "Link preview / unfurl (title, description, image, favicon, logo)",
172
+ "ai": false,
173
+ "category": "web-scraping"
174
+ },
175
+ {
176
+ "slug": "readability",
177
+ "mcp_tool": "readability",
178
+ "param": "url",
179
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
180
+ "options": {},
181
+ "price_usd": 0.01,
182
+ "title": "Readability and word count (Flesch, reading time, top terms)",
183
+ "ai": false,
184
+ "category": "content"
185
+ },
186
+ {
187
+ "slug": "structured-data",
188
+ "mcp_tool": "structured_data",
189
+ "param": "url",
190
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
191
+ "options": {},
192
+ "price_usd": 0.01,
193
+ "title": "Structured data extractor (JSON-LD, microdata, RDFa)",
194
+ "ai": false,
195
+ "category": "seo"
196
+ },
197
+ {
198
+ "slug": "canonical-check",
199
+ "mcp_tool": "canonical_check",
200
+ "param": "url",
201
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
202
+ "options": {},
203
+ "price_usd": 0.01,
204
+ "title": "Canonical, hreflang and indexability check",
205
+ "ai": false,
206
+ "category": "seo"
207
+ },
208
+ {
209
+ "slug": "cors-check",
210
+ "mcp_tool": "cors_check",
211
+ "param": "url",
212
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
213
+ "options": {
214
+ "origin": {
215
+ "pattern": "^https?://[a-zA-Z0-9.-]+(:\\d{1,5})?$",
216
+ "default": "https://example.com",
217
+ "description": "Origin to test, e.g. https://app.example.com"
218
+ },
219
+ "method": {
220
+ "enum": [
221
+ "GET",
222
+ "POST",
223
+ "PUT",
224
+ "PATCH",
225
+ "DELETE"
226
+ ],
227
+ "default": "GET",
228
+ "description": "Method for the preflight"
229
+ }
230
+ },
231
+ "price_usd": 0.01,
232
+ "title": "CORS check (preflight + allowed origins)",
233
+ "ai": false,
234
+ "category": "security"
235
+ },
236
+ {
237
+ "slug": "http-ping",
238
+ "mcp_tool": "http_ping",
239
+ "param": "url",
240
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
241
+ "options": {},
242
+ "price_usd": 0.01,
243
+ "title": "HTTP ping: up or down, status and timings (DNS, TCP, TLS, TTFB)",
244
+ "ai": false,
245
+ "category": "monitoring"
246
+ },
247
+ {
248
+ "slug": "domain-lookalike",
249
+ "mcp_tool": "domain_lookalike",
250
+ "param": "host",
251
+ "param_description": "Public host name, e.g. example.com or www.example.com (a URL also works)",
252
+ "options": {},
253
+ "price_usd": 0.01,
254
+ "title": "Lookalike / homograph domain check (punycode, mixed scripts, brand typos)",
255
+ "ai": false,
256
+ "category": "security"
257
+ },
258
+ {
259
+ "slug": "myr-rates",
260
+ "mcp_tool": "myr_rates",
261
+ "param": "currency",
262
+ "param_description": "3-letter currency code (e.g. USD, SGD, EUR) or ALL",
263
+ "options": {},
264
+ "price_usd": 0.01,
265
+ "title": "Malaysian ringgit exchange rates + OPR (Bank Negara Malaysia data)",
266
+ "ai": false,
267
+ "category": "finance"
268
+ },
269
+ {
270
+ "slug": "verdict-card",
271
+ "mcp_tool": "verdict_card",
272
+ "param": "url",
273
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
274
+ "options": {},
275
+ "price_usd": 0.02,
276
+ "title": "Shareable site verdict card (grade + one-liner, SVG)",
277
+ "ai": true,
278
+ "category": "ai"
279
+ },
280
+ {
281
+ "slug": "page-markdown",
282
+ "mcp_tool": "page_markdown",
283
+ "param": "url",
284
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
285
+ "options": {},
286
+ "price_usd": 0.01,
287
+ "title": "Page to Markdown (main content)",
288
+ "ai": false,
289
+ "category": "web-scraping"
290
+ },
291
+ {
292
+ "slug": "page-meta",
293
+ "mcp_tool": "page_meta",
294
+ "param": "url",
295
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
296
+ "options": {},
297
+ "price_usd": 0.01,
298
+ "title": "Page metadata + Markdown",
299
+ "ai": false,
300
+ "category": "web"
301
+ },
302
+ {
303
+ "slug": "email-auth",
304
+ "mcp_tool": "email_auth",
305
+ "param": "domain",
306
+ "param_description": "Domain name, e.g. example.com (a URL or email address also works)",
307
+ "options": {},
308
+ "price_usd": 0.01,
309
+ "title": "Email authentication check (SPF, DKIM, DMARC)",
310
+ "ai": false,
311
+ "category": "security"
312
+ },
313
+ {
314
+ "slug": "security-headers",
315
+ "mcp_tool": "security_headers",
316
+ "param": "url",
317
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
318
+ "options": {},
319
+ "price_usd": 0.01,
320
+ "title": "Security headers + TLS quick grade",
321
+ "ai": false,
322
+ "category": "security"
323
+ },
324
+ {
325
+ "slug": "tech-stack",
326
+ "mcp_tool": "tech_stack",
327
+ "param": "url",
328
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
329
+ "options": {},
330
+ "price_usd": 0.02,
331
+ "title": "Tech stack detection",
332
+ "ai": false,
333
+ "category": "web"
334
+ },
335
+ {
336
+ "slug": "seo-page",
337
+ "mcp_tool": "seo_page",
338
+ "param": "url",
339
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
340
+ "options": {},
341
+ "price_usd": 0.02,
342
+ "title": "On-page SEO check (one page)",
343
+ "ai": false,
344
+ "category": "seo"
345
+ },
346
+ {
347
+ "slug": "site-snapshot",
348
+ "mcp_tool": "site_snapshot",
349
+ "param": "url",
350
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
351
+ "options": {},
352
+ "price_usd": 0.03,
353
+ "title": "Site snapshot (SEO + security + tech + email)",
354
+ "ai": false,
355
+ "category": "web"
356
+ },
357
+ {
358
+ "slug": "page-quality",
359
+ "mcp_tool": "page_quality",
360
+ "param": "url",
361
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
362
+ "options": {},
363
+ "price_usd": 0.01,
364
+ "title": "Page quality verdict (AI)",
365
+ "ai": true,
366
+ "category": "ai"
367
+ },
368
+ {
369
+ "slug": "site-verdict",
370
+ "mcp_tool": "site_verdict",
371
+ "param": "url",
372
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
373
+ "options": {},
374
+ "price_usd": 0.02,
375
+ "title": "Is this a legit business site? (AI trust verdict)",
376
+ "ai": true,
377
+ "category": "ai"
378
+ },
379
+ {
380
+ "slug": "seo-audit-lite",
381
+ "mcp_tool": "seo_audit_lite",
382
+ "param": "url",
383
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
384
+ "options": {},
385
+ "price_usd": 0.03,
386
+ "title": "SEO Audit LITE (top 10 issues, one page)",
387
+ "ai": false,
388
+ "category": "seo"
389
+ },
390
+ {
391
+ "slug": "security-scan-lite",
392
+ "mcp_tool": "security_scan_lite",
393
+ "param": "url",
394
+ "param_description": "Public http(s) URL or bare domain, e.g. https://example.com",
395
+ "options": {},
396
+ "price_usd": 0.03,
397
+ "title": "Website Security Scan LITE (passive summary)",
398
+ "ai": false,
399
+ "category": "security"
400
+ }
401
+ ]
402
+ }
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "lordevo-x402"
3
+ version = "0.1.0"
4
+ description = "Client for Lordevo's pay-per-call web, DNS, email and domain checks for AI agents: 33 tools from $0.01, paid with x402 (USDC on Base). LangChain and CrewAI tools included."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Lordevo", email = "hello@lordevo.com" }]
10
+ keywords = ["x402", "ai-agents", "mcp", "a2a", "langchain", "crewai", "seo", "security", "dns", "robots.txt", "usdc", "base"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Operating System :: OS Independent",
14
+ "Intended Audience :: Developers",
15
+ "Topic :: Internet :: WWW/HTTP",
16
+ ]
17
+ dependencies = ["httpx>=0.27"]
18
+
19
+ [project.optional-dependencies]
20
+ pay = ["x402[httpx,requests,evm]>=2.25", "eth-account>=0.13"]
21
+ langchain = ["langchain-core>=0.3"]
22
+ crewai = ["crewai>=0.80"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://shop.lordevo.com"
26
+ Source = "https://github.com/forbot26/lordevo-x402"
27
+ Issues = "https://github.com/forbot26/lordevo-x402/issues"
28
+ Documentation = "https://shop.lordevo.com/llms.txt"
29
+
30
+ [build-system]
31
+ requires = ["hatchling>=1.27"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["lordevo_x402"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["lordevo_x402", "tests", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,82 @@
1
+ """Offline tests with httpx.MockTransport; nothing is paid."""
2
+ import asyncio
3
+ import base64
4
+ import json
5
+
6
+ import httpx
7
+ import pytest
8
+
9
+ import lordevo_x402 as L
10
+
11
+
12
+ def test_tools_and_urls():
13
+ assert len(L.TOOLS) == 33
14
+ assert all(0.01 <= t["price_usd"] <= 0.03 for t in L.TOOLS.values())
15
+ assert L.tool_url("dns-lookup", "example.com", type="MX", junk="x") == "https://shop.lordevo.com/api/quick/dns-lookup?host=example.com&type=MX"
16
+ with pytest.raises(ValueError):
17
+ L.tool_url("email-validate")
18
+
19
+
20
+ def test_sync_client_cap_cache_errors():
21
+ seen = []
22
+
23
+ def h(req):
24
+ seen.append(str(req.url))
25
+ if "ssl-cert" in str(req.url):
26
+ return httpx.Response(400, json={"error": "bad_host", "message": "Give a public host name", "charged": False})
27
+ return httpx.Response(200, json={"ok": True})
28
+
29
+ lo = L.LordevoSync(httpx.Client(transport=httpx.MockTransport(h)), max_price_usd=0.02, cache_seconds=60)
30
+ assert lo.can_fetch("https://example.com/a") == {"ok": True}
31
+ lo.can_fetch("https://example.com/a")
32
+ assert len(seen) == 1
33
+ with pytest.raises(ValueError, match="above max_price_usd"):
34
+ lo.site_check("example.com")
35
+ with pytest.raises(L.LordevoError) as e:
36
+ lo.ssl_cert("localhost")
37
+ assert e.value.status == 400 and e.value.charged is False
38
+
39
+
40
+ def test_async_client():
41
+ async def go():
42
+ async def h(req):
43
+ return httpx.Response(200, json={"url": str(req.url)})
44
+ async with L.Lordevo(httpx.AsyncClient(transport=httpx.MockTransport(h))) as lo:
45
+ return await lo.dns_lookup("example.com", type="mx")
46
+ assert asyncio.run(go())["url"].endswith("dns-lookup?host=example.com&type=mx")
47
+
48
+
49
+ def test_free_helpers():
50
+ pr = {"x402Version": 2, "accepts": [{"scheme": "exact", "network": "eip155:8453", "amount": "10000", "asset": "0xA", "payTo": "0xB"}]}
51
+
52
+ def h(req):
53
+ if req.url.path == "/a2a":
54
+ assert json.loads(req.content)["method"] == "message/send"
55
+ return httpx.Response(200, json={"jsonrpc": "2.0", "id": 1, "result": {"kind": "message", "role": "agent", "parts": [{"kind": "text", "text": "hi"}, {"kind": "data", "data": {"intent": "help"}}]}})
56
+ if req.url.path == "/catalog.json":
57
+ return httpx.Response(200, json={"quick_checks": []})
58
+ return httpx.Response(402, json={}, headers={"payment-required": base64.b64encode(json.dumps(pr).encode()).decode()})
59
+
60
+ c = httpx.Client(transport=httpx.MockTransport(h))
61
+ q = L.quote("dns-lookup", "example.com", client=c)
62
+ assert q["price_usd"] == 0.01 and q["network"] == "eip155:8453"
63
+ assert L.ask("help", client=c) == {"text": "hi", "intent": "help"}
64
+ assert L.catalog(client=c) == {"quick_checks": []}
65
+
66
+
67
+ def test_langchain_tools_build():
68
+ pytest.importorskip("langchain_core")
69
+ from lordevo_x402.langchain import langchain_tools
70
+ lo = L.LordevoSync(httpx.Client(transport=httpx.MockTransport(lambda r: httpx.Response(200, json={"u": str(r.url)}))))
71
+ tools = langchain_tools(sync_client=lo, only=["dns-lookup", "can-fetch"])
72
+ assert [t.name for t in tools] == ["can_fetch", "dns_lookup"] or sorted(t.name for t in tools) == ["can_fetch", "dns_lookup"]
73
+ out = json.loads(next(t for t in tools if t.name == "dns_lookup").invoke({"host": "example.com", "type": "MX"}))
74
+ assert out["u"].endswith("host=example.com&type=MX")
75
+
76
+
77
+ def test_crewai_tools_build():
78
+ pytest.importorskip("crewai")
79
+ from lordevo_x402.crewai import crewai_tools
80
+ lo = L.LordevoSync(httpx.Client(transport=httpx.MockTransport(lambda r: httpx.Response(200, json={"u": str(r.url)}))))
81
+ t = next(x for x in crewai_tools(lo, only=["dns-lookup"]) if x.name == "dns_lookup")
82
+ assert json.loads(t.run(host="example.com", type="MX"))["u"].endswith("host=example.com&type=MX")