limitguard 0.1.0__py3-none-any.whl

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.
limitguard/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """LimitGuard.ai Python SDK - Trust Intelligence API client."""
2
+
3
+ from limitguard.client import LimitGuardClient
4
+ from limitguard.exceptions import (
5
+ AuthenticationError,
6
+ LimitGuardError,
7
+ PaymentRequiredError,
8
+ RateLimitError,
9
+ ServerError,
10
+ ValidationError,
11
+ )
12
+ from limitguard.models import (
13
+ Cluster,
14
+ Recommendation,
15
+ RiskResponse,
16
+ TopFactor,
17
+ TrustLevel,
18
+ TrustResponse,
19
+ )
20
+
21
+ __all__ = [
22
+ "LimitGuardClient",
23
+ "LimitGuardError",
24
+ "AuthenticationError",
25
+ "PaymentRequiredError",
26
+ "RateLimitError",
27
+ "ServerError",
28
+ "ValidationError",
29
+ "TrustResponse",
30
+ "RiskResponse",
31
+ "TrustLevel",
32
+ "Recommendation",
33
+ "Cluster",
34
+ "TopFactor",
35
+ ]
36
+
37
+ __version__ = "0.1.0"
limitguard/client.py ADDED
@@ -0,0 +1,219 @@
1
+ """
2
+ Async HTTP client for LimitGuard.ai Trust Intelligence API.
3
+
4
+ Usage with API key:
5
+ async with LimitGuardClient(api_key="lg_live_...") as client:
6
+ result = await client.check_entity("Acme B.V.", "NL", kvk_number="12345678")
7
+
8
+ Usage with Solana wallet (x402 pay-per-call):
9
+ from limitguard.x402 import SolanaWallet
10
+ from solders.keypair import Keypair
11
+ wallet = SolanaWallet(keypair=Keypair.from_base58_string(PRIVATE_KEY))
12
+ async with LimitGuardClient(wallet=wallet) as client:
13
+ result = await client.check_entity("Acme B.V.", "NL")
14
+ """
15
+
16
+ import asyncio
17
+
18
+ import httpx
19
+
20
+ from limitguard.exceptions import (
21
+ AuthenticationError,
22
+ LimitGuardError,
23
+ PaymentRequiredError,
24
+ RateLimitError,
25
+ ServerError,
26
+ ValidationError,
27
+ )
28
+ from limitguard.models import RiskResponse, TrustResponse
29
+
30
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
31
+ _MAX_RETRIES = 3
32
+ _BACKOFF_BASE = 1.0 # seconds
33
+
34
+
35
+ class LimitGuardClient:
36
+ """Async client for the LimitGuard.ai API.
37
+
38
+ Supports two auth modes:
39
+ - API key: subscription-based access (X-API-Key header)
40
+ - Wallet: x402 pay-per-call with crypto (PAYMENT-SIGNATURE header)
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str | None = None,
46
+ wallet=None,
47
+ base_url: str = "https://api.limitguard.ai",
48
+ timeout: float = 30.0,
49
+ ):
50
+ if not api_key and not wallet:
51
+ raise ValueError("Provide api_key, wallet, or both")
52
+ self._api_key = api_key or ""
53
+ self._wallet = wallet
54
+ self._base_url = base_url.rstrip("/")
55
+ headers = {"Content-Type": "application/json"}
56
+ if api_key:
57
+ headers["X-API-Key"] = api_key
58
+ self._client = httpx.AsyncClient(
59
+ base_url=self._base_url,
60
+ headers=headers,
61
+ timeout=timeout,
62
+ )
63
+
64
+ async def _request(
65
+ self,
66
+ method: str,
67
+ path: str,
68
+ json: dict | None = None,
69
+ params: dict | None = None,
70
+ ) -> dict:
71
+ """Make an HTTP request with retry on 429/5xx and auto-pay on 402."""
72
+ last_exc: Exception | None = None
73
+
74
+ for attempt in range(_MAX_RETRIES):
75
+ try:
76
+ response = await self._client.request(
77
+ method, path, json=json, params=params,
78
+ )
79
+ except httpx.HTTPError as exc:
80
+ last_exc = LimitGuardError(f"HTTP error: {exc}")
81
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
82
+ continue
83
+
84
+ if response.status_code == 200:
85
+ return response.json()
86
+
87
+ # Auto-pay on 402 if wallet is configured
88
+ if response.status_code == 402 and self._wallet:
89
+ return await self._pay_and_retry(response, method, path, json, params)
90
+
91
+ if response.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
92
+ retry_after = response.headers.get("retry-after")
93
+ wait = float(retry_after) if retry_after else _BACKOFF_BASE * (2 ** attempt)
94
+ await asyncio.sleep(wait)
95
+ continue
96
+
97
+ self._raise_for_status(response)
98
+
99
+ if last_exc:
100
+ raise last_exc
101
+ raise LimitGuardError("Max retries exceeded")
102
+
103
+ async def _pay_and_retry(
104
+ self,
105
+ response_402: httpx.Response,
106
+ method: str,
107
+ path: str,
108
+ json: dict | None,
109
+ params: dict | None,
110
+ ) -> dict:
111
+ """Handle 402 by making x402 payment and retrying."""
112
+ from limitguard.x402 import parse_402_response
113
+
114
+ body = response_402.json()
115
+ payment_info = parse_402_response(body)
116
+
117
+ # Prefer Solana for SolanaWallet
118
+ from limitguard.x402 import SolanaWallet
119
+ if isinstance(self._wallet, SolanaWallet) and "solana" in payment_info:
120
+ info = payment_info["solana"]
121
+ elif "base" in payment_info:
122
+ info = payment_info["base"]
123
+ elif "solana" in payment_info:
124
+ info = payment_info["solana"]
125
+ else:
126
+ raise PaymentRequiredError("No supported payment chain in 402 response")
127
+
128
+ payment_header = await self._wallet.build_payment(
129
+ amount_micro_usdc=info["amount"],
130
+ recipient=info["recipient"],
131
+ )
132
+
133
+ # Retry with payment header
134
+ paid_response = await self._client.request(
135
+ method, path, json=json, params=params,
136
+ headers={"PAYMENT-SIGNATURE": payment_header},
137
+ )
138
+
139
+ if paid_response.status_code == 200:
140
+ return paid_response.json()
141
+
142
+ self._raise_for_status(paid_response)
143
+ raise LimitGuardError("Payment accepted but request failed")
144
+
145
+ @staticmethod
146
+ def _raise_for_status(response: httpx.Response) -> None:
147
+ """Map HTTP status codes to typed exceptions."""
148
+ body = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
149
+ detail = body.get("detail", body.get("error", response.text))
150
+
151
+ if response.status_code == 401:
152
+ raise AuthenticationError(str(detail))
153
+ if response.status_code == 402:
154
+ raise PaymentRequiredError(str(detail))
155
+ if response.status_code == 422:
156
+ raise ValidationError(str(detail), detail=str(body))
157
+ if response.status_code == 429:
158
+ retry_after = response.headers.get("retry-after")
159
+ raise RateLimitError(
160
+ str(detail),
161
+ retry_after=float(retry_after) if retry_after else None,
162
+ )
163
+ if response.status_code >= 500:
164
+ raise ServerError(str(detail), status_code=response.status_code)
165
+
166
+ raise LimitGuardError(f"Unexpected status {response.status_code}: {detail}", status_code=response.status_code)
167
+
168
+ async def check_entity(
169
+ self,
170
+ entity_name: str,
171
+ country: str,
172
+ *,
173
+ kvk_number: str | None = None,
174
+ cbe_number: str | None = None,
175
+ domain: str | None = None,
176
+ iban: str | None = None,
177
+ vat_number: str | None = None,
178
+ ) -> TrustResponse:
179
+ """POST /v1/entity/check - Full trust intelligence."""
180
+ payload: dict = {"entity_name": entity_name, "country": country}
181
+ if kvk_number:
182
+ payload["kvk_number"] = kvk_number
183
+ if cbe_number:
184
+ payload["cbe_number"] = cbe_number
185
+ if domain:
186
+ payload["domain"] = domain
187
+ if iban:
188
+ payload["iban"] = iban
189
+ if vat_number:
190
+ payload["vat_number"] = vat_number
191
+
192
+ data = await self._request("POST", "/v1/entity/check", json=payload)
193
+ return TrustResponse(**data)
194
+
195
+ async def risk_score(self, entity_name: str, country: str) -> RiskResponse:
196
+ """POST /v1/risk/score - Quick risk assessment."""
197
+ data = await self._request("POST", "/v1/risk/score", json={
198
+ "entity_name": entity_name,
199
+ "country": country,
200
+ })
201
+ return RiskResponse(**data)
202
+
203
+ async def get_usage(self, period: str = "day") -> dict:
204
+ """GET /v1/usage/summary - Usage stats for current API key."""
205
+ return await self._request("GET", "/v1/usage/summary", params={"period": period})
206
+
207
+ async def verify_audit(self) -> dict:
208
+ """GET /v1/audit/verify - Verify audit trail integrity."""
209
+ return await self._request("GET", "/v1/audit/verify")
210
+
211
+ async def close(self) -> None:
212
+ """Close the underlying httpx client."""
213
+ await self._client.aclose()
214
+
215
+ async def __aenter__(self) -> "LimitGuardClient":
216
+ return self
217
+
218
+ async def __aexit__(self, *args: object) -> None:
219
+ await self.close()
@@ -0,0 +1,50 @@
1
+ """
2
+ Typed exceptions for LimitGuard SDK.
3
+
4
+ Maps HTTP status codes to specific exception types.
5
+ """
6
+
7
+
8
+ class LimitGuardError(Exception):
9
+ """Base exception for all LimitGuard SDK errors."""
10
+
11
+ def __init__(self, message: str, status_code: int | None = None):
12
+ self.status_code = status_code
13
+ super().__init__(message)
14
+
15
+
16
+ class AuthenticationError(LimitGuardError):
17
+ """Raised on 401 - invalid or missing API key."""
18
+
19
+ def __init__(self, message: str = "Invalid or missing API key"):
20
+ super().__init__(message, status_code=401)
21
+
22
+
23
+ class PaymentRequiredError(LimitGuardError):
24
+ """Raised on 402 - payment required (x402 protocol)."""
25
+
26
+ def __init__(self, message: str = "Payment required"):
27
+ super().__init__(message, status_code=402)
28
+
29
+
30
+ class RateLimitError(LimitGuardError):
31
+ """Raised on 429 - rate limit or monthly quota exceeded."""
32
+
33
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: float | None = None):
34
+ self.retry_after = retry_after
35
+ super().__init__(message, status_code=429)
36
+
37
+
38
+ class ValidationError(LimitGuardError):
39
+ """Raised on 422 - request validation failed."""
40
+
41
+ def __init__(self, message: str = "Validation error", detail: str | None = None):
42
+ self.detail = detail
43
+ super().__init__(message, status_code=422)
44
+
45
+
46
+ class ServerError(LimitGuardError):
47
+ """Raised on 5xx - server-side error."""
48
+
49
+ def __init__(self, message: str = "Server error", status_code: int = 500):
50
+ super().__init__(message, status_code=status_code)
limitguard/models.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Response models for LimitGuard SDK.
3
+
4
+ Mirrors the API response schemas so callers get typed objects.
5
+ """
6
+
7
+ from enum import Enum
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class TrustLevel(str, Enum):
12
+ HIGH = "high"
13
+ MEDIUM = "medium"
14
+ LOW = "low"
15
+ CRITICAL = "critical"
16
+
17
+
18
+ class Recommendation(str, Enum):
19
+ PROCEED = "proceed"
20
+ REVIEW = "review"
21
+ EDD = "enhanced_due_diligence"
22
+ BLOCK = "block"
23
+
24
+
25
+ class Cluster(str, Enum):
26
+ ESTABLISHED_EU_ENTERPRISE = "established_eu_enterprise"
27
+ ESTABLISHED_EU_SME = "established_eu_sme"
28
+ VERIFIED_STARTUP = "verified_startup"
29
+ UNVERIFIED_NEW = "unverified_new"
30
+ HIGH_RISK_JURISDICTION = "high_risk_jurisdiction"
31
+ SANCTIONS_FLAGGED = "sanctions_flagged"
32
+ INSUFFICIENT_DATA = "insufficient_data"
33
+ MIXED_SIGNALS = "mixed_signals"
34
+
35
+
36
+ class TopFactor(BaseModel):
37
+ source: str
38
+ signal: str
39
+ impact: str
40
+ weight: float
41
+
42
+
43
+ class TrustResponse(BaseModel):
44
+ trust_score: int = Field(ge=0, le=100)
45
+ trust_level: TrustLevel
46
+ cluster: Cluster
47
+ recommendation: Recommendation
48
+ confidence: float = Field(ge=0, le=1)
49
+ top_factors: list[TopFactor]
50
+ correlations: dict[str, float] = Field(default_factory=dict)
51
+ sources_checked: int = Field(ge=0)
52
+ processing_time_ms: int = Field(ge=0)
53
+ version: str = "1.0"
54
+
55
+
56
+ class RiskResponse(BaseModel):
57
+ risk_score: int = Field(ge=0, le=100)
58
+ risk_level: TrustLevel
59
+ recommendation: Recommendation
60
+ top_factors: list[TopFactor]
61
+ processing_time_ms: int = Field(ge=0)
limitguard/x402.py ADDED
@@ -0,0 +1,231 @@
1
+ """
2
+ x402 payment support for LimitGuard SDK.
3
+
4
+ Enables AI agents and programmatic clients to pay for API calls
5
+ using Solana USDC or Base USDC via the x402 HTTP payment protocol.
6
+
7
+ Solana flow:
8
+ 1. Build SPL TransferChecked instruction (USDC)
9
+ 2. Sign and broadcast transaction
10
+ 3. Wait for confirmation
11
+ 4. Send tx signature in PAYMENT-SIGNATURE header
12
+
13
+ Base (EVM) flow:
14
+ 1. Sign EIP-712 typed data (EIP-3009 TransferWithAuthorization)
15
+ 2. Send signature in PAYMENT-SIGNATURE header
16
+ """
17
+
18
+ import asyncio
19
+ import base64
20
+ import json
21
+ import time
22
+
23
+ import httpx
24
+
25
+ # Solana constants
26
+ USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
27
+ TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
28
+ ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
29
+ COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111"
30
+ SOLANA_CHAIN_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
31
+
32
+
33
+ class SolanaWallet:
34
+ """Solana wallet for programmatic x402 payments.
35
+
36
+ Usage:
37
+ from solders.keypair import Keypair
38
+ wallet = SolanaWallet(keypair=Keypair.from_base58_string(PRIVATE_KEY))
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ keypair,
44
+ rpc_url: str = "https://api.mainnet-beta.solana.com",
45
+ ):
46
+ self._keypair = keypair
47
+ self._rpc_url = rpc_url
48
+
49
+ @property
50
+ def pubkey(self) -> str:
51
+ return str(self._keypair.pubkey())
52
+
53
+ async def build_payment(
54
+ self,
55
+ amount_micro_usdc: int,
56
+ recipient: str,
57
+ ) -> str:
58
+ """Build, sign, broadcast a USDC transfer and return PAYMENT-SIGNATURE header.
59
+
60
+ Args:
61
+ amount_micro_usdc: Amount in micro-USDC (e.g. 100000 = $0.10)
62
+ recipient: Recipient Solana address
63
+
64
+ Returns:
65
+ Base64-encoded payment header value for PAYMENT-SIGNATURE header.
66
+ """
67
+ try:
68
+ from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
69
+ from solders.hash import Hash
70
+ from solders.keypair import Keypair
71
+ from solders.message import MessageV0
72
+ from solders.pubkey import Pubkey
73
+ from solders.transaction import VersionedTransaction
74
+ import spl.token.instructions as spl_token
75
+ except ImportError:
76
+ raise ImportError(
77
+ "Solana payment requires: pip install 'limitguard[solana]' "
78
+ "(installs solders, solana, spl-token)"
79
+ )
80
+
81
+ sender_key = self._keypair.pubkey()
82
+ recipient_key = Pubkey.from_string(recipient)
83
+ usdc_mint = Pubkey.from_string(USDC_MINT)
84
+ token_prog = Pubkey.from_string(TOKEN_PROGRAM)
85
+ ata_prog = Pubkey.from_string(ATA_PROGRAM)
86
+
87
+ # Derive ATAs
88
+ sender_ata = Pubkey.find_program_address(
89
+ [bytes(sender_key), bytes(token_prog), bytes(usdc_mint)],
90
+ ata_prog,
91
+ )[0]
92
+ recipient_ata = Pubkey.find_program_address(
93
+ [bytes(recipient_key), bytes(token_prog), bytes(usdc_mint)],
94
+ ata_prog,
95
+ )[0]
96
+
97
+ # Get recent blockhash
98
+ async with httpx.AsyncClient(timeout=15) as client:
99
+ resp = await client.post(self._rpc_url, json={
100
+ "jsonrpc": "2.0", "id": 1,
101
+ "method": "getLatestBlockhash",
102
+ "params": [{"commitment": "confirmed"}],
103
+ })
104
+ result = resp.json()
105
+ if "error" in result:
106
+ raise RuntimeError(f"Blockhash fetch failed: {result['error']}")
107
+ bh_data = result["result"]["value"]
108
+ blockhash = Hash.from_string(bh_data["blockhash"])
109
+
110
+ # Build instructions
111
+ ix0 = set_compute_unit_limit(200_000)
112
+ ix1 = set_compute_unit_price(50_000)
113
+
114
+ # SPL TransferChecked
115
+ ix2 = spl_token.transfer_checked(
116
+ spl_token.TransferCheckedParams(
117
+ program_id=token_prog,
118
+ source=sender_ata,
119
+ mint=usdc_mint,
120
+ dest=recipient_ata,
121
+ owner=sender_key,
122
+ amount=amount_micro_usdc,
123
+ decimals=6,
124
+ )
125
+ )
126
+
127
+ # Build and sign transaction
128
+ msg = MessageV0.try_compile(
129
+ payer=sender_key,
130
+ instructions=[ix0, ix1, ix2],
131
+ address_lookup_table_accounts=[],
132
+ recent_blockhash=blockhash,
133
+ )
134
+ tx = VersionedTransaction(msg, [self._keypair])
135
+
136
+ # Broadcast
137
+ tx_bytes = bytes(tx)
138
+ async with httpx.AsyncClient(timeout=30) as client:
139
+ resp = await client.post(self._rpc_url, json={
140
+ "jsonrpc": "2.0", "id": 1,
141
+ "method": "sendTransaction",
142
+ "params": [
143
+ base64.b64encode(tx_bytes).decode(),
144
+ {"encoding": "base64", "skipPreflight": True},
145
+ ],
146
+ })
147
+ result = resp.json()
148
+ if "error" in result:
149
+ raise RuntimeError(f"Transaction failed: {result['error']}")
150
+ tx_sig = result["result"]
151
+
152
+ # Poll for confirmation
153
+ confirmed = False
154
+ async with httpx.AsyncClient(timeout=60) as client:
155
+ for _ in range(30):
156
+ await asyncio.sleep(2)
157
+ resp = await client.post(self._rpc_url, json={
158
+ "jsonrpc": "2.0", "id": 1,
159
+ "method": "getSignatureStatuses",
160
+ "params": [[tx_sig]],
161
+ })
162
+ body = resp.json()
163
+ if "error" in body:
164
+ # #343: an RPC error (auth, rate limit, malformed reply) is not
165
+ # "not landed yet". Say so, with the signature, instead of
166
+ # burning the whole budget and blaming the wallet's balance.
167
+ raise RuntimeError(
168
+ f"Confirmation poll failed for {tx_sig}: {body['error']}. "
169
+ "The transaction was broadcast and may still land; check the "
170
+ "signature before paying again."
171
+ )
172
+ statuses = body.get("result", {}).get("value", [None])
173
+ st = statuses[0] if statuses else None
174
+ # #344: a transaction can be confirmed or finalized AND have failed
175
+ # (non-null err: missing ATA, insufficient balance, program error).
176
+ # err has to win, or a reverted transfer is reported as a payment.
177
+ if st and st.get("err"):
178
+ raise RuntimeError(f"Transaction {tx_sig} failed on chain: {st['err']}")
179
+ if st and st.get("confirmationStatus") in ("confirmed", "finalized"):
180
+ confirmed = True
181
+ break
182
+
183
+ if not confirmed:
184
+ raise RuntimeError(
185
+ f"Transaction {tx_sig} not confirmed after 60s. "
186
+ "Wallet may have insufficient SOL for gas or USDC for payment."
187
+ )
188
+
189
+ # Build payment header
190
+ now = int(time.time())
191
+ payload = {
192
+ "chainId": SOLANA_CHAIN_ID,
193
+ "sender": str(sender_key),
194
+ "recipient": recipient,
195
+ "amount": str(amount_micro_usdc),
196
+ "nonce": tx_sig,
197
+ "validAfter": now - 10,
198
+ "validBefore": now + 300,
199
+ "signature": tx_sig,
200
+ "payload": {"txSignature": tx_sig},
201
+ }
202
+ return base64.b64encode(json.dumps(payload).encode()).decode()
203
+
204
+
205
+ def parse_402_response(response_json: dict) -> dict:
206
+ """Parse a 402 response to extract payment requirements.
207
+
208
+ Returns dict with keys: amount, recipient, chain_id, fee_payer (Solana only).
209
+ """
210
+ accepts = response_json.get("accepts", [])
211
+ result = {}
212
+ for accept in accepts:
213
+ network = accept.get("network", "")
214
+ entry = {
215
+ "amount": int(accept.get("amount", 0)),
216
+ "recipient": accept.get("payTo", ""),
217
+ "chain_id": network,
218
+ "asset": accept.get("asset", ""),
219
+ }
220
+ # `or {}` not a default: an accepts entry may carry an explicit
221
+ # `"extra": null`, and `.get("extra", {})` returns None for that,
222
+ # so the caller would see AttributeError instead of a payment error.
223
+ fee_payer = (accept.get("extra") or {}).get("feePayer")
224
+ if fee_payer:
225
+ entry["fee_payer"] = fee_payer
226
+
227
+ if "solana" in network:
228
+ result["solana"] = entry
229
+ elif "eip155" in network:
230
+ result["base"] = entry
231
+ return result
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.5
2
+ Name: limitguard
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the LimitGuard Trust Intelligence API
5
+ Project-URL: Homepage, https://limitguard.ai
6
+ Project-URL: Documentation, https://docs.limitguard.ai
7
+ Project-URL: Issues, https://github.com/jwconsultancyteam/limitguard-mcp/issues
8
+ Author: LimitGuard
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: compliance,kyb,limitguard,risk,trust,usdc,x402
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio; extra == 'dev'
26
+ Requires-Dist: respx; extra == 'dev'
27
+ Provides-Extra: solana
28
+ Requires-Dist: solana>=0.34; extra == 'solana'
29
+ Requires-Dist: solders>=0.21; extra == 'solana'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # limitguard
33
+
34
+ Official Python SDK for the [LimitGuard](https://limitguard.ai) Trust Intelligence API: entity verification and risk scoring for EU businesses, paid per call in USDC over x402 or covered by a subscription.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install limitguard
40
+ ```
41
+
42
+ With Solana wallet support, for agents that pay per call:
43
+
44
+ ```bash
45
+ pip install 'limitguard[solana]'
46
+ ```
47
+
48
+ Python 3.10 or newer.
49
+
50
+ ## Quickstart
51
+
52
+ ```python
53
+ import asyncio
54
+ from limitguard import LimitGuardClient
55
+
56
+
57
+ async def main():
58
+ async with LimitGuardClient(api_key="lg_live_...") as client:
59
+ result = await client.check_entity("Acme BV", country="NL")
60
+ print(result.trust_score, result.trust_level, result.recommendation)
61
+
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ `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`.
67
+
68
+ ## Paying for calls
69
+
70
+ Two ways to satisfy the API's per-call price, and the client accepts either or both:
71
+
72
+ | You pass | What happens |
73
+ |---|---|
74
+ | `api_key=` on a **paid** tier (`indie` and up) | The subscription covers usage. No per-call payment. |
75
+ | `wallet=` | The client pays each call in USDC over x402 automatically when the API answers 402. |
76
+ | both | Calls are paid by the wallet and attributed to the key. |
77
+
78
+ A `free`-tier key identifies you and tracks usage; it does not pay for calls. On its own it will raise `PaymentRequiredError` on any paid endpoint. Add a wallet, or upgrade the key.
79
+
80
+ Wallet example (Solana, needs the `[solana]` extra and a wallet holding SOL for gas and USDC for payments):
81
+
82
+ ```python
83
+ from solders.keypair import Keypair
84
+ from limitguard import LimitGuardClient
85
+ from limitguard.x402 import SolanaWallet
86
+
87
+ wallet = SolanaWallet(keypair=Keypair.from_base58_string(private_key))
88
+ async with LimitGuardClient(wallet=wallet) as client:
89
+ result = await client.check_entity("Acme BV", country="NL")
90
+ ```
91
+
92
+ A complete agent is in `examples/agent_with_wallet.py`, included in the source distribution (`pip download --no-binary :all: limitguard`).
93
+
94
+ ## Errors
95
+
96
+ All exceptions subclass `LimitGuardError`: `AuthenticationError`, `PaymentRequiredError`, `RateLimitError`, `ValidationError`, `ServerError`. The client retries 429 and 5xx responses with backoff before raising.
97
+
98
+ ## Links
99
+
100
+ - Documentation: https://docs.limitguard.ai
101
+ - Quickstart and a free sandbox key (mock data, no wallet): https://api.limitguard.ai/v1/quickstart
102
+ - x402 protocol: https://docs.limitguard.ai/x402-protocol
103
+ - Issues and support: https://github.com/jwconsultancyteam/limitguard-mcp/issues
104
+
105
+ ## Version
106
+
107
+ ```python
108
+ import limitguard
109
+ print(limitguard.__version__)
110
+ ```
111
+
112
+ Releases are tagged `sdk-v<version>` and published to PyPI from that tag.
@@ -0,0 +1,9 @@
1
+ limitguard/__init__.py,sha256=Ya6sWSRAcCodtSZNXFmDZZj-XLZ6h_56UFgwT-vRWNM,732
2
+ limitguard/client.py,sha256=R80aXQ8Fki5-_DFN26t7XiRsOxBuqQjBSpQvqm8boPk,7671
3
+ limitguard/exceptions.py,sha256=eHokz2TsGgOp6Yg3RNAB-6nDTQ1Skgih_0GJHyXbWCI,1549
4
+ limitguard/models.py,sha256=92wKryKmNKD6OJuqHgecrl2dE-K1rVqdmo2TLmO22JU,1525
5
+ limitguard/x402.py,sha256=UmkhPqo43v183KAvAUDbatIAAOtUAJ6IQ5JVDbsyhQ8,8504
6
+ limitguard-0.1.0.dist-info/METADATA,sha256=5JvxubakFp8mflnnJRday_BDY516j5I9kBjkqsaLAb8,3991
7
+ limitguard-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
8
+ limitguard-0.1.0.dist-info/licenses/LICENSE,sha256=YxjQ2QGysa2SgAXBO7C8G_ZNukmyLgNkYnogIL2betI,1082
9
+ limitguard-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ambulatio Consulting B.V.
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.