Build a Crypto Payment MCP Server: Connect Claude to Your Payment Gateway

Build a Crypto Payment MCP Server: Connect Claude to Your Payment Gateway

Build a Crypto Payment MCP Server: Connect Claude to Your Payment Gateway

What if Claude could check your wallet balance, create a crypto invoice, and pull up transaction history through a natural language conversation? No dashboard switching, no copy-pasting API keys into Postman, no writing one-off scripts. You just ask, and it does. That is exactly what a Model Context Protocol (MCP) server enables. In this tutorial, you will build a Node.js MCP server that connects Claude to a crypto payment gateway’s REST API. By the end, you will have four working tools that Claude can call on demand: checking balances, creating invoices, querying payment status, and listing recent transactions.

What We’re Building

An MCP server is a lightweight process that exposes structured tools to an AI agent. Claude communicates with it over stdio using JSON-RPC, and the server translates those requests into real API calls against your payment gateway backend.

Here is what our server will expose:

  • check_balance – Returns the current balance for a given cryptocurrency (BTC, ETH, USDT, etc.)
  • create_invoice – Generates a new payment invoice with a specified amount, currency, and description
  • get_payment_status – Looks up the status of a specific payment by its ID
  • list_recent_transactions – Retrieves the latest transactions with optional filtering by currency and status

The architecture is straightforward. Claude sends a tool call to the MCP server over stdio using JSON-RPC. The server validates the input against Zod schemas, makes an HTTP request to the payment gateway API, and returns a structured response that Claude can interpret and relay to you in plain language.

Why build this as an MCP server rather than a standalone script? First, Claude can compose these tools together. Ask it to “create an invoice and then check back in on its status,” and it will chain the create_invoice and get_payment_status calls without you writing any orchestration logic. Second, the MCP protocol is standardized. The server you build today works with Claude Desktop, Claude Code, and any other MCP-compatible client, including those from other vendors adopting the protocol.

Prerequisites

Before you start, make sure you have the following:

  • Node.js 18 or later. The MCP SDK requires a modern Node runtime. Check your version with node --version.
  • npm. Comes with Node.js. You will use it to install dependencies.
  • Claude Desktop or Claude Code. Either client supports MCP servers. Claude Code is ideal for developer workflows.
  • A crypto payment gateway API key. This tutorial uses Aurpay’s API as the example, but the patterns apply to any REST-based payment gateway.
  • Basic understanding of MCP. If MCP is new to you, read our explainer first.

Step 1 — Project Setup

Create a new directory and initialize the project. The MCP TypeScript SDK is the official library maintained by Anthropic, and Zod handles runtime schema validation for tool inputs.

mkdir crypto-payment-mcp-server
cd crypto-payment-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Update your package.json to use ES modules and add a build script:

{
  "name": "crypto-payment-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0",
    "zod": "^3.24.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.7.0"
  }
}

Create a tsconfig.json that targets modern Node:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Your directory structure should look like this:

crypto-payment-mcp-server/
├── package.json
├── tsconfig.json
└── src/
    └── server.ts

Step 2 — Define Your Tools

Each MCP tool needs three things: a name, a description that helps Claude understand when to use it, and a Zod schema that validates the input parameters. Good descriptions matter. Claude uses them to decide which tool to call, so be specific about what each tool does and what it returns.

Create src/server.ts and start with the imports and tool definitions:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const API_BASE = process.env.PAYMENT_API_URL || "https://api.aurpay.net/v1";
const API_KEY = process.env.PAYMENT_API_KEY || "";

if (!API_KEY) {
  console.error("PAYMENT_API_KEY environment variable is required");
  process.exit(1);
}

// Create the MCP server instance
const server = new McpServer({
  name: "crypto-payment-gateway",
  version: "1.0.0",
});

Now define each tool. The server.tool() method takes the tool name, a Zod schema object for parameters, and an async handler function. Here is the first tool:

server.tool(
  "check_balance",
  "Check the current balance for a specific cryptocurrency in your payment gateway wallet. Returns the available and pending balance.",
  {
    currency: z.string().describe(
      "The cryptocurrency ticker symbol, e.g. BTC, ETH, USDT, USDC"
    ),
  },
  async ({ currency }) => {
    const data = await apiRequest(`/wallets/balance?currency=${currency.toUpperCase()}`);
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(data, null, 2),
        },
      ],
    };
  }
);

