swaphunt-mcp 0.4.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,26 @@
1
+ # Secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ venv/
10
+ .venv/
11
+
12
+ # Node
13
+ node_modules/
14
+ dist/
15
+ *.tsbuildinfo
16
+
17
+ # Editors / OS
18
+ .DS_Store
19
+ .idea/
20
+ .vscode/
21
+
22
+ # Local tooling
23
+ .claude/
24
+
25
+ # alert-bot state
26
+ .alert-state.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SwapHunt
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,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: swaphunt-mcp
3
+ Version: 0.4.0
4
+ Summary: MCP server with 30 SwapHunt crypto market tools for AI agents (Hyperliquid prices, regime, volatility, funding, coin strength, anti-FOMO sanity checks; Fear & Greed), paid per call in USDC via x402
5
+ Project-URL: Homepage, https://x402.swaphunt.dev/docs
6
+ Project-URL: Source, https://github.com/swaphunt/x402-examples/tree/main/mcp-server
7
+ Project-URL: Issues, https://github.com/swaphunt/x402-examples/issues
8
+ Author-email: SwapHunt <github@swaphunt.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,base,crypto,market-data,mcp,usdc,x402
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: mcp==2.3.0
18
+ Requires-Dist: x402[evm,httpx]==2.25.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # SwapHunt MCP server
22
+
23
+ <!-- mcp-name: io.github.swaphunt/swaphunt-mcp -->
24
+
25
+ An [MCP](https://modelcontextprotocol.io) server that gives Claude and other AI agents 30 paid SwapHunt tools: prices, BTC market regime, chop detection, volatility, expected move, funding rates, the Alt Season Index, coin strength, momentum and correlation vs BTC, and anti-FOMO sanity checks (is this a dip, a breakout, support, oversold; how late am I, pump age, extension, green-candle streak), all computed from Hyperliquid perpetuals (USDC-margined); plus the Crypto Fear & Greed Index (current, compared with earlier days, and daily history), the halving countdown, and the current trading session and weekend liquidity. The agent pays per call in USDC on Base via x402, from a wallet you control, with a spending cap per session.
26
+
27
+ Every tool description states its price, so the agent can see what a call costs before making it.
28
+
29
+ ```bash
30
+ uvx swaphunt-mcp # needs uv: https://docs.astral.sh/uv/
31
+ ```
32
+
33
+ ## Tools
34
+
35
+ | Tool | Price | What it returns |
36
+ |---|---|---|
37
+ | `get_spending` | free | What the session has spent and how much budget is left |
38
+ | `get_fear_greed` | $0.001 | Crypto Fear & Greed Index (0-100) with its label |
39
+ | `get_fear_greed_compare` | $0.002 | The index now vs yesterday, 7 and 30 days ago, with the changes |
40
+ | `get_fear_greed_history` | $0.005 | Daily index values for up to 365 days |
41
+ | `get_halving` | $0.001 | Halving countdown for BTC or LTC |
42
+ | `get_trading_session` | $0.001 | Current trading session and its typical volatility |
43
+ | `get_weekend_liquidity` | $0.001 | Weekend or low-liquidity window, with a 0-100 liquidity score |
44
+
45
+ ### From Hyperliquid perpetuals (USDC-margined)
46
+
47
+ These tools are computed from Hyperliquid perpetuals, and every result has `"source": "Hyperliquid perpetuals"`. Prices are in USDC (`USD` and `USDT` are accepted as aliases). Funding is settled hourly; the annualized rate is the hourly rate x 8760.
48
+
49
+ | Tool | Price | What it returns |
50
+ |---|---|---|
51
+ | `get_price` | $0.001 | One asset's price in USDC (`price_type` `mark` or `mid`), 24h change and volume |
52
+ | `get_prices` | $0.003 | Prices for up to 20 assets in one call |
53
+ | `get_supported_assets` | $0.001 | Every asset the price tools can quote (all active Hyperliquid perps) |
54
+ | `get_market_regime` | $0.002 | BTC regime (BULLISH/BEARISH/NEUTRAL) from price vs the 12h EMA20 |
55
+ | `get_chop_detection` | $0.003 | Trending or chopping (ADX, +DI/-DI) on 1h, 4h or 1d |
56
+ | `get_volatility` | $0.002 | Realized volatility over 7d, 14d or 30d with a low/normal/high/extreme level |
57
+ | `get_market_silence` | $0.002 | Market-silence score 0-1 (low volume, low volatility, small moves) |
58
+ | `get_compression_alert` | $0.003 | Volatility squeeze (Bollinger band width percentile) on 1h, 4h or 1d |
59
+ | `get_expected_move` | $0.003 | ATR-based 68% and 95% price ranges for 4h, 1d or 1w |
60
+ | `get_funding_rates` | $0.003 | Current hourly funding for up to 20 coins, annualized, with 7d/30d averages |
61
+ | `get_top_funding_rates` | $0.003 | The most positive or most negative funding rates across all perps |
62
+ | `get_funding_history` | $0.004 | Hourly funding history for one coin, up to 365 days, with percentile of the latest rate |
63
+ | `get_alt_season` | $0.003 | SwapHunt Alt Season Index (0-100): share of top perps beating BTC over 90 days |
64
+
65
+ ### Coin analysis and sanity checks (Hyperliquid perpetuals, top 50 coins)
66
+
67
+ These tools are also computed from Hyperliquid perpetuals (USDC-margined), with prices and levels in USDC. They cover the top 50 Hyperliquid perpetuals by 30-day volume (the `covered` list from `get_supported_assets`). For any other coin the API answers `422 not_covered` before asking for payment: nothing is charged, nothing counts against the session cap, and the tool returns the covered list so the agent can pick another coin.
68
+
69
+ | Tool | Price | What it returns |
70
+ |---|---|---|
71
+ | `get_coin_strength` | $0.003 | A coin's return minus BTC's over 24h, 7d or 30d, rated very_strong to very_weak |
72
+ | `get_momentum_rank` | $0.003 | Covered coins ranked by return over 24h, 7d or 30d: top and bottom performers |
73
+ | `get_correlation` | $0.003 | Pearson correlation of a coin's returns with BTC's over 7d or 30d |
74
+ | `is_this_a_dip` | $0.003 | Drop from the 14-day high vs the 14-day ATR: real dip or noise |
75
+ | `is_this_a_breakout` | $0.003 | Breakout through a price level up or down: confirmed or likely fakeout |
76
+ | `is_this_support` | $0.003 | Whether a price level is real support: touches, recency and bounce quality over 90 days |
77
+ | `is_this_oversold` | $0.003 | RSI(14) on 1h, 4h and 1d combined into a weighted RSI, with a verdict |
78
+ | `how_late_am_i` | $0.003 | How far a move has run from its 4h swing low, in % and ATR multiples |
79
+ | `get_pump_age` | $0.003 | Active pump detection on 15m candles: start, duration and size vs the average pump |
80
+ | `is_price_extended` | $0.003 | Distance from the 20 and 50 MA on 4h, Bollinger position and z-score |
81
+ | `get_green_candle_streak` | $0.003 | Current green-candle streak on 1h, 4h or 1d and how often the next candle was green |
82
+
83
+ An uncovered coin returns, for example:
84
+
85
+ ```json
86
+ {"error": "not_covered", "charged": false,
87
+ "message": "SEI is not covered yet. Covered: top 50 Hyperliquid perpetuals by 30-day volume. Nothing was paid. Retry with one of the covered coins.",
88
+ "symbol": "SEI", "covered": ["AAVE", "ADA", "AERO", "...", "kPEPE"],
89
+ "coverage_rule": "Top 50 Hyperliquid perpetuals by 30-day notional volume (stablecoins excluded), recomputed daily. ..."}
90
+ ```
91
+
92
+ Fear & Greed results include an `attribution` field. Alternative.me, the source of the index, asks for that text next to its data, and the tool descriptions tell the agent to include it.
93
+
94
+ ## Setup
95
+
96
+ Use a dedicated wallet with a few dollars of USDC on Base mainnet. Never your main wallet.
97
+
98
+ Store the wallet key in a file only you can read, so it never sits in a config file:
99
+
100
+ ```bash
101
+ install -m 600 /dev/null ~/.swaphunt-wallet.key
102
+ nano ~/.swaphunt-wallet.key # paste the private key, save
103
+ ```
104
+
105
+ The server runs with [uv](https://docs.astral.sh/uv/)'s `uvx`, which downloads it from PyPI into its own environment. There is nothing to clone or install by hand. Try it without a wallet first: with no settings, every tool is a dry run.
106
+
107
+ ## Settings
108
+
109
+ | Variable | Default | Effect |
110
+ |---|---|---|
111
+ | `X402_PAY` | off | Without `X402_PAY=1`, every tool runs as a dry run: it reports the price and returns no data. |
112
+ | `PRIVATE_KEY_FILE` | | Path to a file with the wallet's private key (recommended). |
113
+ | `PRIVATE_KEY` | | The key itself, if you prefer an environment variable. |
114
+ | `MAX_SPEND_USD` | `0.10` | Spending cap for the session (one server process). Calls beyond it are refused, not paid. |
115
+
116
+ Each payment is also locked to the price, asset (USDC), network (Base) and `payTo` address the API quoted for that call, and a tool refuses to pay more than the price in its description. If a call fails after the payment was sent, it still counts against the cap and is reported as "possibly paid".
117
+
118
+ ## Claude Code
119
+
120
+ ```bash
121
+ claude mcp add swaphunt \
122
+ --env X402_PAY=1 \
123
+ --env PRIVATE_KEY_FILE=$HOME/.swaphunt-wallet.key \
124
+ --env MAX_SPEND_USD=0.25 \
125
+ -- uvx swaphunt-mcp
126
+ ```
127
+
128
+ Then ask, for example:
129
+
130
+ - *"What's the crypto market mood today, how has it changed over the past month, and how long until the next Bitcoin halving?"*
131
+ - *"What are BTC, ETH and SOL trading at, and is BTC in a bullish or bearish regime right now?"*
132
+ - *"Is ETH chopping or trending on the 4h chart, and is its volatility compressing?"*
133
+ - *"Which coins have the most crowded longs by funding, and is BTC's current funding unusual compared with the last 90 days?"*
134
+ - *"How far could SOL move today, and are we in alt season?"*
135
+ - *"ETH dropped today. Is it a real dip or just noise, is it oversold, and how late would I be buying the bounce?"*
136
+ - *"SOL is pumping. How old is the pump, is it already extended, and is it stronger than BTC this week?"*
137
+
138
+ ## Claude Desktop
139
+
140
+ Add this to `claude_desktop_config.json` (Settings → Developer → Edit Config) and restart Claude Desktop:
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "swaphunt": {
146
+ "command": "uvx",
147
+ "args": ["swaphunt-mcp"],
148
+ "env": {
149
+ "X402_PAY": "1",
150
+ "PRIVATE_KEY_FILE": "/home/you/.swaphunt-wallet.key",
151
+ "MAX_SPEND_USD": "0.25"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ If Claude Desktop can't find `uvx`, use its full path (`which uvx` on macOS/Linux, `where uvx` on Windows).
159
+
160
+ ## What a tool call returns
161
+
162
+ Paid:
163
+
164
+ ```json
165
+ {
166
+ "data": {"value": 30, "label": "Fear", "timestamp": "2026-10-31T00:00:00", "source": "alternative.me", "...": "..."},
167
+ "cost_usd": 0.001,
168
+ "session_spent_usd": 0.001,
169
+ "session_remaining_usd": 0.099,
170
+ "tx": "https://basescan.org/tx/0x<transaction-hash>"
171
+ }
172
+ ```
173
+
174
+ Dry run (no `X402_PAY=1`):
175
+
176
+ ```json
177
+ {"dry_run": true, "would_cost_usd": 0.001, "network": "eip155:8453",
178
+ "note": "Dry run, nothing paid and no data. The user can set X402_PAY=1 to enable payments."}
179
+ ```
180
+
181
+ When the cap is reached:
182
+
183
+ ```json
184
+ {"error": "Session spending cap reached: $0.002 spent of MAX_SPEND_USD=$0.0025; this call costs $0.001. Restart the server or raise MAX_SPEND_USD."}
185
+ ```
186
+
187
+ ## Run from source
188
+
189
+ ```bash
190
+ git clone https://github.com/swaphunt/x402-examples.git
191
+ cd x402-examples/mcp-server
192
+ uv run swaphunt-mcp # or: uv run swaphunt-mcp --check
193
+ ```
194
+
195
+ ## For maintainers
196
+
197
+ `uvx swaphunt-mcp --check` compares every tool's advertised price with the live API. It pays nothing.
198
+
199
+ Source, issues and the other x402 examples (quickstart, daily brief, alert bot): [github.com/swaphunt/x402-examples](https://github.com/swaphunt/x402-examples).
200
+
201
+ Nothing here is investment advice.
@@ -0,0 +1,181 @@
1
+ # SwapHunt MCP server
2
+
3
+ <!-- mcp-name: io.github.swaphunt/swaphunt-mcp -->
4
+
5
+ An [MCP](https://modelcontextprotocol.io) server that gives Claude and other AI agents 30 paid SwapHunt tools: prices, BTC market regime, chop detection, volatility, expected move, funding rates, the Alt Season Index, coin strength, momentum and correlation vs BTC, and anti-FOMO sanity checks (is this a dip, a breakout, support, oversold; how late am I, pump age, extension, green-candle streak), all computed from Hyperliquid perpetuals (USDC-margined); plus the Crypto Fear & Greed Index (current, compared with earlier days, and daily history), the halving countdown, and the current trading session and weekend liquidity. The agent pays per call in USDC on Base via x402, from a wallet you control, with a spending cap per session.
6
+
7
+ Every tool description states its price, so the agent can see what a call costs before making it.
8
+
9
+ ```bash
10
+ uvx swaphunt-mcp # needs uv: https://docs.astral.sh/uv/
11
+ ```
12
+
13
+ ## Tools
14
+
15
+ | Tool | Price | What it returns |
16
+ |---|---|---|
17
+ | `get_spending` | free | What the session has spent and how much budget is left |
18
+ | `get_fear_greed` | $0.001 | Crypto Fear & Greed Index (0-100) with its label |
19
+ | `get_fear_greed_compare` | $0.002 | The index now vs yesterday, 7 and 30 days ago, with the changes |
20
+ | `get_fear_greed_history` | $0.005 | Daily index values for up to 365 days |
21
+ | `get_halving` | $0.001 | Halving countdown for BTC or LTC |
22
+ | `get_trading_session` | $0.001 | Current trading session and its typical volatility |
23
+ | `get_weekend_liquidity` | $0.001 | Weekend or low-liquidity window, with a 0-100 liquidity score |
24
+
25
+ ### From Hyperliquid perpetuals (USDC-margined)
26
+
27
+ These tools are computed from Hyperliquid perpetuals, and every result has `"source": "Hyperliquid perpetuals"`. Prices are in USDC (`USD` and `USDT` are accepted as aliases). Funding is settled hourly; the annualized rate is the hourly rate x 8760.
28
+
29
+ | Tool | Price | What it returns |
30
+ |---|---|---|
31
+ | `get_price` | $0.001 | One asset's price in USDC (`price_type` `mark` or `mid`), 24h change and volume |
32
+ | `get_prices` | $0.003 | Prices for up to 20 assets in one call |
33
+ | `get_supported_assets` | $0.001 | Every asset the price tools can quote (all active Hyperliquid perps) |
34
+ | `get_market_regime` | $0.002 | BTC regime (BULLISH/BEARISH/NEUTRAL) from price vs the 12h EMA20 |
35
+ | `get_chop_detection` | $0.003 | Trending or chopping (ADX, +DI/-DI) on 1h, 4h or 1d |
36
+ | `get_volatility` | $0.002 | Realized volatility over 7d, 14d or 30d with a low/normal/high/extreme level |
37
+ | `get_market_silence` | $0.002 | Market-silence score 0-1 (low volume, low volatility, small moves) |
38
+ | `get_compression_alert` | $0.003 | Volatility squeeze (Bollinger band width percentile) on 1h, 4h or 1d |
39
+ | `get_expected_move` | $0.003 | ATR-based 68% and 95% price ranges for 4h, 1d or 1w |
40
+ | `get_funding_rates` | $0.003 | Current hourly funding for up to 20 coins, annualized, with 7d/30d averages |
41
+ | `get_top_funding_rates` | $0.003 | The most positive or most negative funding rates across all perps |
42
+ | `get_funding_history` | $0.004 | Hourly funding history for one coin, up to 365 days, with percentile of the latest rate |
43
+ | `get_alt_season` | $0.003 | SwapHunt Alt Season Index (0-100): share of top perps beating BTC over 90 days |
44
+
45
+ ### Coin analysis and sanity checks (Hyperliquid perpetuals, top 50 coins)
46
+
47
+ These tools are also computed from Hyperliquid perpetuals (USDC-margined), with prices and levels in USDC. They cover the top 50 Hyperliquid perpetuals by 30-day volume (the `covered` list from `get_supported_assets`). For any other coin the API answers `422 not_covered` before asking for payment: nothing is charged, nothing counts against the session cap, and the tool returns the covered list so the agent can pick another coin.
48
+
49
+ | Tool | Price | What it returns |
50
+ |---|---|---|
51
+ | `get_coin_strength` | $0.003 | A coin's return minus BTC's over 24h, 7d or 30d, rated very_strong to very_weak |
52
+ | `get_momentum_rank` | $0.003 | Covered coins ranked by return over 24h, 7d or 30d: top and bottom performers |
53
+ | `get_correlation` | $0.003 | Pearson correlation of a coin's returns with BTC's over 7d or 30d |
54
+ | `is_this_a_dip` | $0.003 | Drop from the 14-day high vs the 14-day ATR: real dip or noise |
55
+ | `is_this_a_breakout` | $0.003 | Breakout through a price level up or down: confirmed or likely fakeout |
56
+ | `is_this_support` | $0.003 | Whether a price level is real support: touches, recency and bounce quality over 90 days |
57
+ | `is_this_oversold` | $0.003 | RSI(14) on 1h, 4h and 1d combined into a weighted RSI, with a verdict |
58
+ | `how_late_am_i` | $0.003 | How far a move has run from its 4h swing low, in % and ATR multiples |
59
+ | `get_pump_age` | $0.003 | Active pump detection on 15m candles: start, duration and size vs the average pump |
60
+ | `is_price_extended` | $0.003 | Distance from the 20 and 50 MA on 4h, Bollinger position and z-score |
61
+ | `get_green_candle_streak` | $0.003 | Current green-candle streak on 1h, 4h or 1d and how often the next candle was green |
62
+
63
+ An uncovered coin returns, for example:
64
+
65
+ ```json
66
+ {"error": "not_covered", "charged": false,
67
+ "message": "SEI is not covered yet. Covered: top 50 Hyperliquid perpetuals by 30-day volume. Nothing was paid. Retry with one of the covered coins.",
68
+ "symbol": "SEI", "covered": ["AAVE", "ADA", "AERO", "...", "kPEPE"],
69
+ "coverage_rule": "Top 50 Hyperliquid perpetuals by 30-day notional volume (stablecoins excluded), recomputed daily. ..."}
70
+ ```
71
+
72
+ Fear & Greed results include an `attribution` field. Alternative.me, the source of the index, asks for that text next to its data, and the tool descriptions tell the agent to include it.
73
+
74
+ ## Setup
75
+
76
+ Use a dedicated wallet with a few dollars of USDC on Base mainnet. Never your main wallet.
77
+
78
+ Store the wallet key in a file only you can read, so it never sits in a config file:
79
+
80
+ ```bash
81
+ install -m 600 /dev/null ~/.swaphunt-wallet.key
82
+ nano ~/.swaphunt-wallet.key # paste the private key, save
83
+ ```
84
+
85
+ The server runs with [uv](https://docs.astral.sh/uv/)'s `uvx`, which downloads it from PyPI into its own environment. There is nothing to clone or install by hand. Try it without a wallet first: with no settings, every tool is a dry run.
86
+
87
+ ## Settings
88
+
89
+ | Variable | Default | Effect |
90
+ |---|---|---|
91
+ | `X402_PAY` | off | Without `X402_PAY=1`, every tool runs as a dry run: it reports the price and returns no data. |
92
+ | `PRIVATE_KEY_FILE` | | Path to a file with the wallet's private key (recommended). |
93
+ | `PRIVATE_KEY` | | The key itself, if you prefer an environment variable. |
94
+ | `MAX_SPEND_USD` | `0.10` | Spending cap for the session (one server process). Calls beyond it are refused, not paid. |
95
+
96
+ Each payment is also locked to the price, asset (USDC), network (Base) and `payTo` address the API quoted for that call, and a tool refuses to pay more than the price in its description. If a call fails after the payment was sent, it still counts against the cap and is reported as "possibly paid".
97
+
98
+ ## Claude Code
99
+
100
+ ```bash
101
+ claude mcp add swaphunt \
102
+ --env X402_PAY=1 \
103
+ --env PRIVATE_KEY_FILE=$HOME/.swaphunt-wallet.key \
104
+ --env MAX_SPEND_USD=0.25 \
105
+ -- uvx swaphunt-mcp
106
+ ```
107
+
108
+ Then ask, for example:
109
+
110
+ - *"What's the crypto market mood today, how has it changed over the past month, and how long until the next Bitcoin halving?"*
111
+ - *"What are BTC, ETH and SOL trading at, and is BTC in a bullish or bearish regime right now?"*
112
+ - *"Is ETH chopping or trending on the 4h chart, and is its volatility compressing?"*
113
+ - *"Which coins have the most crowded longs by funding, and is BTC's current funding unusual compared with the last 90 days?"*
114
+ - *"How far could SOL move today, and are we in alt season?"*
115
+ - *"ETH dropped today. Is it a real dip or just noise, is it oversold, and how late would I be buying the bounce?"*
116
+ - *"SOL is pumping. How old is the pump, is it already extended, and is it stronger than BTC this week?"*
117
+
118
+ ## Claude Desktop
119
+
120
+ Add this to `claude_desktop_config.json` (Settings → Developer → Edit Config) and restart Claude Desktop:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "swaphunt": {
126
+ "command": "uvx",
127
+ "args": ["swaphunt-mcp"],
128
+ "env": {
129
+ "X402_PAY": "1",
130
+ "PRIVATE_KEY_FILE": "/home/you/.swaphunt-wallet.key",
131
+ "MAX_SPEND_USD": "0.25"
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ If Claude Desktop can't find `uvx`, use its full path (`which uvx` on macOS/Linux, `where uvx` on Windows).
139
+
140
+ ## What a tool call returns
141
+
142
+ Paid:
143
+
144
+ ```json
145
+ {
146
+ "data": {"value": 30, "label": "Fear", "timestamp": "2026-10-31T00:00:00", "source": "alternative.me", "...": "..."},
147
+ "cost_usd": 0.001,
148
+ "session_spent_usd": 0.001,
149
+ "session_remaining_usd": 0.099,
150
+ "tx": "https://basescan.org/tx/0x<transaction-hash>"
151
+ }
152
+ ```
153
+
154
+ Dry run (no `X402_PAY=1`):
155
+
156
+ ```json
157
+ {"dry_run": true, "would_cost_usd": 0.001, "network": "eip155:8453",
158
+ "note": "Dry run, nothing paid and no data. The user can set X402_PAY=1 to enable payments."}
159
+ ```
160
+
161
+ When the cap is reached:
162
+
163
+ ```json
164
+ {"error": "Session spending cap reached: $0.002 spent of MAX_SPEND_USD=$0.0025; this call costs $0.001. Restart the server or raise MAX_SPEND_USD."}
165
+ ```
166
+
167
+ ## Run from source
168
+
169
+ ```bash
170
+ git clone https://github.com/swaphunt/x402-examples.git
171
+ cd x402-examples/mcp-server
172
+ uv run swaphunt-mcp # or: uv run swaphunt-mcp --check
173
+ ```
174
+
175
+ ## For maintainers
176
+
177
+ `uvx swaphunt-mcp --check` compares every tool's advertised price with the live API. It pays nothing.
178
+
179
+ Source, issues and the other x402 examples (quickstart, daily brief, alert bot): [github.com/swaphunt/x402-examples](https://github.com/swaphunt/x402-examples).
180
+
181
+ Nothing here is investment advice.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.27.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "swaphunt-mcp"
7
+ dynamic = ["version"]
8
+ description = "MCP server with 30 SwapHunt crypto market tools for AI agents (Hyperliquid prices, regime, volatility, funding, coin strength, anti-FOMO sanity checks; Fear & Greed), paid per call in USDC via x402"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "SwapHunt", email = "github@swaphunt.dev" }]
14
+ keywords = ["mcp", "x402", "crypto", "usdc", "base", "ai-agents", "market-data"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Office/Business :: Financial :: Investment",
20
+ ]
21
+ # Exact pins: `uvx swaphunt-mcp` runs in its own environment, so pinning costs nothing
22
+ # and means every user gets the versions this release was tested with.
23
+ dependencies = [
24
+ "mcp==2.3.0",
25
+ "x402[httpx,evm]==2.25.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://x402.swaphunt.dev/docs"
30
+ Source = "https://github.com/swaphunt/x402-examples/tree/main/mcp-server"
31
+ Issues = "https://github.com/swaphunt/x402-examples/issues"
32
+
33
+ [project.scripts]
34
+ swaphunt-mcp = "swaphunt_mcp.server:main"
35
+
36
+ [tool.hatch.version]
37
+ path = "src/swaphunt_mcp/__init__.py"
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src/", "README.md", "LICENSE", "pyproject.toml"]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/swaphunt_mcp"]
@@ -0,0 +1,3 @@
1
+ """SwapHunt MCP server: crypto market context for AI agents, paid per call with x402."""
2
+
3
+ __version__ = "0.4.0"
@@ -0,0 +1,3 @@
1
+ from swaphunt_mcp.server import main
2
+
3
+ main()
@@ -0,0 +1,464 @@
1
+ """SwapHunt MCP server: SwapHunt's x402 endpoints as MCP tools, paid per call in USDC on Base.
2
+
3
+ Dry run by default: tools report what a call would cost and return no data.
4
+ Set X402_PAY=1 and PRIVATE_KEY (or PRIVATE_KEY_FILE) to pay. Spending is capped per session (server process) by MAX_SPEND_USD.
5
+
6
+ Run: uvx swaphunt-mcp (stdio, for Claude Desktop / Claude Code)
7
+ uvx swaphunt-mcp --check (compare advertised prices with the live API, pays nothing)
8
+ """
9
+ import asyncio
10
+ import os
11
+ import sys
12
+ from typing import Any
13
+
14
+ import httpx
15
+ from eth_account import Account
16
+ from mcp.server import MCPServer
17
+ from mcp.types import Icon
18
+ from x402 import x402Client
19
+
20
+ from swaphunt_mcp import __version__
21
+ from x402.http import decode_payment_required_header, decode_payment_response_header
22
+ from x402.http.clients import x402HttpxClient
23
+ from x402.mechanisms.evm import EthAccountSigner
24
+ from x402.mechanisms.evm.exact.register import register_exact_evm_client
25
+ from x402.schemas.errors import NoMatchingRequirementsError
26
+
27
+ BASE = "https://x402.swaphunt.dev"
28
+ ICON_URL = "https://raw.githubusercontent.com/swaphunt/x402-examples/main/mcp-server/assets/icon-512.png"
29
+ MAX_SPEND = float(os.getenv("MAX_SPEND_USD") or "0.10") # empty counts as unset
30
+ PAY = os.getenv("X402_PAY") == "1"
31
+ # The key can come from a file, so it never has to sit in an MCP client's config file.
32
+ if not os.getenv("PRIVATE_KEY") and os.getenv("PRIVATE_KEY_FILE"):
33
+ with open(os.path.expanduser(os.environ["PRIVATE_KEY_FILE"])) as f:
34
+ os.environ["PRIVATE_KEY"] = f.read().strip()
35
+
36
+ mcp = MCPServer(
37
+ "swaphunt",
38
+ title="SwapHunt crypto market context",
39
+ version=__version__,
40
+ website_url="https://x402.swaphunt.dev/docs",
41
+ icons=[Icon(src=ICON_URL, mime_type="image/png", sizes=["512x512"])],
42
+ instructions=(
43
+ "Crypto market context: the Fear & Greed Index (current, compared and history), the "
44
+ "halving countdown, trading session and weekend liquidity, and tools computed from Hyperliquid "
45
+ "perpetuals (USDC-margined): prices in USDC, BTC market regime, chop detection, volatility, "
46
+ "expected move, hourly funding rates, the Alt Season Index, coin strength, momentum and "
47
+ "correlation vs BTC, and sanity checks (is this a dip, breakout, support, oversold; how late am I, "
48
+ "pump age, extension, green-candle streak). The coin-level analysis tools cover the top 50 "
49
+ "Hyperliquid perpetuals by 30-day volume; other coins are rejected with not_covered and not "
50
+ "charged. Every tool except get_spending "
51
+ "costs real USDC per call (price in each description), with a per-session spending cap. "
52
+ "Prefer one well-chosen call over many. When showing Fear & Greed values, include the "
53
+ "attribution text from the result. Nothing returned is investment advice."
54
+ ),
55
+ )
56
+
57
+ _spent = 0.0
58
+ _calls = 0
59
+ _lock = asyncio.Lock() # tools may run concurrently; budget checks must not race
60
+
61
+
62
+ BASE_MAINNET = "eip155:8453"
63
+ USDC_ON_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" # canonical USDC, 6 decimals
64
+
65
+
66
+ def _usd(req) -> float:
67
+ return int(req.amount) / 1e6 # only valid for USDC, which _quote enforces
68
+
69
+
70
+ class NotCovered(Exception):
71
+ """The API rejected the coin before asking for payment (HTTP 422 not_covered). Nothing is charged."""
72
+
73
+ def __init__(self, body: dict):
74
+ super().__init__(body.get("message", "not covered"))
75
+ self.body = body
76
+
77
+
78
+ def _not_covered(r: httpx.Response) -> dict | None:
79
+ """The 422 not_covered body, or None. The API sends it before any payment is requested."""
80
+ if r.status_code != 422:
81
+ return None
82
+ try:
83
+ body = r.json()
84
+ except ValueError:
85
+ return None
86
+ return body if isinstance(body, dict) and body.get("error") == "not_covered" else None
87
+
88
+
89
+ def _not_covered_result(body: dict) -> dict:
90
+ """What the agent sees for an uncovered coin: a clear message and the covered list, no charge."""
91
+ return {"error": "not_covered", "charged": False,
92
+ "message": f"{body.get('message', 'This coin is not covered.')} Nothing was paid. "
93
+ "Retry with one of the covered coins.",
94
+ "symbol": body.get("symbol"), "covered": body.get("covered", []),
95
+ "coverage_rule": body.get("coverage_rule")}
96
+
97
+
98
+ async def _quote(path: str, params: dict) -> Any:
99
+ """Unpaid request -> 402. Returns the USDC-on-Base option; any other asset or network is refused."""
100
+ async with httpx.AsyncClient(timeout=30) as http:
101
+ r = await http.get(BASE + path, params=params)
102
+ nc = _not_covered(r)
103
+ if nc is not None:
104
+ raise NotCovered(nc)
105
+ if r.status_code != 402:
106
+ raise RuntimeError(f"expected 402 from {path}, got {r.status_code}: {r.text[:200]}")
107
+ for req in decode_payment_required_header(r.headers["PAYMENT-REQUIRED"]).accepts:
108
+ if req.network == BASE_MAINNET and req.asset.lower() == USDC_ON_BASE:
109
+ return req
110
+ raise RuntimeError(f"{path} offers no USDC payment on Base")
111
+
112
+
113
+ def _locked_to(quote):
114
+ """Payment policy: pay only exactly what was quoted, in that asset, on that network, to that address."""
115
+ def policy(version, reqs):
116
+ return [r for r in reqs if r.amount == quote.amount and r.network == quote.network
117
+ and r.asset.lower() == quote.asset.lower() and r.pay_to.lower() == quote.pay_to.lower()]
118
+ return policy
119
+
120
+
121
+ def _refused_before_signing(e: BaseException) -> bool:
122
+ """True only if the client rejected the payment terms, which happens before anything is signed.
123
+
124
+ Checks the exception and its explicit cause only; anything less certain counts as possibly paid.
125
+ """
126
+ return isinstance(e, NoMatchingRequirementsError) or isinstance(e.__cause__, NoMatchingRequirementsError)
127
+
128
+
129
+ async def call(path: str, advertised: float, **params: Any) -> dict:
130
+ """Quote, check price and budget, pay, return the data plus what it cost."""
131
+ global _spent, _calls
132
+ params = {k: v for k, v in params.items() if v is not None}
133
+ try:
134
+ quote = await _quote(path, params)
135
+ except NotCovered as e:
136
+ return _not_covered_result(e.body)
137
+ price = _usd(quote)
138
+ if price > advertised:
139
+ return {"error": f"Refused: {path} now costs ${price:.3f}, more than the advertised ${advertised:.3f}."}
140
+ if not PAY:
141
+ return {"dry_run": True, "would_cost_usd": price, "network": quote.network,
142
+ "note": "Dry run, nothing paid and no data. The user can set X402_PAY=1 to enable payments."}
143
+ async with _lock:
144
+ if _spent + price > MAX_SPEND:
145
+ return {"error": f"Session spending cap reached: ${_spent:.3f} spent of MAX_SPEND_USD=${MAX_SPEND:g}; "
146
+ f"this call costs ${price:.3f}. Restart the server or raise MAX_SPEND_USD."}
147
+ _spent += price # reserve before paying so parallel calls can't overshoot
148
+ # The reservation is released only when it is certain nothing was signed. Any failure after
149
+ # the request starts (timeout, non-200, missing receipt) stays counted as possibly paid.
150
+ try:
151
+ client = x402Client().set_spend_controls({"max_amount_per_payment": f"${price:.6f}"})
152
+ client.register_policy(_locked_to(quote))
153
+ register_exact_evm_client(client, EthAccountSigner(Account.from_key(os.environ["PRIVATE_KEY"])))
154
+ except Exception as e:
155
+ async with _lock:
156
+ _spent -= price
157
+ return {"error": f"Not paid (wallet setup failed): {type(e).__name__}"}
158
+ try:
159
+ async with x402HttpxClient(client, timeout=60) as http:
160
+ r = await http.get(BASE + path, params=params)
161
+ except Exception as e:
162
+ if _refused_before_signing(e):
163
+ async with _lock:
164
+ _spent -= price
165
+ return {"error": f"Not paid: the payment terms changed since the quote ({e})"}
166
+ return {"error": f"Possibly paid ${price:.3f}, no data: {type(e).__name__}: {str(e)[:150]}. "
167
+ "Counted against the session cap; check the wallet on basescan.org."}
168
+ receipt = r.headers.get("PAYMENT-RESPONSE")
169
+ nc = _not_covered(r)
170
+ if nc is not None and not receipt:
171
+ # Coverage changed between quote and call. Only give the reservation back when it is certain
172
+ # nothing was signed: the final request carried no payment header. If a payment was signed,
173
+ # the API still does not settle on errors, but keep it counted against the cap to be safe.
174
+ sent = getattr(r, "request", None)
175
+ signed = sent is not None and any(h in sent.headers for h in ("payment-signature", "x-payment"))
176
+ if not signed:
177
+ async with _lock:
178
+ _spent -= price
179
+ return _not_covered_result(nc)
180
+ result = _not_covered_result(nc)
181
+ result["charged"] = "unlikely"
182
+ result["message"] = (f"{nc.get('message', 'This coin is not covered.')} A payment was signed, but the API "
183
+ "does not settle on errors. It stays counted against the session cap to be safe. "
184
+ "Retry with one of the covered coins.")
185
+ return result
186
+ tx = decode_payment_response_header(receipt).transaction if receipt else None
187
+ tx_link = f"https://basescan.org/tx/{tx}" if tx else None
188
+ if r.status_code != 200:
189
+ return {"error": f"Possibly paid ${price:.3f}, no data: HTTP {r.status_code}. Counted against the session cap.",
190
+ "tx": tx_link}
191
+ _calls += 1
192
+ return {"data": r.json(), "cost_usd": price, "session_spent_usd": round(_spent, 6),
193
+ "session_remaining_usd": round(MAX_SPEND - _spent, 6), "tx": tx_link}
194
+
195
+
196
+ def paid(price: float, summary: str):
197
+ """Register a paid tool; the price goes into the description the agent sees."""
198
+ return mcp.tool(description=f"{summary} Costs ${price:.3f} USDC per call.")
199
+
200
+
201
+ # --- Free -------------------------------------------------------------------
202
+
203
+ @mcp.tool(description="Free. Shows payment mode, USDC spent this session, calls made and budget left.")
204
+ async def get_spending() -> dict:
205
+ return {"payments_enabled": PAY, "spent_usd": round(_spent, 6), "paid_calls": _calls,
206
+ "cap_usd": MAX_SPEND, "remaining_usd": round(MAX_SPEND - _spent, 6)}
207
+
208
+
209
+ # --- Fear & Greed -----------------------------------------------------------
210
+ # Alternative.me asks for attribution next to its data: show the `attribution` field with the values.
211
+
212
+ @paid(0.001, "Crypto Fear & Greed Index (0-100) with its label, updated daily. "
213
+ "When showing the value, include the `attribution` text from the result.")
214
+ async def get_fear_greed() -> dict:
215
+ return await call("/v1/market/fear-greed", 0.001)
216
+
217
+
218
+ @paid(0.002, "Fear & Greed Index now vs yesterday, 7 and 30 days ago, with the changes. "
219
+ "When showing the values, include the `attribution` text from the result.")
220
+ async def get_fear_greed_compare() -> dict:
221
+ return await call("/v1/market/fear-greed/compare", 0.002)
222
+
223
+
224
+ @paid(0.005, "Daily Fear & Greed history: the last `days` (up to 365), or `start_date` to `end_date` "
225
+ "(YYYY-MM-DD). When showing the values, include the `attribution` text from the result.")
226
+ async def get_fear_greed_history(days: int = 30, start_date: str | None = None,
227
+ end_date: str | None = None) -> dict:
228
+ return await call("/v1/market/fear-greed/history", 0.005, days=days, start_date=start_date, end_date=end_date)
229
+
230
+
231
+ # --- Halving ----------------------------------------------------------------
232
+
233
+ @paid(0.001, "Halving countdown for `coin` (BTC or LTC): blocks remaining and estimated date of the next halving.")
234
+ async def get_halving(coin: str = "BTC") -> dict:
235
+ return await call("/v1/market/halving", 0.001, coin=coin)
236
+
237
+
238
+ # --- Hyperliquid perpetuals -------------------------------------------------
239
+ # Everything below is computed from Hyperliquid perpetuals (USDC-margined); prices are in USDC.
240
+
241
+ HL = "From Hyperliquid perpetuals (USDC-margined)."
242
+
243
+
244
+ @paid(0.001, "Live price of one asset in USDC: perp mark price (`price_type` mark) or order-book mid "
245
+ "(`price_type` mid), with 24h change and 24h volume. `asset` is any active perp (BTC, ETH, SOL ...; "
246
+ f"BTCUSDT and BTC-PERP also mean BTC). `currency` is USDC (USD and USDT accepted as aliases). {HL}")
247
+ async def get_price(asset: str = "BTC", currency: str = "USDC", price_type: str = "mark") -> dict:
248
+ return await call("/v1/tools/price", 0.001, asset=asset, currency=currency, price_type=price_type)
249
+
250
+
251
+ @paid(0.003, "Prices in USDC for up to 20 assets in one call (`assets` comma-separated, e.g. 'BTC,ETH,SOL'), "
252
+ "each with 24h change and volume; unknown symbols are skipped. `price_type` mark or mid. "
253
+ f"Cheaper than several get_price calls. {HL}")
254
+ async def get_prices(assets: str = "BTC,ETH", currency: str = "USDC", price_type: str = "mark") -> dict:
255
+ return await call("/v1/tools/price/multi", 0.003, assets=assets, currency=currency, price_type=price_type)
256
+
257
+
258
+ @paid(0.001, "List of every asset get_price and get_prices can quote: all active perps, by Hyperliquid name "
259
+ f"(e.g. kPEPE = 1,000 PEPE). {HL}")
260
+ async def get_supported_assets() -> dict:
261
+ return await call("/v1/tools/price/supported", 0.001)
262
+
263
+
264
+ @paid(0.002, "Current BTC market regime (BULLISH/BEARISH/NEUTRAL): BTC perp price vs its 20-period EMA on 12h "
265
+ f"candles, with EMA value and slope. Updates at 00:00 and 12:00 UTC. {HL}")
266
+ async def get_market_regime() -> dict:
267
+ return await call("/v1/regime/current", 0.002)
268
+
269
+
270
+ @paid(0.003, "Is `asset` trending or chopping? ADX-based, with +DI/-DI and trend direction, on `timeframe` "
271
+ f"(1h, 4h, 1d). `quote` is USDC (USD and USDT accepted as aliases). {HL}")
272
+ async def get_chop_detection(asset: str = "BTC", timeframe: str = "4h", quote: str = "USDC") -> dict:
273
+ return await call("/v1/regime/chop-detection", 0.003, asset=asset, timeframe=timeframe, quote=quote)
274
+
275
+
276
+ @paid(0.002, "Realized volatility of `asset` over `period` (7d, 14d, 30d): daily and annualized %, a "
277
+ f"low/normal/high/extreme level and today's UTC range. Backward-looking, not implied vol. {HL}")
278
+ async def get_volatility(asset: str = "BTC", period: str = "7d") -> dict:
279
+ return await call("/v1/vol/realized", 0.002, asset=asset, period=period)
280
+
281
+
282
+ @paid(0.002, "Market-silence score 0-1 for `asset` over `lookback` (7d, 14d, 30d): low volume, low volatility "
283
+ f"and small price change combined. High silence often comes before a big move. {HL}")
284
+ async def get_market_silence(asset: str = "BTC", lookback: str = "7d") -> dict:
285
+ return await call("/v1/vol/silence", 0.002, asset=asset, lookback=lookback)
286
+
287
+
288
+ @paid(0.003, "Volatility squeeze detector (Bollinger band width and its percentile) for `asset` on `timeframe` "
289
+ "(1h, 4h, 1d): extreme/strong/moderate/none. Detects the setup, not the direction. "
290
+ f"`quote` is USDC (USD and USDT accepted as aliases). {HL}")
291
+ async def get_compression_alert(asset: str = "BTC", timeframe: str = "4h", quote: str = "USDC") -> dict:
292
+ return await call("/v1/vol/compression-alert", 0.003, asset=asset, timeframe=timeframe, quote=quote)
293
+
294
+
295
+ @paid(0.003, "ATR-based expected price range for `asset` over `period` (4h, 1d, 1w): 68% and 95% bands around "
296
+ "the current price, in USDC. A statistical range, not a forecast. "
297
+ f"`quote` is USDC (USD and USDT accepted as aliases). {HL}")
298
+ async def get_expected_move(asset: str = "BTC", period: str = "1d", quote: str = "USDC") -> dict:
299
+ return await call("/v1/vol/expected-move", 0.003, asset=asset, period=period, quote=quote)
300
+
301
+
302
+ @paid(0.003, "Current funding rates for up to 20 coins (`symbols` comma-separated, e.g. 'BTC,ETH,SOL'). Funding "
303
+ "is hourly: funding_rate_pct is the hourly rate, annualized = hourly x 8760; with 7d/30d averages "
304
+ f"and a crowding signal. Positive = longs pay shorts. {HL}")
305
+ async def get_funding_rates(symbols: str = "BTC,ETH,SOL") -> dict:
306
+ return await call("/v1/derivatives/funding-rates", 0.003, symbols=symbols)
307
+
308
+
309
+ @paid(0.003, "Leaderboard of the `limit` (1-50) most extreme current funding rates across all active perps, "
310
+ "`sort` highest (crowded longs) or lowest (crowded shorts). Hourly funding, annualized = hourly x 8760. "
311
+ f"{HL}")
312
+ async def get_top_funding_rates(limit: int = 10, sort: str = "highest") -> dict:
313
+ return await call("/v1/derivatives/funding-rates/top", 0.003, limit=limit, sort=sort)
314
+
315
+
316
+ @paid(0.004, "Settled hourly funding history for one coin (`symbol`) over the last `days` (1-365): average, "
317
+ "range, share of positive hours and the percentile of the latest rate. `history_days` in the result "
318
+ f"shows how much history that coin has. Annualized = hourly x 8760. {HL}")
319
+ async def get_funding_history(symbol: str = "BTC", days: int = 90) -> dict:
320
+ return await call("/v1/derivatives/funding-rates/history", 0.004, symbol=symbol, days=days)
321
+
322
+
323
+ @paid(0.003, "SwapHunt Alt Season Index (0-100), daily: share of the top 100 perps by 30-day volume (excl. BTC "
324
+ f"and stablecoins) that beat BTC over 90 days. >=75 alt season, <=25 bitcoin season. {HL}")
325
+ async def get_alt_season() -> dict:
326
+ return await call("/v1/market/alt-season", 0.003)
327
+
328
+
329
+ # --- Coin analysis (Hyperliquid perpetuals, covered coins only) -------------
330
+ # These endpoints cover only the top 50 Hyperliquid perpetuals by 30-day volume. Other coins are
331
+ # rejected with HTTP 422 not_covered before any payment is asked; call() returns the covered list.
332
+
333
+ COV = ("Covers the top 50 Hyperliquid perpetuals by 30-day volume (list in get_supported_assets, field "
334
+ "`covered`); other coins are rejected with not_covered and not charged.")
335
+
336
+
337
+ @paid(0.003, "Strength of `coin` vs BTC: the coin's return minus BTC's over `period` (24h, 7d, 30d), rated "
338
+ "very_strong/strong/neutral/weak/very_weak. `quote` is USDC (USD and USDT accepted as aliases). "
339
+ f"{HL} {COV}")
340
+ async def get_coin_strength(coin: str = "ETH", period: str = "24h", quote: str = "USDC") -> dict:
341
+ return await call("/v1/market/coin-strength", 0.003, coin=coin, period=period, quote=quote)
342
+
343
+
344
+ @paid(0.003, "Momentum leaderboard: covered coins ranked by return over `period` (24h, 7d, 30d), with the "
345
+ "`limit` (1-20) top and bottom performers and the full ranking. `quote` is USDC (USD and USDT "
346
+ f"accepted as aliases). {HL} Ranks only the top 50 Hyperliquid perpetuals by 30-day volume.")
347
+ async def get_momentum_rank(period: str = "24h", limit: int = 10, quote: str = "USDC") -> dict:
348
+ return await call("/v1/market/momentum-rank", 0.003, period=period, limit=limit, quote=quote)
349
+
350
+
351
+ @paid(0.003, "Pearson correlation (-1 to +1) of `coin`'s returns with BTC's over `period` (7d on 1h candles, "
352
+ "30d on 4h candles), with a level from very_high to negative. Shows whether a coin moves on its own "
353
+ f"or just tracks BTC. `quote` is USDC (USD and USDT accepted as aliases). {HL} {COV}")
354
+ async def get_correlation(coin: str = "ETH", period: str = "7d", quote: str = "USDC") -> dict:
355
+ return await call("/v1/market/correlation", 0.003, coin=coin, period=period, quote=quote)
356
+
357
+
358
+ @paid(0.003, "Is this a real dip? Drop of `symbol` from its 14-day high compared with its 14-day ATR on daily "
359
+ "candles; a drop smaller than normal daily volatility is noise. Returns is_dip, drop_pct, atr_pct "
360
+ f"and a verdict. `quote` is USDC (USD and USDT accepted as aliases). {HL} {COV}")
361
+ async def is_this_a_dip(symbol: str = "BTC", quote: str = "USDC") -> dict:
362
+ return await call("/v1/sanity/is-this-a-dip", 0.003, symbol=symbol, quote=quote)
363
+
364
+
365
+ @paid(0.003, "Is a breakout of `symbol` through `level` (price in USDC, required) confirmed or a likely "
366
+ "fakeout? `direction` up or down. Uses 1h candles: volume vs the 20-candle average and time held "
367
+ "beyond the level, with a fakeout probability. `quote` is USDC (USD and USDT accepted as aliases). "
368
+ f"{HL} {COV}")
369
+ async def is_this_a_breakout(level: float, symbol: str = "BTC", direction: str = "up", quote: str = "USDC") -> dict:
370
+ return await call("/v1/sanity/is-this-a-breakout", 0.003, symbol=symbol, level=level, direction=direction,
371
+ quote=quote)
372
+
373
+
374
+ @paid(0.003, "Is `level` (price in USDC, required) real support for `symbol`? Checks 90 daily candles for "
375
+ "repeated, recent touches and clean bounces; returns touch_count, days_since_last_touch, "
376
+ f"bounce_quality and a verdict. `quote` is USDC (USD and USDT accepted as aliases). {HL} {COV}")
377
+ async def is_this_support(level: float, symbol: str = "BTC", quote: str = "USDC") -> dict:
378
+ return await call("/v1/sanity/is-this-support", 0.003, symbol=symbol, level=level, quote=quote)
379
+
380
+
381
+ @paid(0.003, "Is `symbol` really oversold? RSI(14) on 1h, 4h and 1d candles combined into a weighted RSI "
382
+ "(20/30/50%), with a verdict. `quote` is USDC (USD and USDT accepted as aliases). "
383
+ f"{HL} {COV}")
384
+ async def is_this_oversold(symbol: str = "BTC", quote: str = "USDC") -> dict:
385
+ return await call("/v1/sanity/is-this-oversold", 0.003, symbol=symbol, quote=quote)
386
+
387
+
388
+ @paid(0.003, "How late am I to this move? Distance of `symbol` from its recent swing low on 4h candles, in % "
389
+ "and ATR multiples, ranked against its own history, with a verdict. `quote` is USDC (USD and USDT "
390
+ f"accepted as aliases). {HL} {COV}")
391
+ async def how_late_am_i(symbol: str = "BTC", quote: str = "USDC") -> dict:
392
+ return await call("/v1/fomo/how-late-am-i", 0.003, symbol=symbol, quote=quote)
393
+
394
+
395
+ @paid(0.003, "How old is this pump? Detects an active pump in `symbol` on the last 24 hours of 15m candles: "
396
+ "start time, duration, size and the average pump length, with a verdict. `quote` is USDC (USD and "
397
+ f"USDT accepted as aliases). {HL} {COV}")
398
+ async def get_pump_age(symbol: str = "BTC", quote: str = "USDC") -> dict:
399
+ return await call("/v1/fomo/pump-age", 0.003, symbol=symbol, quote=quote)
400
+
401
+
402
+ @paid(0.003, "Is `symbol` already extended? Distance from the 20 and 50 MA on 4h candles, Bollinger band "
403
+ "position and z-score, with a verdict. `quote` is USDC (USD and USDT accepted as aliases). "
404
+ f"{HL} {COV}")
405
+ async def is_price_extended(symbol: str = "BTC", quote: str = "USDC") -> dict:
406
+ return await call("/v1/fomo/already-extended", 0.003, symbol=symbol, quote=quote)
407
+
408
+
409
+ @paid(0.003, "Current green-candle streak of `symbol` on `interval` (1h, 4h, 1d) candles, its gain, the "
410
+ "average streak and how often the next candle was green after similar streaks (last 100 candles). "
411
+ f"`quote` is USDC (USD and USDT accepted as aliases). {HL} {COV}")
412
+ async def get_green_candle_streak(symbol: str = "BTC", interval: str = "4h", quote: str = "USDC") -> dict:
413
+ return await call("/v1/fomo/green-candles-streak", 0.003, symbol=symbol, interval=interval, quote=quote)
414
+
415
+
416
+ # --- Trading context --------------------------------------------------------
417
+
418
+ @paid(0.001, "Current trading session (Asia, London, New York, off-hours) and its typical volatility.")
419
+ async def get_trading_session() -> dict:
420
+ return await call("/v1/context/session", 0.001)
421
+
422
+
423
+ @paid(0.001, "Is it a weekend or low-liquidity window right now? Returns a 0-100 liquidity score and the session.")
424
+ async def get_weekend_liquidity() -> dict:
425
+ return await call("/v1/context/weekend-liquidity", 0.001)
426
+
427
+
428
+ # --- Maintainer check -------------------------------------------------------
429
+
430
+ async def check_prices() -> None:
431
+ """Compare every tool's advertised price with the live 402. Pays nothing."""
432
+ import re
433
+
434
+ src = open(__file__).read()
435
+ pairs = re.findall(r'await call\("([^"]+)", ([0-9.]+)', src)
436
+ bad = 0
437
+ for path, adv in pairs:
438
+ try:
439
+ live = _usd(await _quote(path, {}))
440
+ except Exception as e:
441
+ bad += 1
442
+ print(f"ERR {path:<36} {str(e)[:60]}")
443
+ continue
444
+ ok = abs(live - float(adv)) < 1e-9
445
+ bad += not ok
446
+ print(f"{'ok ' if ok else 'BAD'} {path:<36} advertised ${float(adv):.3f} live ${live:.3f}")
447
+ print(f"{len(pairs)} paid tools, {bad} mismatches")
448
+ sys.exit(1 if bad else 0)
449
+
450
+
451
+ def main() -> None:
452
+ """Entry point for `swaphunt-mcp` (and `uvx swaphunt-mcp`)."""
453
+ if "--version" in sys.argv:
454
+ print(f"swaphunt-mcp {__version__}")
455
+ elif "--check" in sys.argv:
456
+ asyncio.run(check_prices())
457
+ else:
458
+ if PAY and not os.getenv("PRIVATE_KEY"):
459
+ sys.exit("X402_PAY=1 needs PRIVATE_KEY or PRIVATE_KEY_FILE (a dedicated wallet with a few dollars of USDC on Base).")
460
+ mcp.run() # stdio
461
+
462
+
463
+ if __name__ == "__main__":
464
+ main()