Metadata-Version: 2.5
Name: limitguard
Version: 0.1.6
Summary: Official Python SDK for Limitguard: lead validation against the NL and BE company registers, EU VAT and sanctions lists
Project-URL: Homepage, https://limitguard.ai
Project-URL: Documentation, https://docs.limitguard.ai
Project-URL: Issues, https://github.com/jwconsultancyteam/limitguard-mcp/issues
Author: Limitguard
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,aml,b2b,compliance,crewai,kbo,kvk,kyb,langchain,lead-validation,lead-verification,limitguard,mcp,pep,risk,sanctions,usdc,vies,x402
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: crewai
Requires-Dist: crewai>=1.7; extra == 'crewai'
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: respx; extra == 'dev'
Provides-Extra: evm
Requires-Dist: eth-account>=0.13; extra == 'evm'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
Provides-Extra: solana
Requires-Dist: solana>=0.34; extra == 'solana'
Requires-Dist: solders>=0.21; extra == 'solana'
Description-Content-Type: text/markdown

# limitguard

Official Python SDK for [Limitguard](https://limitguard.ai), the lead validation service: check the company behind each lead against the Dutch (KVK) and Belgian (KBO) company registers, EU VAT (VIES), sanctions lists and the email domain, and run company and KYB checks. Paid per call in USDC over x402 or debited from an API key's prepaid balance.

## Install

```bash
pip install limitguard
```

With wallet support, for agents that pay per call:

```bash
pip install 'limitguard[solana]'   # Solana USDC
pip install 'limitguard[evm]'      # Base USDC
```

For agent frameworks:

```bash
pip install 'limitguard[langchain]'   # LangChain tools
pip install 'limitguard[crewai]'      # CrewAI tools
```

Python 3.10 or newer.

## Quickstart

```python
import asyncio
from limitguard import LimitGuardClient


async def main():
    async with LimitGuardClient(api_key="lg_live_...") as client:
        result = await client.check_entity("Acme BV", country="NL")
        print(result.trust_score, result.trust_level, result.recommendation)


asyncio.run(main())
```

`check_entity` returns a `TrustResponse` (`trust_score` 0-100, `trust_level`, `cluster`, `recommendation`, `confidence`, `top_factors`). `risk_score` is the cheaper two-source variant and returns a `RiskResponse`.

## Lead Verify

`verify_lead` checks one Dutch or Belgian sales lead: is it a real, active company? $0.27 per lead.

```python
lead = await client.verify_lead("NL", company_number="32147382", email="sales@example.nl")
print(lead.verdict, lead.lead_score, lead.flags)
```

For an NL lead send `company_number` (8-digit KVK) or `name`. A BE lead needs `company_number` (10-digit KBO): with a name alone it comes back `unverifiable`, and the call is still charged. Add anything else you know: `vat_number`, `email` (only its domain is checked), `domain`, `address` (a `LeadAddress` or a dict with `street`, `house_number`, `postcode`, `city`) and `iban` (only its last 4 characters come back). Returns a `LeadVerifyResponse`: `verdict` (`real_active`, `real_inactive`, `not_found`, `ambiguous`, `unverifiable` or `sanctioned`), `lead_score` 0-100, `company`, `vat`, `iban`, `sanctions`, `email`, `flags` and `findings`. Sandbox keys get `403`: the answer comes from live registers only.

## Agent check

`agent_check` checks one EVM wallet: OFAC SDN address screen, Base balances, ERC-8004 identity and open feedback, and optionally the website domain the agent gives for itself. $0.75 per wallet.

```python
check = await client.agent_check("0x4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b", agent_id=1)
print(check.verdict, check.verdict_reasons)
```

Returns an `AgentCheckResponse`: `verdict` (`block`, `review` or `no_findings`), and each component (`sanctions`, `balance`, `identity`, `reputation`, `wallet_risk`, `domain`) with its own `status`. Sandbox keys get `403`.

## LangChain and CrewAI

Three ready-made tools wrap the client: entity check, risk score and Lead Verify. An agent can call them without any HTTP or payment code of yours.

LangChain (`pip install 'limitguard[langchain]'`):

```python
from limitguard.integrations.langchain import limitguard_tools

tools = limitguard_tools(api_key="lg_live_...")
# hand them to any tool-calling agent, e.g. create_agent(model, tools=tools)
print(tools[0].invoke({"entity_name": "Acme BV", "country": "NL"}))
```

CrewAI (`pip install 'limitguard[crewai]'`):

```python
from crewai import Agent
from limitguard.integrations.crewai import limitguard_tools

agent = Agent(
    role="Supplier vetting",
    goal="Only onboard companies that check out",
    backstory="You check every new supplier before the first order.",
    tools=limitguard_tools(api_key="lg_live_..."),
)
```

- The key can also come from `LIMITGUARD_API_KEY`, and `wallet=` works as described in "Paying for calls".
- Each tool returns the JSON of the typed response (`TrustResponse`, `RiskResponse` or `LeadVerifyResponse`).
- Errors come back to the agent as a short plain-English message instead of an exception: for example a key with no balance (402), or a sandbox key on Lead Verify (403). The cap below is the one case that raises instead.
- Each call is charged like a direct client call.
- The tool classes are `LimitGuardEntityCheckTool`, `LimitGuardRiskScoreTool` and `LimitGuardLeadVerifyTool`, if you want only one.

### Call and spend cap

An agent decides on its own when to call these tools, and every call is paid. So the tools stop on their own: by default after **20 calls** or **USD 5** of charges, whichever comes first.

- `limitguard_tools()` gives its three tools one shared cap: 20 calls and USD 5 for the three together, not per tool. A tool class built on its own has its own cap.
- Set your own limits with `max_calls=` and `max_usd=`, on `limitguard_tools()` or on a tool class. `None` turns a limit off, so `max_calls=None, max_usd=None` turns the cap off completely.
- When the cap is reached, the tool raises `ToolCapReached` (a `LimitGuardError`) and sends nothing, so the refused call costs nothing. In LangChain the agent run stops with that exception. CrewAI catches tool exceptions itself and shows the agent the message; the tool then refuses every further call the same way.
- Every call a tool sends counts toward `max_calls`, including one the API refuses. `max_usd` counts what the API reports it charged: the prepaid debit on an API key, or the amount of every payment a wallet signed or sent, counted even if the API then refuses the call, as a signed payment may still be settled. A wallet payment that was sent but not seen confirmed (the wallet's error says it may still land) counts too, at the quoted price. A call the API refunds counts nothing.
- A call is refused when it would take the total past `max_usd`, judged by what the same tool cost the last time it ran. A tool that has not cost anything yet has no such figure, so while `max_usd` is set only one such call runs at a time and parallel calls wait for it. A call that has waited one request timeout (`timeout=`, 30 seconds by default) is refused with `ToolCapReached` and sends nothing. The total can pass `max_usd` by at most that one call.
- On an API key, a call whose response is lost (a timeout, say) may still have been charged, and the client sends it again. The same goes for a call cancelled while in flight. Each lost response counts at the tool's last price; before the tool has a price it counts nothing.
- `tool.spend_cap` shows where you are: `calls_used`, `spent_usd`, `max_calls` and `max_usd`. To share one cap across several `limitguard_tools()` calls, build a `ToolCap(max_calls=..., max_usd=...)` and pass it as `cap=`.

```python
from limitguard.integrations import ToolCapReached
from limitguard.integrations.langchain import limitguard_tools

tools = limitguard_tools(api_key="lg_live_...", max_calls=50, max_usd=20)
try:
    agent.invoke({"messages": [("user", "Vet our ten new suppliers")]})
except ToolCapReached as err:
    print(err)  # how much was used, and which setting raises the cap
```

Examples that run on a free sandbox key, with no LLM, are in `examples/langchain_tool.py` and `examples/crewai_tool.py`.

## Paying for calls

Two ways to satisfy the API's per-call price, and the client accepts either or both:

| You pass | What happens |
|---|---|
| `api_key=` on a **paid** tier (`indie` and up) | Each call is debited from the key's prepaid balance at list price. No per-call payment by the client. When the balance is short the API answers `402 insufficient_balance`: the client raises `PaymentRequiredError` unless a `wallet=` is set, in which case it pays that call by x402. |
| `wallet=` | The client pays each call in USDC over x402 automatically when the API answers 402, echoes the `X-Payment-Challenge` the 402 carried on the paid retry (the proof is only redeemable together with it), and signs that challenge with the paying wallet. |
| both | Calls are paid by the wallet and attributed to the key. |

A `free`-tier key identifies you and tracks usage; it holds no balance and does not pay for calls. On its own it will raise `PaymentRequiredError` on any paid endpoint. Add a wallet, or top up the key (`POST /v1/keys/upgrade/{tier}` or `POST /v1/keys/topup/{usd}` via x402).

Wallet example (Solana, needs the `[solana]` extra and a wallet holding USDC, no SOL: Limitguard's fee payer co-signs the transaction and covers the network fee. Pass `broadcast=True` to `SolanaWallet` to pay it yourself instead, which requires SOL):

```python
from solders.keypair import Keypair
from limitguard import LimitGuardClient
from limitguard.x402 import SolanaWallet

wallet = SolanaWallet(keypair=Keypair.from_base58_string(private_key))
async with LimitGuardClient(wallet=wallet) as client:
    result = await client.check_entity("Acme BV", country="NL")
```

`SolanaWallet` pays in Solana USDC and `EvmWallet` in Base USDC; both take the key of the wallet the money leaves.

When the API quotes a settled-transfer price, the client also signs a short challenge message with the same wallet that sent the USDC and puts it in `X-Payment-Challenge-Signature`. The message is the protocol tag, the resource path, the chain id, your transaction hash or signature, and the challenge id: five lines, exactly as documented on the [x402 protocol page](https://docs.limitguard.ai/x402-protocol). It exists because a transaction hash is public the moment it lands: without the signature, anyone who saw it could redeem your payment. Your private key never leaves the wallet object, and a wallet that cannot sign still pays: the field is optional server-side today and will become required, so signing now means nothing breaks later.

A complete agent is in `examples/agent_with_wallet.py`, included in the source distribution (`pip download --no-binary :all: limitguard`).

A Lead Verify example is in `examples/verify_lead.py`.

## Errors

All exceptions subclass `LimitGuardError`: `AuthenticationError`, `PaymentRequiredError`, `RateLimitError`, `ValidationError`, `ServerError`. The client retries 429 and 5xx responses with backoff before raising. Any other status raises `LimitGuardError` with `status_code` set: for example `403` when a sandbox key calls Lead Verify or the agent check, and `404` while an endpoint is switched off (nothing is charged).

## Links

- Documentation: https://docs.limitguard.ai
- Quickstart and a free sandbox key (mock data, no wallet): https://api.limitguard.ai/v1/quickstart
- x402 protocol: https://docs.limitguard.ai/x402-protocol
- Issues and support: https://github.com/jwconsultancyteam/limitguard-mcp/issues

## Version

```python
import limitguard
print(limitguard.__version__)
```

Releases are tagged `sdk-v<version>` and published to PyPI from that tag.