The second tool creates invoices. It takes more parameters, so the schema is richer:

server.tool(
  "create_invoice",
  "Create a new payment invoice for a customer. Returns the invoice ID, payment address, and a payment link that can be shared with the customer.",
  {
    amount: z.number().positive().describe("The payment amount in the specified currency"),
    currency: z.string().describe("The cryptocurrency to accept, e.g. BTC, ETH, USDT"),
    description: z.string().optional().describe("A note or memo for this invoice"),
    customer_email: z.string().email().optional().describe("Customer email for payment notifications"),
  },
  async ({ amount, currency, description, customer_email }) => {
    const data = await apiRequest("/invoices", {
      method: "POST",
      body: {
        amount,
        currency: currency.toUpperCase(),
        description: description || "",
        customer_email: customer_email || null,
      },
    });
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(data, null, 2),
        },
      ],
    };
  }
);

The third tool queries a specific payment by ID:

server.tool(
  "get_payment_status",
  "Look up the current status of a payment by its payment ID. Returns the payment amount, currency, status (pending, confirmed, settled, expired, failed), confirmation count, and settlement timestamp if completed.",
  {
    payment_id: z.string().describe("The unique payment identifier returned when the invoice was created"),
  },
  async ({ payment_id }) => {
    const data = await apiRequest(`/payments/${payment_id}`);
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(data, null, 2),
        },
      ],
    };
  }
);

The fourth tool lists recent transactions with optional filters:

server.tool(
  "list_recent_transactions",
  "List recent transactions from the payment gateway. Returns the most recent transactions with amount, currency, status, and timestamps. Supports filtering by currency and status.",
  {
    currency: z.string().optional().describe("Filter by cryptocurrency, e.g. BTC, ETH, USDT"),
    status: z.enum(["pending", "confirmed", "settled", "expired", "failed"]).optional()
      .describe("Filter by transaction status"),
    limit: z.number().min(1).max(100).default(10).describe("Number of transactions to return, max 100"),
  },
  async ({ currency, status, limit }) => {
    const params = new URLSearchParams();
    if (currency) params.set("currency", currency.toUpperCase());
    if (status) params.set("status", status);
    params.set("limit", String(limit));

    const data = await apiRequest(`/transactions?${params.toString()}`);
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(data, null, 2),
        },
      ],
    };
  }
);

Step 3 — Implement the API Client

All four tools share a common HTTP client. Add this helper function above the tool definitions in src/server.ts. It handles authentication, error responses, and JSON parsing in one place.

interface ApiRequestOptions {
  method?: string;
  body?: Record<string, unknown>;
}

