Skip to content

Accept payments with MPP

Last updated View as MarkdownAgent setup

Use Cloudflare Workers to accept Machine Payments Protocol (MPP) payments. Choose an integration based on the service you want to protect:

  • mpp-proxy — Charge for HTTP content without changing your origin code. Refer to Charge for HTTP content.
  • Worker route — Add mppx payment middleware to a Worker application.
  • MCP tool — Require payment before an MCP tool returns its result.

Prerequisites

Create a Cloudflare account. You also need a payment recipient and an MPP secret key.

The examples use a stablecoin payment method on testnet. For other methods, refer to MPP payment methods.

Charge for a Worker route

Add mppx middleware when you control the Worker application:

  1. Install Hono and mppx in the Worker project:

    npm i hono mppx
  2. Store the MPP signing key as a Worker secret:

    npx wrangler secret put MPP_SECRET_KEY
  3. Add the payment middleware before the paid route handler:

    src/index.jsjs
    import { env } from "cloudflare:workers";
    import { Hono } from "hono";
    import { Mppx, tempo } from "mppx/hono";
    
    const workerEnv = env;
    const app = new Hono();
    const mppx = Mppx.create({
    	methods: [tempo.charge({ testnet: true })],
    	secretKey: workerEnv.MPP_SECRET_KEY,
    });
    
    app.get(
    	"/premium",
    	mppx.charge({
    		amount: "0.01",
    		currency: "0x20c0000000000000000000000000000000000000",
    		description: "Premium API access",
    		recipient: "<YOUR_WALLET_ADDRESS>",
    	}),
    	(c) => c.json({ access: "paid" }),
    );
    
    export default app;
    src/index.tsts
    import { env } from "cloudflare:workers";
    import { Hono } from "hono";
    import { Mppx, tempo } from "mppx/hono";
    
    const workerEnv = env as Env & { MPP_SECRET_KEY: string };
    const app = new Hono();
    const mppx = Mppx.create({
      methods: [tempo.charge({ testnet: true })],
      secretKey: workerEnv.MPP_SECRET_KEY,
    });
    
    app.get(
      "/premium",
      mppx.charge({
        amount: "0.01",
        currency: "0x20c0000000000000000000000000000000000000",
        description: "Premium API access",
        recipient: "<YOUR_WALLET_ADDRESS>",
      }),
      (c) => c.json({ access: "paid" }),
    );
    
    export default app;
  4. Deploy the Worker:

    npx wrangler deploy
  5. Request the paid route without a payment:

    curl -i https://<YOUR_WORKER>.workers.dev/premium

    The Worker returns 402 Payment Required and a WWW-Authenticate: Payment header. A paid retry reaches the handler and returns a Payment-Receipt header.

Charge for an MCP tool

Add the MPP transport to an McpAgent:

  1. Install the required packages in an Agents project:

    npm i agents mppx @modelcontextprotocol/sdk zod
  2. Bind the McpAgent Durable Object in the Wrangler configuration:

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "mpp-server",
      "main": "src/index.ts",
      // Set this to today's date
      "compatibility_date": "2026-08-13",
      "compatibility_flags": [
        "nodejs_compat"
      ],
      "durable_objects": {
        "bindings": [
          {
            "name": "MCP_OBJECT",
            "class_name": "PaidMCP"
          }
        ]
      },
      "migrations": [
        {
          "tag": "v1",
          "new_sqlite_classes": [
            "PaidMCP"
          ]
        }
      ]
    }
    name = "mpp-server"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-08-13"
    compatibility_flags = ["nodejs_compat"]
    
    [[durable_objects.bindings]]
    name = "MCP_OBJECT"
    class_name = "PaidMCP"
    
    [[migrations]]
    tag = "v1"
    new_sqlite_classes = ["PaidMCP"]
  3. Store MPP_SECRET_KEY as a Worker secret:

    npx wrangler secret put MPP_SECRET_KEY
  4. Check payment before returning the tool result:

    src/index.jsjs
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { McpAgent } from "agents/mcp";
    import { Mppx, tempo, Transport } from "mppx/server";
    import { z } from "zod";
    
    export class PaidMCP extends McpAgent {
    	server = new McpServer({ name: "paid-search", version: "1.0.0" });
    
    	async init() {
    		const mppx = Mppx.create({
    			methods: [tempo.charge({ testnet: true })],
    			secretKey: this.env.MPP_SECRET_KEY,
    			transport: Transport.mcpSdk(),
    		});
    
    		this.server.tool(
    			"premium_search",
    			"Search premium content",
    			{ query: z.string() },
    			async ({ query }, extra) => {
    				const payment = await mppx.charge({
    					amount: "0.01",
    					currency: "0x20c0000000000000000000000000000000000000",
    					description: "Premium search",
    					recipient: "<YOUR_WALLET_ADDRESS>",
    				})(extra);
    
    				if (payment.status === 402) throw payment.challenge;
    
    				return payment.withReceipt({
    					content: [{ type: "text", text: `Results for: ${query}` }],
    				});
    			},
    		);
    	}
    }
    
    export default PaidMCP.serve("/mcp");
    src/index.tsts
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { McpAgent } from "agents/mcp";
    import { Mppx, tempo, Transport } from "mppx/server";
    import { z } from "zod";
    
    type PaymentEnv = Env & { MPP_SECRET_KEY: string };
    
    export class PaidMCP extends McpAgent<PaymentEnv> {
      server = new McpServer({ name: "paid-search", version: "1.0.0" });
    
      async init() {
        const mppx = Mppx.create({
          methods: [tempo.charge({ testnet: true })],
          secretKey: this.env.MPP_SECRET_KEY,
          transport: Transport.mcpSdk(),
        });
    
        this.server.tool(
          "premium_search",
          "Search premium content",
          { query: z.string() },
          async ({ query }, extra) => {
            const payment = await mppx.charge({
              amount: "0.01",
              currency: "0x20c0000000000000000000000000000000000000",
              description: "Premium search",
              recipient: "<YOUR_WALLET_ADDRESS>",
            })(extra);
    
            if (payment.status === 402) throw payment.challenge;
    
            return payment.withReceipt({
              content: [{ type: "text", text: `Results for: ${query}` }],
            });
          },
        );
      }
    }
    
    export default PaidMCP.serve("/mcp");
  5. Deploy the Worker:

    npx wrangler deploy

    An unpaid premium_search call returns an MPP Challenge. A paid retry returns the tool result and an MPP Receipt in _meta.

To test both payment flows from a Cloudflare Agent, refer to Pay from the Agents SDK. For production billing patterns, refer to MPP payment intents.

Was this helpful?