fizzl 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.
fizzl-0.1.0/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
fizzl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frits Zwager
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.
fizzl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.5
2
+ Name: fizzl
3
+ Version: 0.1.0
4
+ Summary: Safety checks for AI agents that pay: check a transaction or signature before signing, a token before buying, a wallet's approvals, and an x402 endpoint before paying it (presign-guard, x402 Doctor).
5
+ Project-URL: Homepage, https://github.com/Fizzl13/x402-examples/tree/main/fizzl-py
6
+ Project-URL: Repository, https://github.com/Fizzl13/x402-examples
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,ai-agents,approvals,base,honeypot,langchain,mcp,presign,security,solana,token-check,usdc,wallet,x402
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: requests>=2.28
15
+ Provides-Extra: langchain
16
+ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
17
+ Requires-Dist: pydantic>=2; extra == 'langchain'
18
+ Provides-Extra: x402
19
+ Requires-Dist: x402[requests]>=2.0; extra == 'x402'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # fizzl
23
+
24
+ Safety checks for Python AI agents that pay: LangChain, LangGraph, CrewAI or your own loop.
25
+
26
+ | Check | What the agent checks | Service | Price |
27
+ |---|---|---|---|
28
+ | `check_before_signing` | a transaction, token approval or signature **before signing it**: green / orange / red with reason codes (drainers, unlimited approvals to unknown spenders, look-alike tokens, Permit/Permit2/Seaport signatures that hand over tokens) | [presign-guard](https://presign-guard.fizzl.eu) | $0.01 |
29
+ | `check_token` | a token **before buying or accepting it**: honeypot, rug-pull signs, look-alikes (Solana and EVM) | presign-guard | $0.01 |
30
+ | `check_wallet_approvals` | every open approval of a wallet and which to revoke | presign-guard | $0.02 |
31
+ | `check_endpoint_before_paying` | an x402 or MPP paid API **before paying it**: go / caution / no_go, the cheapest option that settles, budget, track record, bait signs (fake brands, airdrop lures, output that doesn't match) | [x402 Doctor](https://x402-doctor.fizzl.eu) | $0.001 |
32
+
33
+ The checks only check. They never sign, pay or move anything themselves. A failed check comes back as
34
+ `{"error": ..., "message": ...}` instead of raising, so the agent can tell its user.
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ pip install "fizzl[x402]" # with the x402 client, to pay per check
40
+ pip install "fizzl[langchain,x402]" # plus LangChain tools
41
+ ```
42
+
43
+ ## Use
44
+
45
+ ```python
46
+ import requests
47
+ from eth_account import Account
48
+ from x402 import x402ClientSync
49
+ from x402.http.clients import wrapRequestsWithPayment
50
+ from x402.mechanisms.evm.exact import ExactEvmScheme
51
+ from fizzl import Fizzl
52
+
53
+ payer = x402ClientSync().register("eip155:8453", ExactEvmScheme(Account.from_key(AGENT_KEY))) # USDC on Base
54
+ session = wrapRequestsWithPayment(requests.Session(), payer)
55
+
56
+ fizzl = Fizzl(session=session)
57
+ fizzl.check_endpoint_before_paying("https://api.example.com/funding", max_usd=0.05)
58
+ # {'verdict': 'go', 'summary': 'OK to pay: $0.02 on Base.', 'options': [...], 'reasons': [], ...}
59
+ fizzl.check_token("base", "0x...")
60
+ fizzl.check_before_signing(type="approval", chainId=8453, token="0x...", spender="0x...", amount="115792089237316195423570985008687907853269984665640564039457584007913129639935")
61
+ ```
62
+
63
+ **Prepaid credits instead of a payment per check:** buy a pack once ([presign-guard](https://presign-guard.fizzl.eu/v1/credits): 100 checks for $0.80; [x402 Doctor](https://x402-doctor.fizzl.eu/api/v1/credits): 1000 preflights for $0.80), then use a plain session:
64
+
65
+ ```python
66
+ fizzl = Fizzl(credit_keys={"presign": PRESIGN_CREDIT_KEY, "doctor": DOCTOR_CREDIT_KEY})
67
+ ```
68
+
69
+ ## LangChain / LangGraph
70
+
71
+ ```python
72
+ from langgraph.prebuilt import create_react_agent
73
+ from fizzl.langchain import fizzl_tools
74
+
75
+ tools = fizzl_tools(session=session) # or credit_keys={...}; only=[...] for some of them
76
+ agent = create_react_agent(model, tools + your_tools,
77
+ prompt="Before you pay any API, call check_endpoint_before_paying. Before you sign anything, call check_before_signing. Never continue on red or no_go.")
78
+ ```
79
+
80
+ The tools are plain `StructuredTool`s with typed arguments, so CrewAI and other frameworks that take LangChain tools can use them too.
81
+
82
+ ## Want the wallet to enforce it, not just inform the model?
83
+
84
+ Tools inform the model; a model can still ignore them. To make red and over-budget payments impossible, guard the
85
+ wallet itself: [presign-guard-wallet](https://www.npmjs.com/package/presign-guard-wallet) (Node) checks every
86
+ signature with presign-guard and your spending limits, with Telegram approval above them.
87
+
88
+ ## License
89
+
90
+ MIT
fizzl-0.1.0/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # fizzl
2
+
3
+ Safety checks for Python AI agents that pay: LangChain, LangGraph, CrewAI or your own loop.
4
+
5
+ | Check | What the agent checks | Service | Price |
6
+ |---|---|---|---|
7
+ | `check_before_signing` | a transaction, token approval or signature **before signing it**: green / orange / red with reason codes (drainers, unlimited approvals to unknown spenders, look-alike tokens, Permit/Permit2/Seaport signatures that hand over tokens) | [presign-guard](https://presign-guard.fizzl.eu) | $0.01 |
8
+ | `check_token` | a token **before buying or accepting it**: honeypot, rug-pull signs, look-alikes (Solana and EVM) | presign-guard | $0.01 |
9
+ | `check_wallet_approvals` | every open approval of a wallet and which to revoke | presign-guard | $0.02 |
10
+ | `check_endpoint_before_paying` | an x402 or MPP paid API **before paying it**: go / caution / no_go, the cheapest option that settles, budget, track record, bait signs (fake brands, airdrop lures, output that doesn't match) | [x402 Doctor](https://x402-doctor.fizzl.eu) | $0.001 |
11
+
12
+ The checks only check. They never sign, pay or move anything themselves. A failed check comes back as
13
+ `{"error": ..., "message": ...}` instead of raising, so the agent can tell its user.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pip install "fizzl[x402]" # with the x402 client, to pay per check
19
+ pip install "fizzl[langchain,x402]" # plus LangChain tools
20
+ ```
21
+
22
+ ## Use
23
+
24
+ ```python
25
+ import requests
26
+ from eth_account import Account
27
+ from x402 import x402ClientSync
28
+ from x402.http.clients import wrapRequestsWithPayment
29
+ from x402.mechanisms.evm.exact import ExactEvmScheme
30
+ from fizzl import Fizzl
31
+
32
+ payer = x402ClientSync().register("eip155:8453", ExactEvmScheme(Account.from_key(AGENT_KEY))) # USDC on Base
33
+ session = wrapRequestsWithPayment(requests.Session(), payer)
34
+
35
+ fizzl = Fizzl(session=session)
36
+ fizzl.check_endpoint_before_paying("https://api.example.com/funding", max_usd=0.05)
37
+ # {'verdict': 'go', 'summary': 'OK to pay: $0.02 on Base.', 'options': [...], 'reasons': [], ...}
38
+ fizzl.check_token("base", "0x...")
39
+ fizzl.check_before_signing(type="approval", chainId=8453, token="0x...", spender="0x...", amount="115792089237316195423570985008687907853269984665640564039457584007913129639935")
40
+ ```
41
+
42
+ **Prepaid credits instead of a payment per check:** buy a pack once ([presign-guard](https://presign-guard.fizzl.eu/v1/credits): 100 checks for $0.80; [x402 Doctor](https://x402-doctor.fizzl.eu/api/v1/credits): 1000 preflights for $0.80), then use a plain session:
43
+
44
+ ```python
45
+ fizzl = Fizzl(credit_keys={"presign": PRESIGN_CREDIT_KEY, "doctor": DOCTOR_CREDIT_KEY})
46
+ ```
47
+
48
+ ## LangChain / LangGraph
49
+
50
+ ```python
51
+ from langgraph.prebuilt import create_react_agent
52
+ from fizzl.langchain import fizzl_tools
53
+
54
+ tools = fizzl_tools(session=session) # or credit_keys={...}; only=[...] for some of them
55
+ agent = create_react_agent(model, tools + your_tools,
56
+ prompt="Before you pay any API, call check_endpoint_before_paying. Before you sign anything, call check_before_signing. Never continue on red or no_go.")
57
+ ```
58
+
59
+ The tools are plain `StructuredTool`s with typed arguments, so CrewAI and other frameworks that take LangChain tools can use them too.
60
+
61
+ ## Want the wallet to enforce it, not just inform the model?
62
+
63
+ Tools inform the model; a model can still ignore them. To make red and over-budget payments impossible, guard the
64
+ wallet itself: [presign-guard-wallet](https://www.npmjs.com/package/presign-guard-wallet) (Node) checks every
65
+ signature with presign-guard and your spending limits, with Telegram approval above them.
66
+
67
+ ## License
68
+
69
+ MIT
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fizzl"
7
+ version = "0.1.0"
8
+ description = "Safety checks for AI agents that pay: check a transaction or signature before signing, a token before buying, a wallet's approvals, and an x402 endpoint before paying it (presign-guard, x402 Doctor)."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ dependencies = ["requests>=2.28"]
14
+ keywords = ["x402", "ai-agents", "agents", "langchain", "wallet", "security", "presign", "token-check", "honeypot", "approvals", "usdc", "base", "solana", "mcp"]
15
+ classifiers = ["Programming Language :: Python :: 3", "Intended Audience :: Developers", "Topic :: Security"]
16
+
17
+ [project.optional-dependencies]
18
+ langchain = ["langchain-core>=0.3", "pydantic>=2"]
19
+ x402 = ["x402[requests]>=2.0"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/Fizzl13/x402-examples/tree/main/fizzl-py"
23
+ Repository = "https://github.com/Fizzl13/x402-examples"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/fizzl"]
@@ -0,0 +1,122 @@
1
+ """Fizzl safety checks for AI agents that pay.
2
+
3
+ Check a transaction or signature before signing, a token before buying, a wallet's open approvals, and an
4
+ x402 endpoint before paying it (presign-guard and x402 Doctor, paid per call over x402 or with prepaid
5
+ credits). The checks never sign, pay or move anything themselves, and a failed check comes back as
6
+ ``{"error": ..., "message": ...}`` instead of an exception, so an agent can tell its user.
7
+
8
+ import requests
9
+ from x402 import x402ClientSync
10
+ from x402.http.clients import wrapRequestsWithPayment
11
+ from fizzl import Fizzl
12
+
13
+ payer = x402ClientSync() # register an EVM scheme with your agent's key
14
+ session = wrapRequestsWithPayment(requests.Session(), payer)
15
+ fizzl = Fizzl(session=session)
16
+ fizzl.check_endpoint_before_paying("https://api.example.com/data", max_usd=0.05)
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Mapping, Optional
21
+ from urllib.parse import urlencode
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = ["Fizzl", "PRICES", "PRESIGN_URL", "DOCTOR_URL", "__version__"]
25
+
26
+ PRESIGN_URL = "https://presign-guard.fizzl.eu"
27
+ DOCTOR_URL = "https://x402-doctor.fizzl.eu"
28
+ PRICES = {
29
+ "check_before_signing": "$0.01",
30
+ "check_token": "$0.01",
31
+ "check_wallet_approvals": "$0.02",
32
+ "check_endpoint_before_paying": "$0.001",
33
+ }
34
+ EVM_CHAIN_IDS = (1, 10, 56, 137, 8453, 42161)
35
+ TOKEN_CHAINS = ("solana", "base", "ethereum", "arbitrum", "optimism", "polygon", "bsc")
36
+ APPROVAL_CHAINS = ("base", "ethereum", "arbitrum", "optimism", "polygon", "bsc")
37
+
38
+
39
+ def _error(code: str, message: str) -> dict:
40
+ return {"error": code, "message": message}
41
+
42
+
43
+ class Fizzl:
44
+ """The four checks. ``session`` is anything with ``request(method, url, **kwargs)`` like a
45
+ ``requests.Session``; wrap it with x402's ``wrapRequestsWithPayment`` to pay per check, or pass
46
+ ``credit_keys={"presign": ..., "doctor": ...}`` to pay from prepaid credits with a plain session."""
47
+
48
+ def __init__(self, session: Any = None, credit_keys: Optional[Mapping[str, str]] = None, timeout: float = 30,
49
+ presign_url: str = PRESIGN_URL, doctor_url: str = DOCTOR_URL):
50
+ if session is None:
51
+ import requests # only needed when no session is given
52
+ session = requests.Session()
53
+ self.session = session
54
+ self.credit_keys = dict(credit_keys or {})
55
+ self.timeout = timeout
56
+ self.presign_url = presign_url.rstrip("/")
57
+ self.doctor_url = doctor_url.rstrip("/")
58
+
59
+ def _call(self, service: str, method: str, url: str, body: Optional[dict] = None) -> dict:
60
+ headers = {"accept": "application/json", "user-agent": f"fizzl-py/{__version__}"}
61
+ key = self.credit_keys.get(service)
62
+ if key:
63
+ headers["x-credit-key"] = key
64
+ try:
65
+ res = self.session.request(method, url, headers=headers, timeout=self.timeout, **({"json": body} if body is not None else {}))
66
+ except Exception as err: # network, timeout, payment client errors
67
+ return _error("check_failed", f"{type(err).__name__}: {err}")
68
+ try:
69
+ data = res.json()
70
+ except Exception:
71
+ data = None
72
+ if res.status_code == 402:
73
+ why = data.get("error") if isinstance(data, dict) and isinstance(data.get("error"), str) else None
74
+ return _error("payment_required", "This check is paid (x402) and no payment went through"
75
+ + (f" ({why})" if why else "")
76
+ + ". Pass a session wrapped with x402's wrapRequestsWithPayment whose wallet holds USDC on Base, or a prepaid credit key.")
77
+ if res.status_code >= 400:
78
+ msg = (data or {}).get("error") or (data or {}).get("message") if isinstance(data, dict) else None
79
+ return _error(f"http_{res.status_code}", str(msg or getattr(res, "text", ""))[:300])
80
+ if not isinstance(data, dict):
81
+ return _error("bad_response", "The check did not answer JSON.")
82
+ data.pop("receipt", None) # the signed receipt is for audits, not for the model
83
+ return data
84
+
85
+ def check_before_signing(self, type: str, chainId: int, token: Optional[str] = None, spender: Optional[str] = None,
86
+ amount: Optional[str] = None, to: Optional[str] = None, data: Optional[str] = None,
87
+ value: Optional[str] = None, typedData: Any = None, origin: Optional[str] = None) -> dict:
88
+ """Check a transaction, token approval or signature BEFORE signing it (presign-guard, $0.01).
89
+ Returns verdict green / orange / red with reason codes. Never sign on red; ask the user on orange."""
90
+ if type not in ("approval", "transaction", "signature"):
91
+ return _error("bad_input", 'type must be "approval", "transaction" or "signature"')
92
+ if chainId not in EVM_CHAIN_IDS:
93
+ return _error("bad_input", f"chainId must be one of {EVM_CHAIN_IDS}")
94
+ body = {k: v for k, v in dict(type=type, chainId=chainId, token=token, spender=spender, amount=amount, to=to,
95
+ data=data, value=value, typedData=typedData, origin=origin).items() if v is not None}
96
+ return self._call("presign", "POST", f"{self.presign_url}/v1/check", body)
97
+
98
+ def check_token(self, chain: str, address: str) -> dict:
99
+ """Check a token BEFORE buying, holding or accepting it (presign-guard, $0.01): honeypot, rug-pull
100
+ signs, look-alikes of known tokens. Solana and EVM chains."""
101
+ if chain not in TOKEN_CHAINS:
102
+ return _error("bad_input", f"chain must be one of {TOKEN_CHAINS}")
103
+ return self._call("presign", "GET", f"{self.presign_url}/v1/token?{urlencode({'chain': chain, 'address': address})}")
104
+
105
+ def check_wallet_approvals(self, chain: str, address: str) -> dict:
106
+ """List every open token approval of an EVM wallet and which ones to revoke (presign-guard, $0.02)."""
107
+ if chain not in APPROVAL_CHAINS:
108
+ return _error("bad_input", f"chain must be one of {APPROVAL_CHAINS}")
109
+ return self._call("presign", "GET", f"{self.presign_url}/v1/approvals?{urlencode({'chain': chain, 'address': address})}")
110
+
111
+ def check_endpoint_before_paying(self, url: str, max_usd: Optional[float] = None, network: Optional[str] = None,
112
+ method: Optional[str] = None) -> dict:
113
+ """Check an x402 or MPP paid API BEFORE paying it (x402 Doctor, $0.001): go / caution / no_go, the
114
+ cheapest option that settles, budget, track record and bait signs. Don't pay on no_go."""
115
+ q = {"url": url}
116
+ if max_usd is not None:
117
+ q["max_usd"] = str(max_usd)
118
+ if network:
119
+ q["network"] = network
120
+ if method:
121
+ q["method"] = method
122
+ return self._call("doctor", "GET", f"{self.doctor_url}/api/v1/preflight?{urlencode(q)}")
@@ -0,0 +1,62 @@
1
+ """The Fizzl checks as LangChain tools (``langchain-core``), for LangChain and LangGraph agents.
2
+
3
+ from fizzl.langchain import fizzl_tools
4
+ tools = fizzl_tools(session=paying_session) # or credit_keys={...}
5
+ agent = create_react_agent(model, tools)
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, List, Literal, Optional, Union
10
+
11
+ from . import Fizzl
12
+
13
+ TOOL_NAMES = ("check_before_signing", "check_token", "check_wallet_approvals", "check_endpoint_before_paying")
14
+
15
+
16
+ def fizzl_tools(session: Any = None, credit_keys: Optional[dict] = None, only: Optional[List[str]] = None, **kwargs) -> list:
17
+ """LangChain StructuredTools for the four checks (all of them, or the names in ``only``)."""
18
+ try:
19
+ from langchain_core.tools import StructuredTool
20
+ from pydantic import BaseModel, Field
21
+ except ImportError as err: # pragma: no cover
22
+ raise ImportError("fizzl.langchain needs langchain-core: pip install 'fizzl[langchain]'") from err
23
+
24
+ f = Fizzl(session=session, credit_keys=credit_keys, **kwargs)
25
+
26
+ class SignArgs(BaseModel):
27
+ type: Literal["approval", "transaction", "signature"] = Field(description="What is about to be signed")
28
+ chainId: int = Field(description="EVM chain id: 1, 10, 56, 137, 8453 (Base) or 42161")
29
+ token: Optional[str] = Field(None, description="approval: token contract")
30
+ spender: Optional[str] = Field(None, description="approval: who gets the allowance")
31
+ amount: Optional[str] = Field(None, description="approval: amount in base units (0 = revoke)")
32
+ to: Optional[str] = Field(None, description="transaction: target contract or recipient")
33
+ data: Optional[str] = Field(None, description="transaction: 0x-prefixed calldata")
34
+ value: Optional[str] = Field(None, description="transaction: native value in wei")
35
+ typedData: Optional[Union[dict, str]] = Field(None, description="signature: the eth_signTypedData_v4 payload")
36
+ origin: Optional[str] = Field(None, description="the site asking for the signature or transaction, if any")
37
+
38
+ class TokenArgs(BaseModel):
39
+ chain: Literal["solana", "base", "ethereum", "arbitrum", "optimism", "polygon", "bsc"]
40
+ address: str = Field(description="Solana mint (base58) or EVM token contract (0x…)")
41
+
42
+ class ApprovalArgs(BaseModel):
43
+ chain: Literal["base", "ethereum", "arbitrum", "optimism", "polygon", "bsc"]
44
+ address: str = Field(description="Wallet address (0x…)")
45
+
46
+ class EndpointArgs(BaseModel):
47
+ url: str = Field(description="The paid endpoint you are about to pay")
48
+ max_usd: Optional[float] = Field(None, description="Your budget for this call in USD")
49
+ network: Optional[str] = Field(None, description="CAIP-2 network to pay on, e.g. eip155:8453")
50
+ method: Optional[Literal["GET", "POST"]] = None
51
+
52
+ specs = {
53
+ "check_before_signing": (f.check_before_signing, SignArgs),
54
+ "check_token": (f.check_token, TokenArgs),
55
+ "check_wallet_approvals": (f.check_wallet_approvals, ApprovalArgs),
56
+ "check_endpoint_before_paying": (f.check_endpoint_before_paying, EndpointArgs),
57
+ }
58
+ names = list(only) if only else list(TOOL_NAMES)
59
+ unknown = [n for n in names if n not in specs]
60
+ if unknown:
61
+ raise ValueError(f"unknown Fizzl tool(s): {', '.join(unknown)}")
62
+ return [StructuredTool.from_function(func=specs[n][0], name=n, description=(specs[n][0].__doc__ or "").strip(), args_schema=specs[n][1]) for n in names]
@@ -0,0 +1,74 @@
1
+ """The checks against a stand-in session (no network, no money), and as LangChain tools."""
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from fizzl import PRICES, Fizzl
7
+
8
+
9
+ class Resp:
10
+ def __init__(self, status, body):
11
+ self.status_code = status
12
+ self.text = body if isinstance(body, str) else json.dumps(body)
13
+
14
+ def json(self):
15
+ return json.loads(self.text)
16
+
17
+
18
+ class StandIn:
19
+ def __init__(self, answers):
20
+ self.answers, self.calls = answers, []
21
+
22
+ def request(self, method, url, **kw):
23
+ self.calls.append({"method": method, "url": url, **kw})
24
+ if isinstance(self.answers, Exception):
25
+ raise self.answers
26
+ path = url.split("://", 1)[1].split("/", 1)[1].split("?")[0]
27
+ status, body = self.answers.get("/" + path, (404, {"error": "not found"}))
28
+ return Resp(status, body)
29
+
30
+
31
+ def test_calls_the_right_api_sends_credit_keys_and_leaves_the_receipt_out():
32
+ s = StandIn({"/v1/check": (200, {"verdict": "red", "reasons": [{"code": "known_drainer"}], "receipt": {"sig": "0x"}}),
33
+ "/v1/token": (200, {"verdict": "green"}), "/v1/approvals": (200, {"summary": {"toRevoke": 1}}),
34
+ "/api/v1/preflight": (200, {"verdict": "caution"})})
35
+ f = Fizzl(session=s, credit_keys={"presign": "pk", "doctor": "dk"})
36
+ assert f.check_before_signing(type="approval", chainId=8453, token="0xa", spender="0xb", amount="1") == {"verdict": "red", "reasons": [{"code": "known_drainer"}]}
37
+ assert s.calls[0]["method"] == "POST" and s.calls[0]["url"] == "https://presign-guard.fizzl.eu/v1/check"
38
+ assert s.calls[0]["headers"]["x-credit-key"] == "pk" and s.calls[0]["json"]["spender"] == "0xb" and "data" not in s.calls[0]["json"]
39
+ f.check_token("solana", "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263")
40
+ assert "/v1/token?chain=solana&address=Dez" in s.calls[1]["url"]
41
+ f.check_wallet_approvals("base", "0x6B0F4651eD42893ab58139938175E4a69f175F25")
42
+ assert "/v1/approvals?chain=base&address=0x6B0F" in s.calls[2]["url"]
43
+ assert f.check_endpoint_before_paying("https://api.example.com/x", max_usd=0.05, network="eip155:8453")["verdict"] == "caution"
44
+ assert s.calls[3]["url"] == "https://x402-doctor.fizzl.eu/api/v1/preflight?url=https%3A%2F%2Fapi.example.com%2Fx&max_usd=0.05&network=eip155%3A8453"
45
+ assert s.calls[3]["headers"]["x-credit-key"] == "dk"
46
+
47
+
48
+ def test_failures_come_back_as_error_dicts():
49
+ f = Fizzl(session=StandIn({"/v1/token": (402, {}), "/v1/check": (500, {"error": "boom"}), "/v1/approvals": (200, "not json")}))
50
+ assert f.check_token("base", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")["error"] == "payment_required"
51
+ assert f.check_before_signing(type="transaction", chainId=1, to="0x1") == {"error": "http_500", "message": "boom"}
52
+ assert f.check_wallet_approvals("base", "0x6B0F4651eD42893ab58139938175E4a69f175F25")["error"] == "bad_response"
53
+ assert Fizzl(session=StandIn(ConnectionError("down"))).check_endpoint_before_paying("https://a.example") == {"error": "check_failed", "message": "ConnectionError: down"}
54
+ assert f.check_before_signing(type="approval", chainId=999)["error"] == "bad_input"
55
+ assert f.check_token("dogechain", "x")["error"] == "bad_input"
56
+
57
+
58
+ def test_prices_cover_every_check():
59
+ assert sorted(PRICES) == ["check_before_signing", "check_endpoint_before_paying", "check_token", "check_wallet_approvals"]
60
+
61
+
62
+ def test_langchain_tools_call_the_checks():
63
+ pytest.importorskip("langchain_core")
64
+ from fizzl.langchain import fizzl_tools
65
+ s = StandIn({"/api/v1/preflight": (200, {"verdict": "no_go", "summary": "Do not pay: over budget."})})
66
+ tools = fizzl_tools(session=s)
67
+ assert [t.name for t in tools] == ["check_before_signing", "check_token", "check_wallet_approvals", "check_endpoint_before_paying"]
68
+ pre = next(t for t in tools if t.name == "check_endpoint_before_paying")
69
+ assert "BEFORE paying" in pre.description
70
+ assert pre.invoke({"url": "https://api.example.com/x", "max_usd": 0.01})["verdict"] == "no_go"
71
+ assert "max_usd=0.01" in s.calls[0]["url"]
72
+ assert [t.name for t in fizzl_tools(session=s, only=["check_token"])] == ["check_token"]
73
+ with pytest.raises(ValueError):
74
+ fizzl_tools(session=s, only=["nope"])