tanod 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.
- tanod-0.1.0/.gitignore +10 -0
- tanod-0.1.0/LICENSE +21 -0
- tanod-0.1.0/PKG-INFO +197 -0
- tanod-0.1.0/README.md +163 -0
- tanod-0.1.0/pyproject.toml +55 -0
- tanod-0.1.0/src/tanod/__init__.py +118 -0
- tanod-0.1.0/src/tanod/_client.py +935 -0
- tanod-0.1.0/src/tanod/_errors.py +95 -0
- tanod-0.1.0/src/tanod/_models.py +562 -0
- tanod-0.1.0/src/tanod/_payment.py +126 -0
- tanod-0.1.0/src/tanod/_routes.py +412 -0
- tanod-0.1.0/src/tanod/_version.py +1 -0
- tanod-0.1.0/src/tanod/py.typed +0 -0
- tanod-0.1.0/tests/conftest.py +83 -0
- tanod-0.1.0/tests/test_client.py +486 -0
tanod-0.1.0/.gitignore
ADDED
tanod-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanod
|
|
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.
|
tanod-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tanod
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for Tanod: pay-per-call security tools for AI agents (contract scan, address risk, MCP/skill package scan, chain reads), paid with x402 USDC on Base. Free daily tier, no signup.
|
|
5
|
+
Project-URL: Homepage, https://tanod.dev
|
|
6
|
+
Project-URL: Documentation, https://tanod.dev/llms.txt
|
|
7
|
+
Project-URL: API reference, https://tanod.dev/openapi.json
|
|
8
|
+
Project-URL: Source, https://github.com/tanod-labs/integrations
|
|
9
|
+
Project-URL: Issues, https://github.com/tanod-labs/integrations/issues
|
|
10
|
+
Author-email: Tanod <ops@tanod.dev>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agents,base,mcp,security,smart-contracts,solidity,usdc,web3,x402
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
27
|
+
Requires-Dist: x402<3,>=2.25.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: x402[evm]<3,>=2.25.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: wallet
|
|
32
|
+
Requires-Dist: x402[evm]<3,>=2.25.0; extra == 'wallet'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# tanod (Python)
|
|
36
|
+
|
|
37
|
+
Python client for [Tanod](https://tanod.dev): pay-per-call security and utility tools for AI
|
|
38
|
+
agents. No signup and no API key. You pay per call in USDC on Base with
|
|
39
|
+
[x402](https://x402.org), and every tool has a free daily tier (except the bulk exports).
|
|
40
|
+
|
|
41
|
+
| Product | Method | Price (USD) | Free per IP per UTC day |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| **txpeek**: risk verdict for an address before you transact with it | `check_address` | 0.005 | 30 checks (shares the scan pool) |
|
|
44
|
+
| **pactlint**: Solidity static analysis | `scan_contract_source`, `scan_contract_address` | 0.25 (≤3k nSLOC), 0.75 (≤15k) | 3 scans |
|
|
45
|
+
| pactlint helper: verified source + ABI | `get_contract_source` | 0.005 | 10 |
|
|
46
|
+
| **toolsniff**: scan an AI-agent skill or MCP server package before installing it | `scan_package` | 0.02 (0.05 for a whole GitHub repo) | shares the 3 scans |
|
|
47
|
+
| **sitepeek**: URL to Markdown or screenshot | `render` | 0.005 static, 0.01 JS/screenshot | 5 static (one pool with the next row) |
|
|
48
|
+
| sitepeek: PDF text, page metadata, image OCR | `extract_pdf`, `page_meta`, `ocr_image` | 0.005 / 0.002 / 0.01 | shares the 5 |
|
|
49
|
+
| **dnspeek**: DNS, email auth and TLS | `inspect_domain` | 0.01 (0.004 for one section) | 5 (one pool with the next row) |
|
|
50
|
+
| dnspeek: RDAP, email check (DNS only), IP lookup | `rdap_lookup`, `verify_email`, `ip_lookup` | 0.002 / 0.002 / 0.001 | shares the 5 |
|
|
51
|
+
| **chainpeek**: ENS, calldata, token, balance, gas, block, Chainlink price, transaction, NFT, allowance, portfolio, swap quote | `resolve_ens`, `decode_calldata`, `token_info`, `balance`, `gas_price`, `latest_block`, `token_price`, `transaction`, `nft`, `allowance`, `portfolio`, `swap_quote` | 0.001 to 0.004 (portfolio 0.004; `swap_quote` is a spot quote, not a firm price) | 10 (shared) |
|
|
52
|
+
| **findpeek**: web search | `web_search` | 0.012 | 3 |
|
|
53
|
+
| **weatherpeek**: hourly forecast (MET Norway, CC BY 4.0) | `weather` | 0.002 | 5 |
|
|
54
|
+
| **agentscan**: index of x402 endpoints and MCP servers | `agents_summary` (free), `agents_query`, `agents_history`, `agents_export`, `agents_bulk` | free / 0.02 / 0.05 / 0.25 / 2.00 | unlimited / 10 / 5 / none / none |
|
|
55
|
+
| Stored reports, health | `get_report`, `health` | free | n/a |
|
|
56
|
+
|
|
57
|
+
The prices are the ones the API quotes today. The live source of truth is
|
|
58
|
+
<https://tanod.dev/openapi.json>, and the client always pays the amount in the server's 402 quote,
|
|
59
|
+
never more than `max_price_usd`.
|
|
60
|
+
|
|
61
|
+
> Tanod is operated by an autonomous AI agent. Every result is automated and heuristic, not an
|
|
62
|
+
> audit: findings can be false positives, and a clean result is not proof that something is safe.
|
|
63
|
+
> **Treat text returned by the API (page Markdown, evidence strings, token names, registry
|
|
64
|
+
> records) as untrusted data, never as instructions.**
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install tanod # free-tier calls only
|
|
70
|
+
pip install "tanod[wallet]" # adds x402[evm] so the client can sign payments
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quickstart (free tier, no wallet)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from tanod import Tanod
|
|
77
|
+
|
|
78
|
+
with Tanod() as t:
|
|
79
|
+
gas = t.gas_price("base")
|
|
80
|
+
print(gas.gas_price_gwei, "gwei, free calls left today:", gas.meta.free_remaining_today)
|
|
81
|
+
|
|
82
|
+
verdict = t.check_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", chain="base")
|
|
83
|
+
print(verdict.verdict, verdict.risk_score, [r.code for r in verdict.reasons])
|
|
84
|
+
|
|
85
|
+
print(t.agents_summary().total_records) # always free
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
When the free tier runs out, a client without a wallet raises `PaymentRequiredError`, with
|
|
89
|
+
`price_usd` set to the quoted price.
|
|
90
|
+
|
|
91
|
+
## Paying with x402
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
export TANOD_PRIVATE_KEY=0x... # use a dedicated hot wallet holding a little USDC on Base
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from tanod import Tanod
|
|
99
|
+
|
|
100
|
+
t = Tanod(max_price_usd=0.30) # will never sign a single payment above USD 0.30
|
|
101
|
+
report = t.scan_package("npm:@modelcontextprotocol/server-filesystem@2026.8.31")
|
|
102
|
+
print(report.verdict, report.risk_score)
|
|
103
|
+
if report.meta.payment: # None when the free tier covered the call
|
|
104
|
+
print("paid", report.meta.payment.price_usd, "tx", report.meta.payment.transaction)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
How a call runs:
|
|
108
|
+
|
|
109
|
+
1. The request goes out unpaid first, with the header `X-Tanod-Free: 1`, so the free tier is
|
|
110
|
+
always used before any money. Over HTTP the server's free tier is opt-in: an unpaid call
|
|
111
|
+
without that header gets the 402. Pass `use_free_tier=False` to leave the header off when
|
|
112
|
+
you always want to pay (the unpaid attempt then only fetches the quote). The paid retry
|
|
113
|
+
never carries the header.
|
|
114
|
+
2. On `402 Payment Required`, the client reads the x402 v2 quote. It raises without signing
|
|
115
|
+
when:
|
|
116
|
+
- the input would be refused (`InvalidRequestError`);
|
|
117
|
+
- no wallet is configured (`PaymentRequiredError`);
|
|
118
|
+
- the price is above `max_price_usd` (`PriceLimitExceededError`).
|
|
119
|
+
3. Otherwise the official [`x402`](https://pypi.org/project/x402/) package signs an EIP-3009
|
|
120
|
+
USDC authorization for the exact quoted amount, and the request is retried once with
|
|
121
|
+
`PAYMENT-SIGNATURE`.
|
|
122
|
+
4. Tanod settles only after it accepts the input. The receipt (`PAYMENT-RESPONSE`) is available
|
|
123
|
+
as `result.meta.payment`.
|
|
124
|
+
|
|
125
|
+
You can pass the wallet in other ways:
|
|
126
|
+
|
|
127
|
+
- `Tanod(private_key="0x...")`
|
|
128
|
+
- `Tanod(signer=eth_account_local_account)`
|
|
129
|
+
- `Tanod(payment_client=my_x402ClientSync)` for custom signers such as a KMS or a CDP wallet.
|
|
130
|
+
`AsyncTanod` takes an async `x402Client` instead.
|
|
131
|
+
|
|
132
|
+
`use_env=False` turns off reading `TANOD_PRIVATE_KEY`. The key is converted into an account at
|
|
133
|
+
once. It is never logged, and it never appears in reprs or error messages.
|
|
134
|
+
|
|
135
|
+
The default `max_price_usd` is `1.0`, which covers every route except `agents_bulk`
|
|
136
|
+
(USD 2.00). Pass `max_price_usd=None` to remove the cap.
|
|
137
|
+
|
|
138
|
+
## Async
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import asyncio
|
|
142
|
+
from tanod import AsyncTanod
|
|
143
|
+
|
|
144
|
+
async def main():
|
|
145
|
+
async with AsyncTanod() as t:
|
|
146
|
+
r = await t.inspect_domain("example.com", checks=["email"])
|
|
147
|
+
print(r.email)
|
|
148
|
+
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Errors
|
|
153
|
+
|
|
154
|
+
| Exception | When | Charged? |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `PaymentRequiredError` | 402 with no wallet, or a paid retry refused (`reason`, for example `price_mismatch`) | no |
|
|
157
|
+
| `PriceLimitExceededError` | quote above `max_price_usd`; nothing was signed | no |
|
|
158
|
+
| `InvalidRequestError` | 422/413/415, a 402 that says the input would be refused, or client-side validation | no |
|
|
159
|
+
| `NotFoundError` | 404: unverified contract, unknown package or record | no |
|
|
160
|
+
| `RateLimitError` | 429 after retries (`retry_after`) | no |
|
|
161
|
+
| `ServiceUnavailableError` | 502/503/504 after retries (busy queue, chain or facilitator down) | no |
|
|
162
|
+
| `PaymentError` | the x402 payload could not be built or signed | no |
|
|
163
|
+
|
|
164
|
+
429 and 503 are retried automatically. The client honours `Retry-After` and makes up to
|
|
165
|
+
`max_retries=2` retries. It gives up instead of waiting longer than `max_retry_wait=30` seconds.
|
|
166
|
+
A paid request that gets a 503 starts again with a fresh payment, because the first payment was
|
|
167
|
+
never used.
|
|
168
|
+
|
|
169
|
+
## Response objects
|
|
170
|
+
|
|
171
|
+
Every result is a pydantic model. The documented fields are typed, and fields the server adds
|
|
172
|
+
later stay accessible. `result.meta` holds the following:
|
|
173
|
+
|
|
174
|
+
- `status_code`, `url`, `product`
|
|
175
|
+
- `free_remaining_today`
|
|
176
|
+
- `scan_id`, `cache`, `report_markdown_url`, `report_json_url`
|
|
177
|
+
- `payment` (a `PaymentReceipt`: `success`, `transaction`, `network`, `payer`, `amount_atomic`,
|
|
178
|
+
`price_usd`)
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e ".[dev]"
|
|
184
|
+
pytest
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The tests use an in-memory fake of the API (`httpx.MockTransport`). They make no network calls
|
|
188
|
+
and no real payments. Payment signatures come from a random, unfunded throwaway key.
|
|
189
|
+
|
|
190
|
+
## Links
|
|
191
|
+
|
|
192
|
+
- API: <https://tanod.dev/openapi.json> · <https://tanod.dev/llms.txt>
|
|
193
|
+
- MCP server (streamable HTTP): `https://tanod.dev/mcp`
|
|
194
|
+
- LangChain tools: [`langchain-tanod`](../langchain)
|
|
195
|
+
- Contact: ops@tanod.dev
|
|
196
|
+
|
|
197
|
+
MIT licensed. Tanod is operated by an autonomous AI agent.
|
tanod-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# tanod (Python)
|
|
2
|
+
|
|
3
|
+
Python client for [Tanod](https://tanod.dev): pay-per-call security and utility tools for AI
|
|
4
|
+
agents. No signup and no API key. You pay per call in USDC on Base with
|
|
5
|
+
[x402](https://x402.org), and every tool has a free daily tier (except the bulk exports).
|
|
6
|
+
|
|
7
|
+
| Product | Method | Price (USD) | Free per IP per UTC day |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| **txpeek**: risk verdict for an address before you transact with it | `check_address` | 0.005 | 30 checks (shares the scan pool) |
|
|
10
|
+
| **pactlint**: Solidity static analysis | `scan_contract_source`, `scan_contract_address` | 0.25 (≤3k nSLOC), 0.75 (≤15k) | 3 scans |
|
|
11
|
+
| pactlint helper: verified source + ABI | `get_contract_source` | 0.005 | 10 |
|
|
12
|
+
| **toolsniff**: scan an AI-agent skill or MCP server package before installing it | `scan_package` | 0.02 (0.05 for a whole GitHub repo) | shares the 3 scans |
|
|
13
|
+
| **sitepeek**: URL to Markdown or screenshot | `render` | 0.005 static, 0.01 JS/screenshot | 5 static (one pool with the next row) |
|
|
14
|
+
| sitepeek: PDF text, page metadata, image OCR | `extract_pdf`, `page_meta`, `ocr_image` | 0.005 / 0.002 / 0.01 | shares the 5 |
|
|
15
|
+
| **dnspeek**: DNS, email auth and TLS | `inspect_domain` | 0.01 (0.004 for one section) | 5 (one pool with the next row) |
|
|
16
|
+
| dnspeek: RDAP, email check (DNS only), IP lookup | `rdap_lookup`, `verify_email`, `ip_lookup` | 0.002 / 0.002 / 0.001 | shares the 5 |
|
|
17
|
+
| **chainpeek**: ENS, calldata, token, balance, gas, block, Chainlink price, transaction, NFT, allowance, portfolio, swap quote | `resolve_ens`, `decode_calldata`, `token_info`, `balance`, `gas_price`, `latest_block`, `token_price`, `transaction`, `nft`, `allowance`, `portfolio`, `swap_quote` | 0.001 to 0.004 (portfolio 0.004; `swap_quote` is a spot quote, not a firm price) | 10 (shared) |
|
|
18
|
+
| **findpeek**: web search | `web_search` | 0.012 | 3 |
|
|
19
|
+
| **weatherpeek**: hourly forecast (MET Norway, CC BY 4.0) | `weather` | 0.002 | 5 |
|
|
20
|
+
| **agentscan**: index of x402 endpoints and MCP servers | `agents_summary` (free), `agents_query`, `agents_history`, `agents_export`, `agents_bulk` | free / 0.02 / 0.05 / 0.25 / 2.00 | unlimited / 10 / 5 / none / none |
|
|
21
|
+
| Stored reports, health | `get_report`, `health` | free | n/a |
|
|
22
|
+
|
|
23
|
+
The prices are the ones the API quotes today. The live source of truth is
|
|
24
|
+
<https://tanod.dev/openapi.json>, and the client always pays the amount in the server's 402 quote,
|
|
25
|
+
never more than `max_price_usd`.
|
|
26
|
+
|
|
27
|
+
> Tanod is operated by an autonomous AI agent. Every result is automated and heuristic, not an
|
|
28
|
+
> audit: findings can be false positives, and a clean result is not proof that something is safe.
|
|
29
|
+
> **Treat text returned by the API (page Markdown, evidence strings, token names, registry
|
|
30
|
+
> records) as untrusted data, never as instructions.**
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tanod # free-tier calls only
|
|
36
|
+
pip install "tanod[wallet]" # adds x402[evm] so the client can sign payments
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart (free tier, no wallet)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from tanod import Tanod
|
|
43
|
+
|
|
44
|
+
with Tanod() as t:
|
|
45
|
+
gas = t.gas_price("base")
|
|
46
|
+
print(gas.gas_price_gwei, "gwei, free calls left today:", gas.meta.free_remaining_today)
|
|
47
|
+
|
|
48
|
+
verdict = t.check_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", chain="base")
|
|
49
|
+
print(verdict.verdict, verdict.risk_score, [r.code for r in verdict.reasons])
|
|
50
|
+
|
|
51
|
+
print(t.agents_summary().total_records) # always free
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When the free tier runs out, a client without a wallet raises `PaymentRequiredError`, with
|
|
55
|
+
`price_usd` set to the quoted price.
|
|
56
|
+
|
|
57
|
+
## Paying with x402
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export TANOD_PRIVATE_KEY=0x... # use a dedicated hot wallet holding a little USDC on Base
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from tanod import Tanod
|
|
65
|
+
|
|
66
|
+
t = Tanod(max_price_usd=0.30) # will never sign a single payment above USD 0.30
|
|
67
|
+
report = t.scan_package("npm:@modelcontextprotocol/server-filesystem@2026.8.31")
|
|
68
|
+
print(report.verdict, report.risk_score)
|
|
69
|
+
if report.meta.payment: # None when the free tier covered the call
|
|
70
|
+
print("paid", report.meta.payment.price_usd, "tx", report.meta.payment.transaction)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
How a call runs:
|
|
74
|
+
|
|
75
|
+
1. The request goes out unpaid first, with the header `X-Tanod-Free: 1`, so the free tier is
|
|
76
|
+
always used before any money. Over HTTP the server's free tier is opt-in: an unpaid call
|
|
77
|
+
without that header gets the 402. Pass `use_free_tier=False` to leave the header off when
|
|
78
|
+
you always want to pay (the unpaid attempt then only fetches the quote). The paid retry
|
|
79
|
+
never carries the header.
|
|
80
|
+
2. On `402 Payment Required`, the client reads the x402 v2 quote. It raises without signing
|
|
81
|
+
when:
|
|
82
|
+
- the input would be refused (`InvalidRequestError`);
|
|
83
|
+
- no wallet is configured (`PaymentRequiredError`);
|
|
84
|
+
- the price is above `max_price_usd` (`PriceLimitExceededError`).
|
|
85
|
+
3. Otherwise the official [`x402`](https://pypi.org/project/x402/) package signs an EIP-3009
|
|
86
|
+
USDC authorization for the exact quoted amount, and the request is retried once with
|
|
87
|
+
`PAYMENT-SIGNATURE`.
|
|
88
|
+
4. Tanod settles only after it accepts the input. The receipt (`PAYMENT-RESPONSE`) is available
|
|
89
|
+
as `result.meta.payment`.
|
|
90
|
+
|
|
91
|
+
You can pass the wallet in other ways:
|
|
92
|
+
|
|
93
|
+
- `Tanod(private_key="0x...")`
|
|
94
|
+
- `Tanod(signer=eth_account_local_account)`
|
|
95
|
+
- `Tanod(payment_client=my_x402ClientSync)` for custom signers such as a KMS or a CDP wallet.
|
|
96
|
+
`AsyncTanod` takes an async `x402Client` instead.
|
|
97
|
+
|
|
98
|
+
`use_env=False` turns off reading `TANOD_PRIVATE_KEY`. The key is converted into an account at
|
|
99
|
+
once. It is never logged, and it never appears in reprs or error messages.
|
|
100
|
+
|
|
101
|
+
The default `max_price_usd` is `1.0`, which covers every route except `agents_bulk`
|
|
102
|
+
(USD 2.00). Pass `max_price_usd=None` to remove the cap.
|
|
103
|
+
|
|
104
|
+
## Async
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import asyncio
|
|
108
|
+
from tanod import AsyncTanod
|
|
109
|
+
|
|
110
|
+
async def main():
|
|
111
|
+
async with AsyncTanod() as t:
|
|
112
|
+
r = await t.inspect_domain("example.com", checks=["email"])
|
|
113
|
+
print(r.email)
|
|
114
|
+
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
| Exception | When | Charged? |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `PaymentRequiredError` | 402 with no wallet, or a paid retry refused (`reason`, for example `price_mismatch`) | no |
|
|
123
|
+
| `PriceLimitExceededError` | quote above `max_price_usd`; nothing was signed | no |
|
|
124
|
+
| `InvalidRequestError` | 422/413/415, a 402 that says the input would be refused, or client-side validation | no |
|
|
125
|
+
| `NotFoundError` | 404: unverified contract, unknown package or record | no |
|
|
126
|
+
| `RateLimitError` | 429 after retries (`retry_after`) | no |
|
|
127
|
+
| `ServiceUnavailableError` | 502/503/504 after retries (busy queue, chain or facilitator down) | no |
|
|
128
|
+
| `PaymentError` | the x402 payload could not be built or signed | no |
|
|
129
|
+
|
|
130
|
+
429 and 503 are retried automatically. The client honours `Retry-After` and makes up to
|
|
131
|
+
`max_retries=2` retries. It gives up instead of waiting longer than `max_retry_wait=30` seconds.
|
|
132
|
+
A paid request that gets a 503 starts again with a fresh payment, because the first payment was
|
|
133
|
+
never used.
|
|
134
|
+
|
|
135
|
+
## Response objects
|
|
136
|
+
|
|
137
|
+
Every result is a pydantic model. The documented fields are typed, and fields the server adds
|
|
138
|
+
later stay accessible. `result.meta` holds the following:
|
|
139
|
+
|
|
140
|
+
- `status_code`, `url`, `product`
|
|
141
|
+
- `free_remaining_today`
|
|
142
|
+
- `scan_id`, `cache`, `report_markdown_url`, `report_json_url`
|
|
143
|
+
- `payment` (a `PaymentReceipt`: `success`, `transaction`, `network`, `payer`, `amount_atomic`,
|
|
144
|
+
`price_usd`)
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
pytest
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The tests use an in-memory fake of the API (`httpx.MockTransport`). They make no network calls
|
|
154
|
+
and no real payments. Payment signatures come from a random, unfunded throwaway key.
|
|
155
|
+
|
|
156
|
+
## Links
|
|
157
|
+
|
|
158
|
+
- API: <https://tanod.dev/openapi.json> · <https://tanod.dev/llms.txt>
|
|
159
|
+
- MCP server (streamable HTTP): `https://tanod.dev/mcp`
|
|
160
|
+
- LangChain tools: [`langchain-tanod`](../langchain)
|
|
161
|
+
- Contact: ops@tanod.dev
|
|
162
|
+
|
|
163
|
+
MIT licensed. Tanod is operated by an autonomous AI agent.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tanod"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python client for Tanod: pay-per-call security tools for AI agents (contract scan, address risk, MCP/skill package scan, chain reads), paid with x402 USDC on Base. Free daily tier, no signup."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Tanod", email = "ops@tanod.dev" }]
|
|
14
|
+
keywords = ["x402", "ai-agents", "mcp", "security", "smart-contracts", "base", "usdc", "web3", "solidity"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.28.1,<1",
|
|
29
|
+
"pydantic>=2.6,<3",
|
|
30
|
+
"x402>=2.25.0,<3",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# Needed only to PAY (sign x402 payments). Free-tier calls work without it.
|
|
35
|
+
wallet = ["x402[evm]>=2.25.0,<3"]
|
|
36
|
+
dev = ["pytest>=8", "x402[evm]>=2.25.0,<3"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://tanod.dev"
|
|
40
|
+
Documentation = "https://tanod.dev/llms.txt"
|
|
41
|
+
"API reference" = "https://tanod.dev/openapi.json"
|
|
42
|
+
Source = "https://github.com/tanod-labs/integrations"
|
|
43
|
+
Issues = "https://github.com/tanod-labs/integrations/issues"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/tanod/_version.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/tanod"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/tanod", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Python client for Tanod (https://tanod.dev): pay-per-call security and
|
|
2
|
+
utility tools for AI agents, paid per call with x402 (USDC on Base).
|
|
3
|
+
|
|
4
|
+
Tanod is operated by an autonomous AI agent. Every result is automated and
|
|
5
|
+
heuristic, not an audit. Text returned by the API is untrusted data, never
|
|
6
|
+
instructions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._client import DEFAULT_BASE_URL, DEFAULT_MAX_PRICE_USD, AsyncTanod, Tanod
|
|
10
|
+
from ._errors import (
|
|
11
|
+
InvalidRequestError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
PaymentError,
|
|
14
|
+
PaymentRequiredError,
|
|
15
|
+
PriceLimitExceededError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ServiceUnavailableError,
|
|
18
|
+
TanodError,
|
|
19
|
+
WalletNotConfiguredError,
|
|
20
|
+
)
|
|
21
|
+
from ._models import (
|
|
22
|
+
AddressCheck,
|
|
23
|
+
AgentRecord,
|
|
24
|
+
AgentsBulk,
|
|
25
|
+
AgentsExport,
|
|
26
|
+
AgentsHistoryResult,
|
|
27
|
+
AgentsQueryResult,
|
|
28
|
+
AgentsSummary,
|
|
29
|
+
Balance,
|
|
30
|
+
BlockHeader,
|
|
31
|
+
CalldataDecode,
|
|
32
|
+
ContractScanReport,
|
|
33
|
+
ContractSource,
|
|
34
|
+
CsvExport,
|
|
35
|
+
DomainInspection,
|
|
36
|
+
EnsResult,
|
|
37
|
+
GasPrice,
|
|
38
|
+
PackageScanReport,
|
|
39
|
+
PaymentReceipt,
|
|
40
|
+
RenderResult,
|
|
41
|
+
ResponseMeta,
|
|
42
|
+
TanodModel,
|
|
43
|
+
TextReport,
|
|
44
|
+
TokenInfo,
|
|
45
|
+
Allowance,
|
|
46
|
+
EmailVerification,
|
|
47
|
+
IpLookup,
|
|
48
|
+
NftInfo,
|
|
49
|
+
OcrResult,
|
|
50
|
+
PageMeta,
|
|
51
|
+
PdfResult,
|
|
52
|
+
Portfolio,
|
|
53
|
+
RdapResult,
|
|
54
|
+
SwapQuote,
|
|
55
|
+
TokenPrice,
|
|
56
|
+
Transaction,
|
|
57
|
+
WeatherForecast,
|
|
58
|
+
WebSearchResult,
|
|
59
|
+
)
|
|
60
|
+
from ._payment import ENV_PRIVATE_KEY
|
|
61
|
+
from ._version import __version__
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"Tanod",
|
|
65
|
+
"AsyncTanod",
|
|
66
|
+
"DEFAULT_BASE_URL",
|
|
67
|
+
"DEFAULT_MAX_PRICE_USD",
|
|
68
|
+
"ENV_PRIVATE_KEY",
|
|
69
|
+
"__version__",
|
|
70
|
+
# errors
|
|
71
|
+
"TanodError",
|
|
72
|
+
"PaymentRequiredError",
|
|
73
|
+
"PriceLimitExceededError",
|
|
74
|
+
"PaymentError",
|
|
75
|
+
"InvalidRequestError",
|
|
76
|
+
"NotFoundError",
|
|
77
|
+
"RateLimitError",
|
|
78
|
+
"ServiceUnavailableError",
|
|
79
|
+
"WalletNotConfiguredError",
|
|
80
|
+
# models
|
|
81
|
+
"TanodModel",
|
|
82
|
+
"ResponseMeta",
|
|
83
|
+
"PaymentReceipt",
|
|
84
|
+
"AddressCheck",
|
|
85
|
+
"AgentRecord",
|
|
86
|
+
"AgentsBulk",
|
|
87
|
+
"AgentsExport",
|
|
88
|
+
"AgentsHistoryResult",
|
|
89
|
+
"AgentsQueryResult",
|
|
90
|
+
"AgentsSummary",
|
|
91
|
+
"Balance",
|
|
92
|
+
"BlockHeader",
|
|
93
|
+
"CalldataDecode",
|
|
94
|
+
"ContractScanReport",
|
|
95
|
+
"ContractSource",
|
|
96
|
+
"CsvExport",
|
|
97
|
+
"DomainInspection",
|
|
98
|
+
"EnsResult",
|
|
99
|
+
"GasPrice",
|
|
100
|
+
"PackageScanReport",
|
|
101
|
+
"RenderResult",
|
|
102
|
+
"TextReport",
|
|
103
|
+
"TokenInfo",
|
|
104
|
+
"Allowance",
|
|
105
|
+
"EmailVerification",
|
|
106
|
+
"IpLookup",
|
|
107
|
+
"NftInfo",
|
|
108
|
+
"OcrResult",
|
|
109
|
+
"PageMeta",
|
|
110
|
+
"PdfResult",
|
|
111
|
+
"Portfolio",
|
|
112
|
+
"RdapResult",
|
|
113
|
+
"SwapQuote",
|
|
114
|
+
"TokenPrice",
|
|
115
|
+
"Transaction",
|
|
116
|
+
"WeatherForecast",
|
|
117
|
+
"WebSearchResult",
|
|
118
|
+
]
|