Skip to main content
Glama
Crypto Market MCP

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