async function apiRequest(
  endpoint: string,
  options: ApiRequestOptions = {}
): Promise<unknown> {
  const { method = "GET", body } = options;

  const headers: Record<string, string> = {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Accept": "application/json",
  };

  const response = await fetch(`${API_BASE}${endpoint}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(
      `API request failed: ${response.status} ${response.statusText} — ${errorText}`
    );
  }

  return response.json();
}

A few things worth noting. The fetch API is available natively in Node.js 18+, so you do not need axios or node-fetch. The Bearer token pattern is standard for payment gateway APIs. If your gateway uses a different authentication scheme (HMAC signatures, API key headers), adjust the headers object accordingly.

Error handling matters for MCP servers. When an API call fails, the error message propagates back to Claude, which then explains the problem to you in natural language. A vague “request failed” message is useless. That is why the error includes the HTTP status code, status text, and the full response body. Claude can use that information to suggest fixes: “It looks like your API key does not have permission to access the wallets endpoint. You may need to generate a new key with read:wallets scope.”

For production use, consider adding retry logic for transient failures (429 rate limits, 503 service unavailable) and a request timeout. The native fetch in Node.js 18+ supports an AbortSignal for timeouts:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout

const response = await fetch(`${API_BASE}${endpoint}`, {
  method,
  headers,
  body: body ? JSON.stringify(body) : undefined,
  signal: controller.signal,
});
clearTimeout(timeout);

Step 4 — Wire Up the Server

The final piece is connecting the server to a transport. MCP servers communicate over stdio by default. The client (Claude) launches the server as a subprocess and exchanges JSON-RPC messages through stdin/stdout.

Add this at the bottom of src/server.ts:

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Crypto payment MCP server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error starting server:", error);
  process.exit(1);
});

Notice that the log message uses console.error, not console.log. This is critical. The stdio transport uses stdout for JSON-RPC messages. Any stray console.log call will corrupt the protocol stream and break the connection. Always write diagnostic output to stderr.

Here is the complete src/server.ts file for reference. You can copy the full listing from the sections above, or grab it from the companion repository.

Build the project to confirm everything compiles:

npx tsc

If you see no errors, you are ready to connect Claude.

Step 5 — Configure Claude

How you register the MCP server depends on your Claude client.

Claude Desktop

Open your claude_desktop_config.json file. On macOS, it lives at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows, check %APPDATA%\Claude\claude_desktop_config.json.

Add your server to the mcpServers object:

{
  "mcpServers": {
    "crypto-payments": {
      "command": "node",
      "args": ["/absolute/path/to/crypto-payment-mcp-server/dist/server.js"],
      "env": {
        "PAYMENT_API_KEY": "your-gateway-api-key",
        "PAYMENT_API_URL": "https://api.your-gateway.com/v1"
      }
    }
  }
}

Restart Claude Desktop after saving the file. The server should appear in the tools menu.

Claude Code

If you use Claude Code, add the server configuration to your project’s .mcp.json file in the repository root:

{
  "mcpServers": {
    "crypto-payments": {
      "command": "node",
      "args": ["./crypto-payment-mcp-server/dist/server.js"],
      "env": {
        "PAYMENT_API_KEY": "your-gateway-api-key",
        "PAYMENT_API_URL": "https://api.your-gateway.com/v1"
      }
    }
  }
}

Claude Code picks up .mcp.json automatically when you open the project directory. For more on integrating MCP into your Claude Code development workflow, see our Web3 development guide.

Testing with MCP Inspector

Before connecting to Claude, you can verify your server works with the MCP Inspector, a browser-based debugging tool:

PAYMENT_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/server.js

The Inspector opens a web UI where you can see all registered tools, test them with sample inputs, and inspect the JSON-RPC messages flowing between client and server. It is the fastest way to catch schema errors before Claude gets involved.

Step 6 — Test It

With the server configured, open Claude and try some natural language queries. Here is what the interaction looks like in practice.

You: “What’s my current USDT balance?”

Claude calls check_balance with {"currency": "USDT"} and returns something like:

Your USDT balance:
• Available: 12,450.00 USDT
• Pending: 380.00 USDT (2 incoming transactions awaiting confirmation)
• Total: 12,830.00 USDT

You: “Create an invoice for 500 USDT for the quarterly hosting payment.”

Claude calls create_invoice with {"amount": 500, "currency": "USDT", "description": "Quarterly hosting payment"}:

Invoice created:
• Invoice ID: inv_7f3a8b2c
• Amount: 500.00 USDT
• Payment address: 0x1a2b3c4d5e6f...
• Payment link: https://pay.aurpay.net/inv_7f3a8b2c
• Expires: 2026-03-08 14:30 UTC

Share the payment link with your customer. You'll be notified when the payment is confirmed.

You: “Show me all pending transactions.”

Claude calls list_recent_transactions with {"status": "pending", "limit": 10}:

You have 3 pending transactions:

1. 0.15 BTC — inv_4d2e1f0a — Created 2 hours ago, 2/6 confirmations
2. 500.00 USDT — inv_7f3a8b2c — Created just now, awaiting first confirmation
3. 2.5 ETH — inv_9c8b7a6d — Created 45 minutes ago, 8/12 confirmations

Transaction #3 should settle within the next few minutes based on current block times.

You: “What’s the status of payment inv_4d2e1f0a?”

Claude calls get_payment_status with {"payment_id": "inv_4d2e1f0a"}:

Payment inv_4d2e1f0a:
• Amount: 0.15 BTC ($9,735.00 at time of creation)
• Status: Pending (2 of 6 required confirmations)
• Created: 2026-03-07 10:15 UTC
• Customer: [email protected]
• Estimated settlement: ~40 minutes based on current Bitcoin block times

Notice how Claude interprets the raw API data and presents it in a human-readable format. It adds context: estimating settlement time based on confirmation counts, converting BTC to dollar values, flagging transactions that are close to settling. You did not write any formatting logic. The MCP server returns raw JSON; Claude handles the presentation layer.

This is the practical advantage. A traditional dashboard gives you a fixed UI that someone designed months ago. An MCP-connected agent gives you a conversational interface that adapts to your question. You can ask follow-ups like “which of those pending transactions is closest to confirming?” or “create another invoice for the same amount but in ETH this time,” and Claude composes the right tool calls without you navigating menus or filling forms.

If something goes wrong, say the API returns an error because a payment ID does not exist — Claude will relay the error and suggest next steps. It might say: “That payment ID was not found. Do you want me to list recent transactions so you can find the correct one?” The error handling you built into the apiRequest function gives Claude enough context to be helpful rather than just displaying a stack trace.

Going Further: Adding Write Operations

The four tools above cover read-heavy workflows. For a production setup, you will likely want write operations too. Here are two useful additions.

A payment link tool creates a shareable URL that accepts payments without requiring an invoice. Useful for recurring payments or donation pages:

server.tool(
  "create_payment_link",
  "Create a reusable payment link that customers can use to pay any amount in a specified cryptocurrency.",
  {
    currency: z.string().describe("Accepted cryptocurrency, e.g. USDT, BTC"),
    fixed_amount: z.number().positive().optional()
      .describe("Fixed payment amount. If omitted, the customer chooses the amount."),
    label: z.string().describe("A label for this payment link, e.g. 'Monthly subscription'"),
  },
  async ({ currency, fixed_amount, label }) => {
    const data = await apiRequest("/payment-links", {
      method: "POST",
      body: { currency: currency.toUpperCase(), fixed_amount, label },
    });
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  }
);

A refund tool initiates a refund for a settled payment. This is a sensitive operation that deserves extra safeguards:

server.tool(
  "refund_transaction",
  "Initiate a refund for a settled payment. Requires the original payment ID and a reason. The refund will be sent to the original sender address.",
  {
    payment_id: z.string().describe("The payment ID to refund"),
    reason: z.string().describe("Reason for the refund"),
  },
  async ({ payment_id, reason }) => {
    const data = await apiRequest(`/payments/${payment_id}/refund`, {
      method: "POST",
      body: { reason },
    });
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  }
);

Security Considerations

Write operations bring additional responsibility. Keep these practices in mind:

  • API key scoping. Use an API key with the minimum permissions required. If the server only needs to read balances and transactions, do not give it refund access. Aurpay’s API supports scoped API keys for exactly this purpose.
  • Rate limiting. Add client-side rate limiting to prevent accidental floods. A simple token bucket or fixed-window counter in the apiRequest function is enough.
  • Confirmation prompts. Claude will ask for confirmation before calling destructive tools if you include words like “refund” or “delete” in the tool description. Lean into this behavior by making your descriptions explicit about side effects.
  • Audit logging. Log every tool call with its parameters and response to stderr. This gives you a trail when something goes wrong.
  • Environment isolation. Never hardcode API keys. The env block in the MCP config passes them as environment variables, keeping secrets out of your codebase. For details on security workflows with Claude Code, see our auditing guide.

For a more advanced implementation that monitors real-time blockchain events, see our tutorial on building a Lightning Network payment monitor as an MCP server.

Start Building with Crypto Payments

Aurpay’s non-custodial payment gateway gives you API access to accept BTC, ETH, USDT, and 10+ cryptocurrencies. No middleman holds your funds. Get your API keys and start integrating.

Ricky

Growth Strategist at Aurpay

As a growth strategist at Aurpay, Ricky is dedicated to removing the friction between traditional commerce and blockchain technology. He helps merchants navigate the complex landscape of Web3 payments, ensuring seamless compliance while executing high-impact marketing campaigns. Beyond his core responsibilities, he is a relentless experimenter, constantly testing new growth tactics and tweaking product UX to maximize conversion rates and user satisfaction

Sign Up for Our Newsletter

Get the latest crypto news and updates from the experts at Aurpay.