Crypto Market MCP
README.md
# Crypto Market MCP
A Model Context Protocol server for crypto market data. It runs over stdio for local clients, and over [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) at `/mcp` when you host it. Paid tools use [x402](https://github.com/coinbase/x402) (USDC on Base). Free tools never ask for payment.
**Hosted endpoint (Base mainnet):** [https://2-31-102-176.sslip.io/mcp](https://2-31-102-176.sslip.io/mcp)
Official registry name: `io.github.gojisatoshi888/crypto-market`. Each client gets **3 free paid-tool calls per UTC day**. After that, paid tools require a USDC payment on Base via x402 (see [How an agent pays](#how-an-agent-pays)). `get_price` and `fear_greed` stay free.
| Tool | What it returns | Default price |
| --- | --- | --- |
| `get_price(symbols="BTC,ETH")` | CoinGecko USD price, 24h change, and volume (`ok`, `prices`) | Free |
| `fear_greed()` | alternative.me index (`value`, `classification`, `as_of`) | Free |
| `top_movers(limit, min_volume_usd, include_suspicious)` | Gainers, losers, `filtered_out`, and duplicate merges | $0.005 |
| `funding_rates(symbols="BTC,ETH,SOL")` | Perpetual funding. Binance, then Bybit, then OKX | $0.005 |
| `market_brief(symbols="BTC,ETH,SOL")` | InferHub brief (`max_tokens` 800) plus the raw snapshot | $0.03 |
| `rug_risk(token_address, chain, include_summary=False)` | 0–100 score, level, and flags. Chains: ethereum, bsc, base, solana (`eth`, `bnb`, `sol`) | $0.02 |
The tool bodies are the original `crypto_data.py` and `rug_risk.py` logic, including the documented weights and trusted-token handling. Score bands are `0–24` low, `25–49` medium, `50–74` high, and `75–100` critical. Trusted tokens still list issuer-style flags, and those flags do not add points. Honeypot, can't-sell, high tax, dead liquidity, and fake-token flags always do. Successful upstream GETs are cached. Each vendor call uses `HTTP_TIMEOUT_SECONDS`.
## Run locally
Python 3.10+.
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
Stdio (no wallet, payments off unless you set `PAYMENTS_ENABLED=true`):
```bash
python -m crypto_mcp
```
HTTP, with a health check on `/health` and MCP on `/mcp`:
```bash
export PAY_TO_ADDRESS=0x49Ac893403009239F96a188d55C2FD40f77A8195
export X402_NETWORK=base-sepolia
python -m crypto_mcp.http_server
```
The process listens on `HOST` and `PORT` (`0.0.0.0` and `8080` by default). Open `http://127.0.0.1:8080/health`.
Docker Compose publishes the HTTP server on `127.0.0.1:44021`. If `PAY_TO_ADDRESS` is unset, Compose uses a dummy address so the process can boot. Put your real address in `.env` before you accept payments.
```bash
docker compose up --build
```
## How an agent pays
Payment follows the [x402 MCP transport](https://github.com/coinbase/x402/blob/main/specs/transports-v2/mcp.md). There is no HTTP 402 on `/mcp`, and no extra endpoint. `initialize` and `tools/list` are free. A paid `tools/call` without payment does not run the tool and does not move funds, unless the caller still has a free daily allowance (see below).
1. The client calls the tool normally.
2. The server returns a tool result with `isError: true`. `structuredContent` is an x402 `PaymentRequired` object (`x402Version` 2). `content[0].text` is that same object as JSON.
3. `accepts[0]` is the `exact` scheme on the configured network (`eip155:84532` for Base Sepolia, `eip155:8453` for Base). `amount` is USDC's atomic units (6 decimals, so `$0.02` is `"20000"`). `asset` is the USDC contract. `payTo` is `PAY_TO_ADDRESS`. `extra.name` and `extra.version` are the EIP-712 domain for USDC.
4. The client signs an EIP-3009 `transferWithAuthorization` for that amount and recipient, and retries the **same** `tools/call` with the payload on `params._meta["x402/payment"]`.
5. This server asks the facilitator to verify, then to settle (it retries once if settlement fails for a transient reason). Only after settlement succeeds does the tool run. A failed settle returns `PaymentRequired` and never returns paid tool data. The tool result includes the receipt at `_meta["x402/payment-response"]` (`success`, `transaction`, `network`, `payer`).
A client that cannot pay stops at step 2. It is not charged.
The Python package `x402.mcp` (`create_x402_mcp_client`) does steps 3–5 for an agent that holds an EVM key. This server uses x402's resource server, scheme, and facilitator client. It does not use x402's FastMCP decorator: that decorator imports `mcp.server.fastmcp`, which mcp 2.3 removed when FastMCP was renamed `MCPServer`.
Example challenge (values depend on your env):
```json
{
"isError": true,
"structuredContent": {
"x402Version": 2,
"error": "Payment required to access this resource ($0.02 USDC on eip155:84532)",
"resource": {"url": "mcp://tool/rug_risk", "mimeType": "application/json"},
"accepts": [{
"scheme": "exact",
"network": "eip155:84532",
"amount": "20000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0xYourAddress",
"maxTimeoutSeconds": 60,
"extra": {"name": "USDC", "version": "2"}
}]
}
}
```
Retry shape:
```json
{
"method": "tools/call",
"params": {
"name": "rug_risk",
"arguments": {"token_address": "...", "chain": "solana"},
"_meta": {"x402/payment": {"x402Version": 2, "accepted": {}, "payload": {}, "resource": {}}}
}
}
```
The public facilitator at `https://x402.org/facilitator` is the default for Base Sepolia and needs no API key. Coinbase's CDP facilitator (`https://api.cdp.coinbase.com/platform/v2/x402`) requires `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET`. JWT auth is attached only when `FACILITATOR_URL` points at that CDP host, so you can store the keys now and keep testnet on x402.org. This process never holds a payment signing key and never sends a payment itself.
To go to Base mainnet after a testnet payment works: set `X402_NETWORK=base` and `FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402` in `.env.prod`, then restart the container. Prices and `PAY_TO_ADDRESS` can stay the same if that address is the wallet you control on Base.
### Daily free allowance
Paid tools (`rug_risk`, `market_brief`, `top_movers`, `funding_rates`) allow `FREE_PAID_CALLS_PER_DAY` free calls per client per UTC day (default 3, reset at 00:00 UTC). A client is the request IP plus the optional `X-Client-Id` header. After the allowance is used, those tools require x402 payment. Remaining calls are returned in `_meta.freePaidCallsRemaining` (also `freePaidCallsLimit` and `freePaidCallsResetsAt`). Set `FREE_PAID_CALLS_PER_DAY=0` to require payment on every paid-tool call. The count is stored in `FREE_TIER_PATH` (default `/app/data/free_tier.sqlite`) so a restart does not reset the day. Owner keys in `FREE_API_KEYS` still skip both the charge and the daily allowance.
### Owner API key
Set `FREE_API_KEYS` to a comma-separated list. A request with `Authorization: Bearer <key>` or `X-API-Key: <key>` skips the charge and the free-tool rate limit. Anyone without a key still has to pay for paid tools.
## Connect a client
Remote URL, Cursor (`.cursor/mcp.json`):
```json
{
"mcpServers": {
"crypto-market": {
"url": "https://2-31-102-176.sslip.io/mcp"
}
}
}
```
Remote URL, Claude Desktop:
```json
{
"mcpServers": {
"crypto-market": {
"type": "http",
"url": "https://2-31-102-176.sslip.io/mcp"
}
}
}
```
Add the owner header only on clients that should skip payment:
```json
{
"mcpServers": {
"crypto-market": {
"url": "https://2-31-102-176.sslip.io/mcp",
"headers": {"Authorization": "Bearer YOUR_FREE_API_KEY"}
}
}
}
```
Local stdio:
```json
{
"mcpServers": {
"crypto-market": {
"command": "python",
"args": ["-m", "crypto_mcp"]
}
}
}
```
## Environment
Copy `.env.example`. Do not commit `.env`.
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `PAY_TO_ADDRESS` | When payments are on | empty | EVM address that receives USDC |
| `X402_NETWORK` | no | `base-sepolia` | `base-sepolia`, `base`, or `eip155:<id>` |
| `FACILITATOR_URL` | no | `https://x402.org/facilitator` | x402 facilitator. Use the CDP URL for mainnet. |
| `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET` | When using the CDP facilitator | empty | Coinbase CDP API keys. Never commit them. |
| `PAYMENTS_ENABLED` | no | on for HTTP, off for stdio | Force the gate on or off |
| `FREE_API_KEYS` | no | empty | Comma-separated owner keys |
| `FREE_PAID_CALLS_PER_DAY` | no | `3` | Free paid-tool calls per client per UTC day. `0` disables the allowance. |
| `FREE_TIER_PATH` | no | `/app/data/free_tier.sqlite` | Persisted free-tier counters |
| `PRICE_RUG_RISK` | no | `$0.02` | Per-tool price override |
| `PRICE_MARKET_BRIEF` | no | `$0.03` | Per-tool price override |
| `PRICE_TOP_MOVERS` | no | `$0.005` | Per-tool price override |
| `PRICE_FUNDING_RATES` | no | `$0.005` | Per-tool price override |
| `X402_MAX_TIMEOUT_SECONDS` | no | `60` | Authorization lifetime advertised to the client |
| `INFERHUB_API_KEY` | no | empty | Bearer token for `market_brief` |
| `INFERHUB_MODEL` | with the key | empty | Model name sent to InferHub |
| `INFERHUB_BASE_URL` | no | `https://api.inferhub.dev/v1` | OpenAI-compatible base URL |
| `HTTP_TIMEOUT_SECONDS` | no | `10` | Upstream call timeout |
| `CACHE_TTL_SECONDS` | no | `30` | In-memory TTL for upstream GETs |
| `RATE_LIMIT_FREE_PER_MINUTE` | no | `60` | Per-IP limit for free tools on HTTP |
| `HOST` | no | `0.0.0.0` | Bind address |
| `PORT` | no | `8080` | Bind port. Railway sets this |
`get_price` and `fear_greed` stay free. There is no price env for them.
Free tools are limited per client IP. Behind Railway the first `X-Forwarded-For` hop is the client. Upstream GETs are cached for `CACHE_TTL_SECONDS`. InferHub completions are not cached. Every upstream call uses `HTTP_TIMEOUT_SECONDS`.
## Deploy on Railway
This repo is set up for Railway and is not deployed from here.
1. Push the project to a Git repository you control.
2. Create a Railway service from that repo. `railway.json` selects the Dockerfile, checks `GET /health`, and restarts on failure.
3. In the service variables, set `PAY_TO_ADDRESS=0x49Ac893403009239F96a188d55C2FD40f77A8195` (the owner's Base wallet; EIP-55 checksum checked) and leave `X402_NETWORK=base-sepolia` for testing. Set `FACILITATOR_URL` if you are not using the public facilitator. Set `INFERHUB_API_KEY` and `INFERHUB_MODEL` only if you want model-written briefs. Set `FREE_API_KEYS` if you want an owner bypass.
4. Generate a public domain. The image listens on Railway's `PORT`.
5. Open `https://<domain>/health`. You should see `"status": "ok"`.
6. Point Cursor or Claude at `https://<domain>/mcp`.
7. For mainnet, after a successful Sepolia paid call, set `X402_NETWORK=base` and `FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402` (CDP keys already in the environment), then restart. Try one paid tool on Sepolia first.
`server.json` describes the hosted Streamable HTTP server for the official MCP Registry (`io.github.gojisatoshi888/crypto-market`). `smithery.yaml` is historical listing metadata only; Smithery's publish API and CLI shut down on 31 October 2026.
## Tests
```bash
pytest
```
Payment tests check that an unpaid paid-tool call returns `PaymentRequired` with the configured amount, network, asset, and `payTo`; that free tools run; and that `FREE_API_KEYS` skips the gate. They do not call a facilitator and do not sign a transaction. `tests/test_network.py` calls CoinGecko and alternative.me and skips itself if those hosts are blocked.
## Layout
- `python -m crypto_mcp` — stdio
- `python -m crypto_mcp.http_server` — Streamable HTTP on `/mcp`, health on `/health`
- `crypto_mcp/payments.py` — x402 challenge, verify, settle
- `crypto_mcp/market.py` — vendor calls
- `crypto_mcp/scoring.py` — volume flags and the rug score
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues