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 spin up a crypto payment order, hand you the hosted checkout URL, and verify the inbound payment callback through a natural language conversation? No dashboard switching, no hand-crafted curl commands, 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 Aurpay’s REST API. By the end, you will have four working tools that Claude can call on demand: listing the chain/currency combinations Aurpay supports, creating a typed payment order via /api/order/pay-info, creating a chain-picker hosted checkout via /api/order/pay-url, and verifying inbound callbacks using Aurpay’s signed-header scheme.

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:

  • list_supported_currencies – Returns the (chain, currency) pairs you can quote through /api/order/pay-info (e.g. USDT-TRC20, USDC-ERC20, BTC)
  • create_payment_order – Calls Aurpay’s /api/order/pay-info to create a new payment order with a specified chain, currency, and USD amount, returning the hosted-payment URL and on-chain receiving address
  • get_payment_url – Calls Aurpay’s /api/order/pay-url when you want the buyer to pick the chain and currency on the hosted checkout
  • summarize_order_callback – Verifies an inbound Aurpay callback (Callback-Token, Date, Signature headers) and returns a human-readable summary of what changed

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 a payment order and explain the callback when it arrives,” and it will chain the create_payment_order and summarize_order_callback 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.
  • An Aurpay API key. Generated from the Aurpay dashboard; it travels in the API-Key header on every request. See the integration docs for the signature-auth alternative recommended for production.
  • 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://dashboard.aurpay.net";
const API_KEY = process.env.PAYMENT_API_KEY || "";
const CALLBACK_URL = process.env.PAYMENT_CALLBACK_URL || "";
const CALLBACK_SECRET = process.env.PAYMENT_CALLBACK_SECRET || "";
const CALLBACK_TOKEN = process.env.PAYMENT_CALLBACK_TOKEN || "";

const SUPPORTED_PAIRS = [
  { chain: "ETH", currency: "USDT-ERC20" },
  { chain: "TRX", currency: "USDT-TRC20" },
  { chain: "ETH", currency: "USDC-ERC20" },
  { chain: "TRX", currency: "USDC-TRC20" },
  { chain: "ETH", currency: "DAI-ERC20"  },
  { chain: "ETH", currency: "ETH"        },
  { chain: "BTC", currency: "BTC"        },
  { chain: "BNB", currency: "BNB"        },
] as const;

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. The first tool just lists the chain/currency combinations Aurpay accepts — Claude uses it as a sanity check before composing a payment order.

server.tool(
  "list_supported_currencies",
  "List the (chain, currency) pairs that Aurpay accepts for payment orders. Useful as a sanity check before calling create_payment_order so the agent does not invent unsupported chains like 'USDT on Polygon'.",
  {},
  async () => ({
    content: [
      { type: "text" as const, text: JSON.stringify(SUPPORTED_PAIRS, null, 2) },
    ],
  })
);

The second tool creates a payment order against /api/order/pay-info. The buyer is sent to the pay_url Aurpay returns; settlement lands at address on chain confirmation:

server.tool(
  "create_payment_order",
  "Create a new Aurpay payment order. Returns the order_id, the on-chain receiving address, and the hosted pay_url that you share with the customer.",
  {
    chain: z.enum(["ETH", "TRX", "BTC", "BNB"]).describe("Chain identifier"),
    currency: z.enum([
      "USDT-ERC20", "USDT-TRC20",
      "USDC-ERC20", "USDC-TRC20",
      "DAI-ERC20", "ETH", "BTC", "BNB",
    ]).describe("Currency with network suffix"),
    vs_price: z.number().positive().describe("Fiat amount, denominated in vs_currency"),
    vs_currency: z.string().default("USD").describe("Fiat reference, e.g. USD"),
    order_ref: z.string().describe("Your idempotency key / internal order ref"),
  },
  async ({ chain, currency, vs_price, vs_currency, order_ref }) => {
    const data = await apiRequest("/api/order/pay-info", {
      method: "POST",
      body: {
        chain,
        currency,
        vs_currency,
        vs_price,
        succeed_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=ok`,
        timeout_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        callback_url: `${CALLBACK_URL}?order_ref=${order_ref}`,
        timeout_callback: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        fixed_encrypt_price: true,
        enable_post_callback: false,
      },
    });
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  }
);

The third tool calls /api/order/pay-url for the simpler “let the buyer pick the currency on the hosted checkout” flow. No chain or currency in this request — the buyer chooses on Aurpay’s page.

server.tool(
  "get_payment_url",
  "Create an Aurpay hosted-checkout URL where the buyer picks the chain and currency. Returns the pay_url.",
  {
    price: z.number().positive().describe("Fiat amount"),
    currency: z.string().default("USD").describe("Fiat code, e.g. USD"),
    order_ref: z.string().describe("Your idempotency key / internal order ref"),
  },
  async ({ price, currency, order_ref }) => {
    const data = await apiRequest("/api/order/pay-url", {
      method: "POST",
      body: {
        price,
        currency,
        succeed_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=ok`,
        timeout_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        callback_url: `${CALLBACK_URL}?order_ref=${order_ref}`,
        timeout_callback: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        fixed_encrypt_price: true,
        enable_post_callback: false,
      },
    });
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  }
);

The fourth tool lets Claude verify and explain an inbound Aurpay callback. Pass the headers and full callback URL captured by your HTTP receiver. The signature scheme is HMAC-SHA256 of {date} | {callback_url} using your dashboard callback_secret, base64-encoded.

import { createHmac, timingSafeEqual } from "node:crypto";

function safeEqual(a: string, b: string): boolean {
  const aBuf = Buffer.from(a);
  const bBuf = Buffer.from(b);
  return aBuf.length === bBuf.length && timingSafeEqual(aBuf, bBuf);
}

function verifyCallback(token: string, date: string, sigB64: string, fullUrl: string): boolean {
  if (!safeEqual(token, CALLBACK_TOKEN)) return false;
  const expected = createHmac("sha256", CALLBACK_SECRET)
    .update(`${date} | ${fullUrl}`)
    .digest("base64");
  return safeEqual(expected, sigB64);
}

server.tool(
  "summarize_order_callback",
  "Verify an Aurpay callback (Callback-Token / Date / Signature headers) for a given full URL, then summarize the order status change in plain English.",
  {
    callback_token: z.string(),
    date: z.string().describe("ISO-8601 timestamp from the Date header"),
    signature: z.string().describe("Base64 HMAC-SHA256 signature from the Signature header"),
    full_url: z.string().url().describe("The full callback URL exactly as Aurpay called it"),
  },
  async ({ callback_token, date, signature, full_url }) => {
    const ok = verifyCallback(callback_token, date, signature, full_url);
    if (!ok) {
      return {
        content: [{ type: "text" as const, text: "Signature INVALID. Reject this callback." }],
      };
    }
    const parsed = new URL(full_url);
    const order_id = parsed.searchParams.get("order_id");
    const status   = parsed.searchParams.get("status"); // "succeed" | "timeout"
    return {
      content: [
        {
          type: "text" as const,
          text: `Signature OK. Order ${order_id ?? "(missing)"} status: ${status ?? "(missing)"}.`,
        },
      ],
    };
  }
);

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> = {
    "API-Key": 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. Aurpay’s REST API authenticates with an API-Key header on every request; the production-recommended alternative is a signature scheme that hashes {algorithm} | {date} | {request_info} | {body_md5} with HMAC-SHA256 and presents it in API-Token, Algorithm, Date, Body-MD5, and Signature headers. Drop the latter in once the integration is stable.

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 is invalid or revoked — re-issue one in the Aurpay dashboard.”

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-aurpay-api-key",
        "PAYMENT_API_URL": "https://dashboard.aurpay.net",
        "PAYMENT_CALLBACK_URL": "https://your.public.host/callbacks/aurpay",
        "PAYMENT_CALLBACK_SECRET": "your-dashboard-callback-secret",
        "PAYMENT_CALLBACK_TOKEN": "your-dashboard-callback-token"
      }
    }
  }
}

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-aurpay-api-key",
        "PAYMENT_API_URL": "https://dashboard.aurpay.net",
        "PAYMENT_CALLBACK_URL": "https://your.public.host/callbacks/aurpay",
        "PAYMENT_CALLBACK_SECRET": "your-dashboard-callback-secret",
        "PAYMENT_CALLBACK_TOKEN": "your-dashboard-callback-token"
      }
    }
  }
}

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: “Which chain/currency combinations can I quote on Aurpay?”

