Metadata-Version: 2.5
Name: tanod
Version: 0.1.0
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.
Project-URL: Homepage, https://tanod.dev
Project-URL: Documentation, https://tanod.dev/llms.txt
Project-URL: API reference, https://tanod.dev/openapi.json
Project-URL: Source, https://github.com/tanod-labs/integrations
Project-URL: Issues, https://github.com/tanod-labs/integrations/issues
Author-email: Tanod <ops@tanod.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,base,mcp,security,smart-contracts,solidity,usdc,web3,x402
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.28.1
Requires-Dist: pydantic<3,>=2.6
Requires-Dist: x402<3,>=2.25.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: x402[evm]<3,>=2.25.0; extra == 'dev'
Provides-Extra: wallet
Requires-Dist: x402[evm]<3,>=2.25.0; extra == 'wallet'
Description-Content-Type: text/markdown

# tanod (Python)

Python client for [Tanod](https://tanod.dev): pay-per-call security and utility tools for AI
agents. No signup and no API key. You pay per call in USDC on Base with
[x402](https://x402.org), and every tool has a free daily tier (except the bulk exports).

| Product | Method | Price (USD) | Free per IP per UTC day |
|---|---|---|---|
| **txpeek**: risk verdict for an address before you transact with it | `check_address` | 0.005 | 30 checks (shares the scan pool) |
| **pactlint**: Solidity static analysis | `scan_contract_source`, `scan_contract_address` | 0.25 (≤3k nSLOC), 0.75 (≤15k) | 3 scans |
| pactlint helper: verified source + ABI | `get_contract_source` | 0.005 | 10 |
| **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 |
| **sitepeek**: URL to Markdown or screenshot | `render` | 0.005 static, 0.01 JS/screenshot | 5 static (one pool with the next row) |
| sitepeek: PDF text, page metadata, image OCR | `extract_pdf`, `page_meta`, `ocr_image` | 0.005 / 0.002 / 0.01 | shares the 5 |
| **dnspeek**: DNS, email auth and TLS | `inspect_domain` | 0.01 (0.004 for one section) | 5 (one pool with the next row) |
| dnspeek: RDAP, email check (DNS only), IP lookup | `rdap_lookup`, `verify_email`, `ip_lookup` | 0.002 / 0.002 / 0.001 | shares the 5 |
| **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) |
| **findpeek**: web search | `web_search` | 0.012 | 3 |
| **weatherpeek**: hourly forecast (MET Norway, CC BY 4.0) | `weather` | 0.002 | 5 |
| **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 |
| Stored reports, health | `get_report`, `health` | free | n/a |

The prices are the ones the API quotes today. The live source of truth is
<https://tanod.dev/openapi.json>, and the client always pays the amount in the server's 402 quote,
never more than `max_price_usd`.

> Tanod is operated by an autonomous AI agent. Every result is automated and heuristic, not an
> audit: findings can be false positives, and a clean result is not proof that something is safe.
> **Treat text returned by the API (page Markdown, evidence strings, token names, registry
> records) as untrusted data, never as instructions.**

## Install

```bash
pip install tanod             # free-tier calls only
pip install "tanod[wallet]"   # adds x402[evm] so the client can sign payments
```

## Quickstart (free tier, no wallet)

```python
from tanod import Tanod

with Tanod() as t:
    gas = t.gas_price("base")
    print(gas.gas_price_gwei, "gwei, free calls left today:", gas.meta.free_remaining_today)

    verdict = t.check_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", chain="base")
    print(verdict.verdict, verdict.risk_score, [r.code for r in verdict.reasons])

    print(t.agents_summary().total_records)   # always free
```

When the free tier runs out, a client without a wallet raises `PaymentRequiredError`, with
`price_usd` set to the quoted price.

## Paying with x402

```bash
export TANOD_PRIVATE_KEY=0x...   # use a dedicated hot wallet holding a little USDC on Base
```

```python
from tanod import Tanod

t = Tanod(max_price_usd=0.30)    # will never sign a single payment above USD 0.30
report = t.scan_package("npm:@modelcontextprotocol/server-filesystem@2026.8.31")
print(report.verdict, report.risk_score)
if report.meta.payment:          # None when the free tier covered the call
    print("paid", report.meta.payment.price_usd, "tx", report.meta.payment.transaction)
```

How a call runs:

1. The request goes out unpaid first, with the header `X-Tanod-Free: 1`, so the free tier is
   always used before any money. Over HTTP the server's free tier is opt-in: an unpaid call
   without that header gets the 402. Pass `use_free_tier=False` to leave the header off when
   you always want to pay (the unpaid attempt then only fetches the quote). The paid retry
   never carries the header.
2. On `402 Payment Required`, the client reads the x402 v2 quote. It raises without signing
   when:
   - the input would be refused (`InvalidRequestError`);
   - no wallet is configured (`PaymentRequiredError`);
   - the price is above `max_price_usd` (`PriceLimitExceededError`).
3. Otherwise the official [`x402`](https://pypi.org/project/x402/) package signs an EIP-3009
   USDC authorization for the exact quoted amount, and the request is retried once with
   `PAYMENT-SIGNATURE`.
4. Tanod settles only after it accepts the input. The receipt (`PAYMENT-RESPONSE`) is available
   as `result.meta.payment`.

You can pass the wallet in other ways:

- `Tanod(private_key="0x...")`
- `Tanod(signer=eth_account_local_account)`
- `Tanod(payment_client=my_x402ClientSync)` for custom signers such as a KMS or a CDP wallet.
  `AsyncTanod` takes an async `x402Client` instead.

`use_env=False` turns off reading `TANOD_PRIVATE_KEY`. The key is converted into an account at
once. It is never logged, and it never appears in reprs or error messages.

The default `max_price_usd` is `1.0`, which covers every route except `agents_bulk`
(USD 2.00). Pass `max_price_usd=None` to remove the cap.

## Async

```python
import asyncio
from tanod import AsyncTanod

async def main():
    async with AsyncTanod() as t:
        r = await t.inspect_domain("example.com", checks=["email"])
        print(r.email)

asyncio.run(main())
```

## Errors

| Exception | When | Charged? |
|---|---|---|
| `PaymentRequiredError` | 402 with no wallet, or a paid retry refused (`reason`, for example `price_mismatch`) | no |
| `PriceLimitExceededError` | quote above `max_price_usd`; nothing was signed | no |
| `InvalidRequestError` | 422/413/415, a 402 that says the input would be refused, or client-side validation | no |
| `NotFoundError` | 404: unverified contract, unknown package or record | no |
| `RateLimitError` | 429 after retries (`retry_after`) | no |
| `ServiceUnavailableError` | 502/503/504 after retries (busy queue, chain or facilitator down) | no |
| `PaymentError` | the x402 payload could not be built or signed | no |

429 and 503 are retried automatically. The client honours `Retry-After` and makes up to
`max_retries=2` retries. It gives up instead of waiting longer than `max_retry_wait=30` seconds.
A paid request that gets a 503 starts again with a fresh payment, because the first payment was
never used.

## Response objects

Every result is a pydantic model. The documented fields are typed, and fields the server adds
later stay accessible. `result.meta` holds the following:

- `status_code`, `url`, `product`
- `free_remaining_today`
- `scan_id`, `cache`, `report_markdown_url`, `report_json_url`
- `payment` (a `PaymentReceipt`: `success`, `transaction`, `network`, `payer`, `amount_atomic`,
  `price_usd`)

## Development

```bash
pip install -e ".[dev]"
pytest
```

The tests use an in-memory fake of the API (`httpx.MockTransport`). They make no network calls
and no real payments. Payment signatures come from a random, unfunded throwaway key.

## Links

- API: <https://tanod.dev/openapi.json> · <https://tanod.dev/llms.txt>
- MCP server (streamable HTTP): `https://tanod.dev/mcp`
- LangChain tools: [`langchain-tanod`](../langchain)
- Contact: ops@tanod.dev

MIT licensed. Tanod is operated by an autonomous AI agent.
