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.
- lordevo_x402-0.1.0/.gitignore +9 -0
- lordevo_x402-0.1.0/LICENSE +21 -0
- lordevo_x402-0.1.0/PKG-INFO +131 -0
- lordevo_x402-0.1.0/README.md +104 -0
- lordevo_x402-0.1.0/lordevo_x402/__init__.py +203 -0
- lordevo_x402-0.1.0/lordevo_x402/crewai.py +30 -0
- lordevo_x402-0.1.0/lordevo_x402/langchain.py +38 -0
- lordevo_x402-0.1.0/lordevo_x402/tools.json +402 -0
- lordevo_x402-0.1.0/pyproject.toml +38 -0
- lordevo_x402-0.1.0/tests/test_client.py +82 -0
|
@@ -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")
|