Skip to main content
Most MCP clients (Claude Desktop, Cursor, etc.) don’t have x402 payment support built in. This recipe creates a local proxy that sits between your MCP client and a paywalled MCP server, automatically handling payments on each tool call.

How it works

MCP Client  →  Local Proxy (:8402)  →  Paywalled MCP Server
              (pays via x402)
The proxy intercepts outgoing requests and routes them along two paths:
  • Free path — protocol-level MCP methods (initialize, tools/list, prompts/list, resources/list) are forwarded directly using plain fetch. These are handshake/discovery calls that never cost anything.
  • Paid path — everything else (tools/call, prompts/get, resources/read, etc.) is sent through payer.fetch, which automatically detects a 402 Payment Required response, settles the x402 payment on-chain, and retries the request with proof of payment.

Installation

pnpm add express dotenv @faremeter/rides @faremeter/logs

The proxy

mcp-proxy.ts
import "dotenv/config"
import express from "express"
import { payer } from "@faremeter/rides"
import { getLogger } from "@faremeter/logs"

const logger = await getLogger(["mcp-proxy"])

await payer.addLocalWallet(process.env.PAYER_KEYPAIR_PATH)

const app = express()
app.use(express.json())

const MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? "https://your-mcp-server.com/mcp"

// ── Free path ────────────────────────────────────────────────
// Protocol-level methods: forwarded with plain fetch (no payment)
const FREE_METHODS = ["initialize", "tools/list", "prompts/list", "resources/list"]

// ── Paid path ────────────────────────────────────────────────
// Everything else (tools/call, prompts/get, resources/read, …)
// goes through payer.fetch, which handles the 402 → pay → retry flow

app.all("/mcp", async (req, res) => {
  const method = req.body?.method
  const shouldPay = !FREE_METHODS.includes(method)

  logger.info(`${method}${shouldPay ? "paying" : "free"}`)

  // Free methods use plain fetch; paid methods use payer.fetch
  const fetchFn = shouldPay ? payer.fetch : fetch
  const response = await fetchFn(MCP_SERVER_URL, {
    method: req.method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req.body),
  })

  const data = await response.text()
  res.status(response.status).send(data)
})

app.listen(8402, () => {
  logger.info("MCP proxy running on http://localhost:8402/mcp")
})

What each path looks like

The client discovers available tools — no payment involved:
Client → Proxy → MCP Server
                  ← 200 { tools: [...] }
        ← 200
The proxy uses plain fetch and passes the response straight through.

Running it

PAYER_KEYPAIR_PATH=~/.config/solana/id.json \
MCP_SERVER_URL=https://your-mcp-server.com/mcp \
pnpm tsx mcp-proxy.ts
Point your MCP client to http://localhost:8402/mcp — the proxy handles payments transparently.

Claude Desktop configuration

Add the proxy as a custom MCP server in your Claude Desktop config:
{
  "mcpServers": {
    "paid-api": {
      "url": "http://localhost:8402/mcp"
    }
  }
}