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.
- token_verdict_tools-0.1.0/.github/workflows/publish.yml +36 -0
- token_verdict_tools-0.1.0/.gitignore +5 -0
- token_verdict_tools-0.1.0/LICENSE +21 -0
- token_verdict_tools-0.1.0/PKG-INFO +133 -0
- token_verdict_tools-0.1.0/README.md +102 -0
- token_verdict_tools-0.1.0/pyproject.toml +43 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/__init__.py +54 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/client.py +265 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/constants.py +17 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/crewai_tools.py +143 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/descriptions.py +25 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/errors.py +19 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/langchain_tools.py +146 -0
- token_verdict_tools-0.1.0/src/token_verdict_tools/schemas.py +89 -0
- token_verdict_tools-0.1.0/tests/conftest.py +154 -0
- token_verdict_tools-0.1.0/tests/test_client.py +250 -0
- token_verdict_tools-0.1.0/tests/test_tools.py +163 -0
|
@@ -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,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
|