token-verdict-tools 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.
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: read
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Check out repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Install package and test dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ python -m pip install -e ".[all,dev]"
26
+
27
+ - name: Test package
28
+ run: python -m pytest
29
+
30
+ - name: Build package
31
+ run: |
32
+ python -m pip install --upgrade build
33
+ python -m build
34
+
35
+ - name: Publish package with PyPI trusted publishing
36
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .tmp/
5
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quincy Goertz-Brown
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.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: token-verdict-tools
3
+ Version: 0.1.0
4
+ Summary: Token Verdict tools for LangChain and CrewAI - pay-per-call token risk verdicts (SOLID/CAUTION/DANGER) and pre-trade checks (PROCEED/CAUTION/ABORT) via x402 USDC payments on Base.
5
+ Project-URL: Homepage, https://github.com/QuincyGB/token-verdict
6
+ Project-URL: Repository, https://github.com/QuincyGB/token-verdict
7
+ Author: QuincyGB
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,base,crewai,langchain,risk,token,web3,x402
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: eth-account>=0.13
18
+ Requires-Dist: pydantic>=2
19
+ Requires-Dist: requests>=2.32
20
+ Requires-Dist: x402[evm,requests]>=2.4.0
21
+ Provides-Extra: all
22
+ Requires-Dist: crewai>=0.80; extra == 'all'
23
+ Requires-Dist: langchain-core>=0.3; extra == 'all'
24
+ Provides-Extra: crewai
25
+ Requires-Dist: crewai>=0.80; extra == 'crewai'
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Provides-Extra: langchain
29
+ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # token-verdict-tools
33
+
34
+ Token Verdict tools for **LangChain** and **CrewAI** — plus the shared,
35
+ framework-free client underneath both.
36
+
37
+ [Token Verdict](https://token-verdict-production.up.railway.app) is a
38
+ pay-per-call token risk service. Send a token contract address, get back a
39
+ **SOLID / CAUTION / DANGER** verdict, a 0–100 score, six market-structure
40
+ signals (liquidity depth, volume churn, sell pressure, price action, trader
41
+ breadth, pair age) and a narrative take. A second route, the **pre-trade
42
+ check**, combines the token verdict with optional wallet and allowance legs
43
+ into one **PROCEED / CAUTION / ABORT** decision — the check to run before an
44
+ agent spends.
45
+
46
+ Calls are paid per use with [x402](https://x402.org): USDC on Base, $0.02
47
+ per verdict, $0.05 per pre-trade check. There are no API keys and no
48
+ accounts — the HTTP 402 challenge *is* the checkout.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install token-verdict-tools[langchain] # LangChain tools
54
+ pip install token-verdict-tools[crewai] # CrewAI tools
55
+ pip install token-verdict-tools # just the shared client
56
+ ```
57
+
58
+ ## You need a payer account
59
+
60
+ Neither framework has a wallet provider, so the tools take an
61
+ [`eth_account`](https://eth-account.readthedocs.io) local account at
62
+ construction. The account must hold a little USDC on **Base mainnet** — it
63
+ signs an EIP-3009 authorization per call and the service settles it. Use a
64
+ dedicated, small-balance agent wallet.
65
+
66
+ ```python
67
+ import os
68
+ from eth_account import Account
69
+
70
+ account = Account.from_key(os.environ["AGENT_PRIVATE_KEY"])
71
+ ```
72
+
73
+ Every call is spend-guarded: the client only signs a payment if the
74
+ challenge asks for USDC on Base at or under the route's cap — **$0.02** for
75
+ a verdict, **$0.05** for a pre-trade check by default. A repriced endpoint is
76
+ refused, never silently paid. Raise a cap explicitly via
77
+ `max_verdict_amount=` / `max_pre_trade_amount=` if the service's price ever
78
+ changes.
79
+
80
+ ## LangChain
81
+
82
+ ```python
83
+ from token_verdict_tools.langchain_tools import PreTradeCheckTool, TokenVerdictTool
84
+
85
+ tools = [TokenVerdictTool(account=account), PreTradeCheckTool(account=account)]
86
+ # ...hand `tools` to any LangChain agent as usual.
87
+
88
+ TokenVerdictTool(account=account).invoke({"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"})
89
+ ```
90
+
91
+ Tools return the service's JSON. Failures raise `ToolException` with
92
+ `handle_tool_error` on, so `invoke` returns an `"Error getting token
93
+ verdict: …"` message to the agent instead of crashing the run — a verdict is
94
+ never fabricated.
95
+
96
+ ## CrewAI
97
+
98
+ ```python
99
+ from token_verdict_tools.crewai_tools import PreTradeCheckTool, TokenVerdictTool
100
+
101
+ agent = Agent(role="Risk checker", goal="Vet tokens before any trade",
102
+ tools=[TokenVerdictTool(account=account), PreTradeCheckTool(account=account)])
103
+ ```
104
+
105
+ Same contract, CrewAI idiom: `_run` returns the JSON string on success and
106
+ an `"Error …"` string on failure.
107
+
108
+ ## Just the client
109
+
110
+ ```python
111
+ from token_verdict_tools import TokenVerdictClient
112
+
113
+ client = TokenVerdictClient(account=account)
114
+ client.verdict("0x...") # $0.02
115
+ client.verdict("0x...", chain="ethereum") # Base/ETH/SOL markets
116
+ client.pre_trade("0x...", wallet_address="0x...", spender="0x...") # $0.05
117
+ ```
118
+
119
+ `verdict()` / `pre_trade()` return the response dict and raise
120
+ `TokenVerdictPaymentError` (challenge unpaid — e.g. the spend guard
121
+ refused) or `TokenVerdictRequestError` (any other non-200) on failure.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ pip install -e .[all,dev]
127
+ pytest
128
+ ```
129
+
130
+ Tests are fully offline: HTTP is stubbed at the `requests` adapter layer,
131
+ so the real x402 client, exact EVM scheme and EIP-712 signing run against
132
+ recorded 402 challenges — including a signature-recovery check on the
133
+ payment the client produces. No test makes a live paid call.
@@ -0,0 +1,102 @@
1
+ # token-verdict-tools
2
+
3
+ Token Verdict tools for **LangChain** and **CrewAI** — plus the shared,
4
+ framework-free client underneath both.
5
+
6
+ [Token Verdict](https://token-verdict-production.up.railway.app) is a
7
+ pay-per-call token risk service. Send a token contract address, get back a
8
+ **SOLID / CAUTION / DANGER** verdict, a 0–100 score, six market-structure
9
+ signals (liquidity depth, volume churn, sell pressure, price action, trader
10
+ breadth, pair age) and a narrative take. A second route, the **pre-trade
11
+ check**, combines the token verdict with optional wallet and allowance legs
12
+ into one **PROCEED / CAUTION / ABORT** decision — the check to run before an
13
+ agent spends.
14
+
15
+ Calls are paid per use with [x402](https://x402.org): USDC on Base, $0.02
16
+ per verdict, $0.05 per pre-trade check. There are no API keys and no
17
+ accounts — the HTTP 402 challenge *is* the checkout.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install token-verdict-tools[langchain] # LangChain tools
23
+ pip install token-verdict-tools[crewai] # CrewAI tools
24
+ pip install token-verdict-tools # just the shared client
25
+ ```
26
+
27
+ ## You need a payer account
28
+
29
+ Neither framework has a wallet provider, so the tools take an
30
+ [`eth_account`](https://eth-account.readthedocs.io) local account at
31
+ construction. The account must hold a little USDC on **Base mainnet** — it
32
+ signs an EIP-3009 authorization per call and the service settles it. Use a
33
+ dedicated, small-balance agent wallet.
34
+
35
+ ```python
36
+ import os
37
+ from eth_account import Account
38
+
39
+ account = Account.from_key(os.environ["AGENT_PRIVATE_KEY"])
40
+ ```
41
+
42
+ Every call is spend-guarded: the client only signs a payment if the
43
+ challenge asks for USDC on Base at or under the route's cap — **$0.02** for
44
+ a verdict, **$0.05** for a pre-trade check by default. A repriced endpoint is
45
+ refused, never silently paid. Raise a cap explicitly via
46
+ `max_verdict_amount=` / `max_pre_trade_amount=` if the service's price ever
47
+ changes.
48
+
49
+ ## LangChain
50
+
51
+ ```python
52
+ from token_verdict_tools.langchain_tools import PreTradeCheckTool, TokenVerdictTool
53
+
54
+ tools = [TokenVerdictTool(account=account), PreTradeCheckTool(account=account)]
55
+ # ...hand `tools` to any LangChain agent as usual.
56
+
57
+ TokenVerdictTool(account=account).invoke({"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"})
58
+ ```
59
+
60
+ Tools return the service's JSON. Failures raise `ToolException` with
61
+ `handle_tool_error` on, so `invoke` returns an `"Error getting token
62
+ verdict: …"` message to the agent instead of crashing the run — a verdict is
63
+ never fabricated.
64
+
65
+ ## CrewAI
66
+
67
+ ```python
68
+ from token_verdict_tools.crewai_tools import PreTradeCheckTool, TokenVerdictTool
69
+
70
+ agent = Agent(role="Risk checker", goal="Vet tokens before any trade",
71
+ tools=[TokenVerdictTool(account=account), PreTradeCheckTool(account=account)])
72
+ ```
73
+
74
+ Same contract, CrewAI idiom: `_run` returns the JSON string on success and
75
+ an `"Error …"` string on failure.
76
+
77
+ ## Just the client
78
+
79
+ ```python
80
+ from token_verdict_tools import TokenVerdictClient
81
+
82
+ client = TokenVerdictClient(account=account)
83
+ client.verdict("0x...") # $0.02
84
+ client.verdict("0x...", chain="ethereum") # Base/ETH/SOL markets
85
+ client.pre_trade("0x...", wallet_address="0x...", spender="0x...") # $0.05
86
+ ```
87
+
88
+ `verdict()` / `pre_trade()` return the response dict and raise
89
+ `TokenVerdictPaymentError` (challenge unpaid — e.g. the spend guard
90
+ refused) or `TokenVerdictRequestError` (any other non-200) on failure.
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ pip install -e .[all,dev]
96
+ pytest
97
+ ```
98
+
99
+ Tests are fully offline: HTTP is stubbed at the `requests` adapter layer,
100
+ so the real x402 client, exact EVM scheme and EIP-712 signing run against
101
+ recorded 402 challenges — including a signature-recovery check on the
102
+ payment the client produces. No test makes a live paid call.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "token-verdict-tools"
7
+ version = "0.1.0"
8
+ description = "Token Verdict tools for LangChain and CrewAI - pay-per-call token risk verdicts (SOLID/CAUTION/DANGER) and pre-trade checks (PROCEED/CAUTION/ABORT) via x402 USDC payments on Base."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "QuincyGB" }]
13
+ keywords = ["langchain", "crewai", "agents", "x402", "token", "risk", "base", "web3"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = [
22
+ "eth-account>=0.13",
23
+ "pydantic>=2",
24
+ "requests>=2.32",
25
+ "x402[evm,requests]>=2.4.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ langchain = ["langchain-core>=0.3"]
30
+ crewai = ["crewai>=0.80"]
31
+ all = ["langchain-core>=0.3", "crewai>=0.80"]
32
+ dev = ["pytest>=8"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/QuincyGB/token-verdict"
36
+ Repository = "https://github.com/QuincyGB/token-verdict"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/token_verdict_tools"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src"]
@@ -0,0 +1,54 @@
1
+ """Token Verdict tools for LangChain and CrewAI.
2
+
3
+ This package's core is the shared, framework-free client:
4
+
5
+ from eth_account import Account
6
+ from token_verdict_tools import TokenVerdictClient
7
+
8
+ client = TokenVerdictClient(account=Account.from_key(...))
9
+ client.verdict("0x...") # POST /v1/verdict, $0.02 USDC on Base
10
+ client.pre_trade("0x...", wallet_address="0x...") # $0.05
11
+
12
+ The framework tools live in submodules so importing the client never drags
13
+ a framework in:
14
+
15
+ from token_verdict_tools.langchain_tools import TokenVerdictTool
16
+ from token_verdict_tools.crewai_tools import PreTradeCheckTool
17
+ """
18
+
19
+ from .client import TokenVerdictClient, TokenVerdictOptions, usd_to_atomic
20
+ from .constants import (
21
+ BASE_NETWORK,
22
+ BASE_USDC_ADDRESS,
23
+ TOKEN_VERDICT_BASE_URL,
24
+ TOKEN_VERDICT_PATH,
25
+ TOKEN_VERDICT_PRE_TRADE_PATH,
26
+ TOKEN_VERDICT_PRE_TRADE_PRICE,
27
+ TOKEN_VERDICT_PRICE,
28
+ )
29
+ from .errors import (
30
+ TokenVerdictError,
31
+ TokenVerdictPaymentError,
32
+ TokenVerdictRequestError,
33
+ )
34
+ from .schemas import PreTradeCheckInput, TokenVerdictInput
35
+
36
+ __all__ = [
37
+ "BASE_NETWORK",
38
+ "BASE_USDC_ADDRESS",
39
+ "TOKEN_VERDICT_BASE_URL",
40
+ "TOKEN_VERDICT_PATH",
41
+ "TOKEN_VERDICT_PRE_TRADE_PATH",
42
+ "TOKEN_VERDICT_PRE_TRADE_PRICE",
43
+ "TOKEN_VERDICT_PRICE",
44
+ "PreTradeCheckInput",
45
+ "TokenVerdictClient",
46
+ "TokenVerdictError",
47
+ "TokenVerdictInput",
48
+ "TokenVerdictOptions",
49
+ "TokenVerdictPaymentError",
50
+ "TokenVerdictRequestError",
51
+ "usd_to_atomic",
52
+ ]
53
+
54
+ __version__ = "0.1.0"
@@ -0,0 +1,265 @@
1
+ """Shared Token Verdict client for the LangChain and CrewAI tools.
2
+
3
+ One client serves both frameworks. It reuses the x402 client approach from
4
+ the AgentKit Python action provider (coinbase/agentkit PR #1599): an
5
+ ``x402ClientSync`` with the exact EVM scheme registered over an
6
+ ``EthAccountSigner``, guarded by a payment policy that only lets USDC-on-Base
7
+ requirements at or under the per-route cap through to signing.
8
+
9
+ Unlike AgentKit, LangChain and CrewAI have no wallet-provider abstraction,
10
+ so the signer is an ``eth_account`` local account supplied by the user. All
11
+ signing goes through that account via the x402 SDK's signer bridge; this
12
+ package never handles raw key material beyond holding the account object the
13
+ user passed in.
14
+ """
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ import requests
21
+ from eth_account.signers.local import LocalAccount
22
+ from x402 import x402ClientSync
23
+ from x402.client_base import RequirementsView
24
+ from x402.http.clients.requests import wrapRequestsWithPayment
25
+ from x402.mechanisms.evm import EthAccountSigner
26
+ from x402.mechanisms.evm.exact.register import register_exact_evm_client
27
+
28
+ from .constants import (
29
+ BASE_NETWORK,
30
+ BASE_USDC_ADDRESS,
31
+ TOKEN_VERDICT_BASE_URL,
32
+ TOKEN_VERDICT_PATH,
33
+ TOKEN_VERDICT_PRE_TRADE_PATH,
34
+ TOKEN_VERDICT_PRE_TRADE_PRICE,
35
+ TOKEN_VERDICT_PRICE,
36
+ USDC_DECIMALS,
37
+ )
38
+ from .errors import TokenVerdictPaymentError, TokenVerdictRequestError
39
+
40
+ ADDRESS_PATTERN = re.compile(r"^0x[a-fA-F0-9]{40}$")
41
+ USD_AMOUNT_PATTERN = re.compile(r"^\$?(\d+)(?:\.(\d+))?$")
42
+
43
+ # Atomic USDC units per dollar (10 ** USDC_DECIMALS).
44
+ ATOMIC_PER_DOLLAR: int = 10**USDC_DECIMALS
45
+
46
+ SUPPORTED_CHAINS = ("base", "ethereum", "solana")
47
+
48
+
49
+ def usd_to_atomic(amount: str) -> int:
50
+ """Convert a USD amount string (e.g. "$0.02") to USDC atomic units.
51
+
52
+ Args:
53
+ amount: The USD amount string to convert.
54
+
55
+ Returns:
56
+ The amount in USDC atomic units (6 decimals).
57
+
58
+ Raises:
59
+ ValueError: If the amount string is not a valid USD amount.
60
+ """
61
+ match = USD_AMOUNT_PATTERN.match(amount.strip())
62
+ if not match:
63
+ raise ValueError(f"Invalid USD amount: {amount}")
64
+
65
+ dollars: str = match.group(1)
66
+ fraction: str = match.group(2) or ""
67
+ if len(fraction) > USDC_DECIMALS:
68
+ raise ValueError(
69
+ f"USD amount has too many decimal places (max {USDC_DECIMALS}): {amount}"
70
+ )
71
+
72
+ return int(dollars) * ATOMIC_PER_DOLLAR + int(fraction.ljust(USDC_DECIMALS, "0") or "0")
73
+
74
+
75
+ @dataclass
76
+ class TokenVerdictOptions:
77
+ """Options for the Token Verdict client.
78
+
79
+ Attributes:
80
+ base_url: Base URL of the Token Verdict service.
81
+ max_verdict_amount: Spend guard for /v1/verdict - the maximum USD
82
+ amount the client will sign for in one verdict call. Defaults to
83
+ the service's known price.
84
+ max_pre_trade_amount: Spend guard for /v1/pre-trade, likewise.
85
+ session: Optional pre-configured requests session (test seam,
86
+ mirroring the AgentKit provider's session override).
87
+ """
88
+
89
+ base_url: str = TOKEN_VERDICT_BASE_URL
90
+ max_verdict_amount: str = TOKEN_VERDICT_PRICE
91
+ max_pre_trade_amount: str = TOKEN_VERDICT_PRE_TRADE_PRICE
92
+ session: requests.Session | None = None
93
+
94
+
95
+ @dataclass
96
+ class TokenVerdictClient:
97
+ """Pays Token Verdict's x402 charges from a user-supplied account.
98
+
99
+ Attributes:
100
+ account: The eth_account local account that signs (and funds) the
101
+ USDC payments on Base.
102
+ options: Service URL and spend-guard configuration.
103
+ """
104
+
105
+ account: LocalAccount
106
+ options: TokenVerdictOptions = field(default_factory=TokenVerdictOptions)
107
+ _active_cap_atomic: int = field(init=False, repr=False)
108
+
109
+ def __post_init__(self) -> None:
110
+ # The cap the spend-guard policy enforces for the in-flight call.
111
+ # The requests stack is synchronous, so one active cap per client is
112
+ # exact; it defaults to the lower (verdict) cap.
113
+ self._active_cap_atomic = usd_to_atomic(self.options.max_verdict_amount)
114
+
115
+ def _spend_guard_policy(
116
+ self, version: int, options: list[RequirementsView]
117
+ ) -> list[RequirementsView]:
118
+ """Filter a challenge's payment options down to payable ones.
119
+
120
+ Mirrors the AgentKit providers' PaymentPolicy: an option is payable
121
+ only if it asks for USDC on Base at or under the active route's cap.
122
+ Anything else is filtered out before signing, so the x402 client
123
+ refuses instead of paying.
124
+ """
125
+ del version
126
+ return [
127
+ option
128
+ for option in options
129
+ if str(option.network) == BASE_NETWORK
130
+ and option.asset.lower() == BASE_USDC_ADDRESS.lower()
131
+ and int(option.get_amount()) <= self._active_cap_atomic
132
+ ]
133
+
134
+ def _create_paying_session(self) -> requests.Session:
135
+ """Create a requests session that pays x402 challenges automatically."""
136
+ client = x402ClientSync()
137
+ register_exact_evm_client(
138
+ client,
139
+ EthAccountSigner(self.account),
140
+ policies=[self._spend_guard_policy],
141
+ )
142
+ session = self.options.session or requests.Session()
143
+ return wrapRequestsWithPayment(session, client)
144
+
145
+ def _post(self, path: str, body: dict[str, Any], cap_usd: str) -> dict[str, Any]:
146
+ """POST to a paid route, paying its challenge under the route's cap.
147
+
148
+ Raises:
149
+ TokenVerdictPaymentError: If the challenge stays unpaid (402) -
150
+ e.g. the spend guard filtered every payment option out.
151
+ TokenVerdictRequestError: On any other non-200 response or a
152
+ malformed success body.
153
+ """
154
+ self._active_cap_atomic = usd_to_atomic(cap_usd)
155
+ try:
156
+ session = self._create_paying_session()
157
+ try:
158
+ response = session.post(f"{self.options.base_url}{path}", json=body)
159
+ except Exception as e:
160
+ # The x402 client raises (rather than returning the 402)
161
+ # when the spend-guard policy filters every payment option
162
+ # out, and for other payment-construction failures. Those
163
+ # are payment failures, not request failures.
164
+ if "payment" in str(e).lower():
165
+ raise TokenVerdictPaymentError(
166
+ f"Token Verdict payment failed: {e}"
167
+ ) from e
168
+ raise TokenVerdictRequestError(
169
+ f"Token Verdict request failed: {e}"
170
+ ) from e
171
+ finally:
172
+ self._active_cap_atomic = usd_to_atomic(self.options.max_verdict_amount)
173
+
174
+ if response.status_code == 402:
175
+ raise TokenVerdictPaymentError(
176
+ "Token Verdict payment failed: the x402 challenge could not be "
177
+ f"paid within the spend guard (cap {cap_usd} USDC on Base). "
178
+ "The endpoint may have been repriced, or the account may lack "
179
+ "USDC / a settlement route."
180
+ )
181
+ if response.status_code != 200:
182
+ body_text = response.text[:500]
183
+ raise TokenVerdictRequestError(
184
+ f"Token Verdict request failed: HTTP {response.status_code}: {body_text}"
185
+ )
186
+
187
+ data = response.json()
188
+ if not isinstance(data, dict):
189
+ raise TokenVerdictRequestError(
190
+ "Token Verdict request failed: response was not a JSON object"
191
+ )
192
+ return data
193
+
194
+ def verdict(self, address: str, chain: str = "base") -> dict[str, Any]:
195
+ """Get a risk verdict for a token (POST /v1/verdict, $0.02).
196
+
197
+ Args:
198
+ address: The token contract address (0x-prefixed, 40 hex chars).
199
+ chain: Which chain's market to analyze: "base" (default),
200
+ "ethereum", or "solana". Payment always settles on Base.
201
+
202
+ Returns:
203
+ The service's verdict response: SOLID / CAUTION / DANGER, a 0-100
204
+ score, the six market-structure signals, and a narrative take.
205
+
206
+ Raises:
207
+ ValueError: If the address or chain is malformed.
208
+ TokenVerdictError: If the call or its payment fails.
209
+ """
210
+ if not ADDRESS_PATTERN.match(address or ""):
211
+ raise ValueError("address must be a 0x token contract address")
212
+ if chain not in SUPPORTED_CHAINS:
213
+ raise ValueError(f"chain must be one of {SUPPORTED_CHAINS}")
214
+ return self._post(
215
+ TOKEN_VERDICT_PATH,
216
+ {"address": address, "chain": chain},
217
+ self.options.max_verdict_amount,
218
+ )
219
+
220
+ def pre_trade(
221
+ self,
222
+ token_address: str,
223
+ wallet_address: str | None = None,
224
+ spender: str | None = None,
225
+ chain: str = "base",
226
+ ) -> dict[str, Any]:
227
+ """Run the combined pre-trade check (POST /v1/pre-trade, $0.05).
228
+
229
+ Combines the token verdict with optional wallet (sanctions, age,
230
+ poisoning) and allowance legs into one PROCEED / CAUTION / ABORT
231
+ decision - the check to run before an agent spends.
232
+
233
+ Args:
234
+ token_address: The token contract the trade would buy or receive.
235
+ wallet_address: Optional Base wallet to check.
236
+ spender: Optional spender contract; with wallet_address, adds
237
+ the allowance leg.
238
+ chain: Chain for the token leg (default "base"); wallet and
239
+ allowance legs are Base-only, enforced by the service.
240
+
241
+ Returns:
242
+ The service's combined decision response.
243
+
244
+ Raises:
245
+ ValueError: If an address is malformed, or spender is given
246
+ without wallet_address.
247
+ TokenVerdictError: If the call or its payment fails.
248
+ """
249
+ if not ADDRESS_PATTERN.match(token_address or ""):
250
+ raise ValueError("token_address must be a 0x contract address")
251
+ if wallet_address is not None and not ADDRESS_PATTERN.match(wallet_address):
252
+ raise ValueError("wallet_address must be a 0x address")
253
+ if spender is not None and not ADDRESS_PATTERN.match(spender):
254
+ raise ValueError("spender must be a 0x contract address")
255
+ if spender is not None and wallet_address is None:
256
+ raise ValueError("spender requires wallet_address")
257
+
258
+ body: dict[str, Any] = {"token_address": token_address, "chain": chain}
259
+ if wallet_address is not None:
260
+ body["wallet_address"] = wallet_address
261
+ if spender is not None:
262
+ body["spender"] = spender
263
+ return self._post(
264
+ TOKEN_VERDICT_PRE_TRADE_PATH, body, self.options.max_pre_trade_amount
265
+ )
@@ -0,0 +1,17 @@
1
+ """Constants for the Token Verdict tools package."""
2
+
3
+ # The live Token Verdict service (https://github.com/QuincyGB/token-verdict).
4
+ TOKEN_VERDICT_BASE_URL = "https://token-verdict-production.up.railway.app"
5
+ TOKEN_VERDICT_PATH = "/v1/verdict"
6
+ TOKEN_VERDICT_PRE_TRADE_PATH = "/v1/pre-trade"
7
+
8
+ # The service's known prices. Also the default spend-guard caps: the client
9
+ # never signs a payment above the cap for the route being called, so a
10
+ # repriced or hostile endpoint can never silently charge the agent more.
11
+ TOKEN_VERDICT_PRICE = "$0.02"
12
+ TOKEN_VERDICT_PRE_TRADE_PRICE = "$0.05"
13
+
14
+ # Payment always settles in USDC on Base, whichever chain's token is analyzed.
15
+ BASE_NETWORK = "eip155:8453"
16
+ BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
17
+ USDC_DECIMALS = 6