Claude calls list_supported_currencies and returns something like:

Aurpay accepts these pairs:
• USDT-ERC20 on chain ETH
• USDT-TRC20 on chain TRX
• USDC-ERC20 on chain ETH
• USDC-TRC20 on chain TRX
• DAI-ERC20 on chain ETH
• ETH, BTC, and BNB on their native chains

Polygon, Arbitrum, and BSC versions of USDT are not part of the supported set.

You: “Create a 500 USD order in USDT on TRC-20 for the quarterly hosting payment.”

Claude calls create_payment_order with {"chain": "TRX", "currency": "USDT-TRC20", "vs_price": 500, "order_ref": "hosting-q3"}:

Order created:
• Order ID: ord_7f3a8b2c
• vs_price: 500 USD (price-locked, fixed_encrypt_price=true)
• Receiving address: TYz... (TRC-20)
• Pay URL: https://dashboard.aurpay.net/#/cashier/choose?token=...

Share the pay URL with your customer. Aurpay will call your callback_url once the on-chain payment confirms.

You: “Got a callback at this URL with these headers — is it real?”

Claude calls summarize_order_callback, the server verifies the signature, and Claude replies:

Signature OK. Order ord_7f3a8b2c status: succeed.

You can safely mark this order paid in your system. The signature was HMAC-SHA256 of
"{date} | {callback_url}" using your dashboard callback_secret, and it matched.

You: “Make me a generic hosted checkout for $50 — let the buyer pick the chain.”

Claude calls get_payment_url with {"price": 50, "currency": "USD", "order_ref": "consult-50"}:

Hosted checkout ready:
• Pay URL: https://dashboard.aurpay.net/#/cashier/choose?token=...
• vs_price: $50 USD (price-locked)
• Buyer picks chain + currency on the hosted page.

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 the core order-creation and callback-verification workflow. For a richer setup, you will likely want a couple more tools that match what the Aurpay API actually exposes today.

A wrapper around /api/order/pay-info with hard-coded chain defaults is useful when one product line always settles in a single currency (e.g. a SaaS plan always priced in USDT-TRC20). It removes one decision from the agent loop:

server.tool(
  "create_usdt_tron_order",
  "Create an Aurpay order denominated in USD and settled in USDT on TRC-20. Same response shape as create_payment_order. Use this when the product line is fixed to TRC-20.",
  {
    vs_price: z.number().positive(),
    order_ref: z.string(),
  },
  async ({ vs_price, order_ref }) => {
    const data = await apiRequest("/api/order/pay-info", {
      method: "POST",
      body: {
        chain: "TRX",
        currency: "USDT-TRC20",
        vs_currency: "USD",
        vs_price,
        succeed_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=ok`,
        timeout_url: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        callback_url: `${CALLBACK_URL}?order_ref=${order_ref}`,
        timeout_callback: `${CALLBACK_URL}?order_ref=${order_ref}&result=timeout`,
        fixed_encrypt_price: true,
        enable_post_callback: false,
      },
    });
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  }
);

If you want Lightning specifically, do not pass BTC-LN into /api/order/pay-info. Aurpay documents Lightning through the separate /api/bln/order endpoint, with fields such as price, currency, succeed_url, timeout_url, callback_url, and timeout_callback. Keep that as a separate tool so the model does not mix Lightning fields with on-chain order fields.

For refunds, Aurpay’s settlement is non-custodial: there is no merchant-funds account that Aurpay can pull from to refund a buyer. Refunds in practice are merchant-initiated outbound transfers from the wallet that received the original payment, and they live in your own treasury workflow rather than as a single REST call. Don’t expose a generic refund_transaction tool — it would imply behavior the gateway doesn’t provide.

Security Considerations

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

  • API key handling. Treat the Aurpay API-Key like a production secret. For high-traffic deployments, switch from API-Key to the signature scheme (API-Token + HMAC-SHA256 over {algorithm} | {date} | {request_info} | {body_md5}) so a leaked log line is harder to replay.
  • 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 or callback secrets. 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, USDC, DAI, BNB, and Lightning through documented order endpoints. 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.