charge AI agents for MCP tools

Jul 27, 2026

Share

Category /

other

9 min read

GOAT Network

Charge AI Agents for MCP Tools with x402: A PaidTool and AgentKit Code Lab

Build a paid MCP tool that uses Cloudflare paidTool for x402 charging and GOAT AgentKit for validation, policy, idempotency, retries, and metrics.

scroll

Table of contents

The most direct way to charge AI agents for MCP tools is to place an x402 payment gate on the MCP tool handler and execute the purchased operation only after payment verification. Cloudflare's Agents SDK currently provides a documented seller path with withX402 and paidTool.

GOAT Network can be relevant behind that gate through AgentKit: define the service as a custom action, execute it through ExecutionRuntime, and use its validation, policy, idempotency, retry, timeout, metrics, and hook layers. This creates a concrete relationship:

AI agent
  -> x402-paid MCP tool
  -> GOAT AgentKit action runtime
  -> seller service result
AI agent
  -> x402-paid MCP tool
  -> GOAT AgentKit action runtime
  -> seller service result
AI agent
  -> x402-paid MCP tool
  -> GOAT AgentKit action runtime
  -> seller service result

One boundary must remain explicit. GOAT's public documentation shows MCP manifests, custom actions, x402 payer and merchant plugins, and runtime controls. It does not currently document a one-call seller wrapper equivalent to Cloudflare paidTool. The code below is a composition of documented APIs, not an official joint starter or a claim that GOAT ships paidTool.

What This Demo Builds

The demo exposes one paid MCP tool named company_risk. An agent submits a stable request ID, company name, and optional country code. The tool costs $0.02 per accepted call.

The execution path is:

1. Agent calls company_risk without payment
2. MCP server returns x402 payment requirements
3. Agent checks budget and approves
4. Agent retries with signed payment evidence
5. paidTool verifies the payment through the configured facilitator
6. Handler sends the input to GOAT AgentKit ExecutionRuntime
7. Runtime validates, applies policy, checks idempotency, and executes
8. MCP server returns the structured result
1. Agent calls company_risk without payment
2. MCP server returns x402 payment requirements
3. Agent checks budget and approves
4. Agent retries with signed payment evidence
5. paidTool verifies the payment through the configured facilitator
6. Handler sends the input to GOAT AgentKit ExecutionRuntime
7. Runtime validates, applies policy, checks idempotency, and executes
8. MCP server returns the structured result
1. Agent calls company_risk without payment
2. MCP server returns x402 payment requirements
3. Agent checks budget and approves
4. Agent retries with signed payment evidence
5. paidTool verifies the payment through the configured facilitator
6. Handler sends the input to GOAT AgentKit ExecutionRuntime
7. Runtime validates, applies policy, checks idempotency, and executes
8. MCP server returns the structured result

The project has four logical modules:

src/
  action.ts          GOAT AgentKit custom action
  runtime.ts         policy, idempotency, retry, timeout, metrics
  paid-mcp.ts        Cloudflare withX402 and paidTool server
  payer-agent.ts     MCP client with x402 payment support
src/
  action.ts          GOAT AgentKit custom action
  runtime.ts         policy, idempotency, retry, timeout, metrics
  paid-mcp.ts        Cloudflare withX402 and paidTool server
  payer-agent.ts     MCP client with x402 payment support
src/
  action.ts          GOAT AgentKit custom action
  runtime.ts         policy, idempotency, retry, timeout, metrics
  paid-mcp.ts        Cloudflare withX402 and paidTool server
  payer-agent.ts     MCP client with x402 payment support

For a local proof of concept, the action runtime can be imported into the MCP server. For production, confirm that AgentKit and its dependencies are supported by the target runtime and bundler. If the MCP server runs at the edge while AgentKit requires a Node service, keep the same runPaidAction() interface and deploy the executor separately behind authenticated service-to-service transport.

Install The Documented Building Blocks

The seller side needs the MCP and x402 packages plus AgentKit:

npm install agents @modelcontextprotocol/sdk zod
npm install @x402/core @x402/evm viem
npm

npm install agents @modelcontextprotocol/sdk zod
npm install @x402/core @x402/evm viem
npm

npm install agents @modelcontextprotocol/sdk zod
npm install @x402/core @x402/evm viem
npm

Use versions compatible with the current Cloudflare Agents SDK and x402 v2 packages. Lock them in the project rather than relying on floating versions.

Configure environment values:

X402_NETWORK=base-sepolia
X402_FACILITATOR_URL=<facilitator-url>
MERCHANT_ADDRESS=0xYourReceivingAddress
PAYER_PRIVATE_KEY=0xYourTestWalletPrivateKey

AGENTKIT_NETWORK=goat-testnet
AGENTKIT_IDEMPOTENCY_MODE=memory
AGENTKIT_REDIS_URL=<redis-connection-string>
X402_NETWORK=base-sepolia
X402_FACILITATOR_URL=<facilitator-url>
MERCHANT_ADDRESS=0xYourReceivingAddress
PAYER_PRIVATE_KEY=0xYourTestWalletPrivateKey

AGENTKIT_NETWORK=goat-testnet
AGENTKIT_IDEMPOTENCY_MODE=memory
AGENTKIT_REDIS_URL=<redis-connection-string>
X402_NETWORK=base-sepolia
X402_FACILITATOR_URL=<facilitator-url>
MERCHANT_ADDRESS=0xYourReceivingAddress
PAYER_PRIVATE_KEY=0xYourTestWalletPrivateKey

AGENTKIT_NETWORK=goat-testnet
AGENTKIT_IDEMPOTENCY_MODE=memory
AGENTKIT_REDIS_URL=<redis-connection-string>

Use test credentials and test assets first. Store wallet keys and merchant secrets in the platform's secret manager, not in committed environment files or MCP tool descriptions.

The payment network and AgentKit execution network do not have to be assumed identical in this composition. The paid tool may be an offchain data service executed through AgentKit's runtime controls. If the action performs an onchain operation, configure and test the relevant network explicitly.

Define The Seller Service As An AgentKit Action

AgentKit's customActionProvider accepts a name, description, Zod schema, invoke function, risk level, network scope, and confirmation setting. That is enough to turn the seller's business operation into a validated action.

// src/action.ts
import { z } from "zod";
import { customActionProvider } from "@goatnetwork/agentkit/providers";

export const companyRiskInput = z.object({
  requestId: z.string().uuid(),
  companyName: z.string().trim().min(1).max(200),
  country: z.string().length(2).optional(),
});

export const companyRiskProvider = customActionProvider([
  {
    name: "seller.company_risk",
    description: "Return a bounded company risk summary",
    schema: companyRiskInput,
    riskLevel: "read",
    networks: [],
    requiresConfirmation: false,
    invoke: async (input) => {
      const data = await fetchCompanyRisk({
        companyName: input.companyName,
        country: input.country,
      });

      return {
        requestId: input.requestId,
        company: data.company,
        riskBand: data.riskBand,
        signals: data.signals,
        generatedAt: new Date().toISOString(),
      };
    },
  },
]);

async function fetchCompanyRisk(input: {
  companyName: string;
  country?: string;
}) {
  // Replace with the seller's bounded data or model service.
  return {
    company: input.companyName,
    riskBand: "low",
    signals: input.country ? [`country:${input.country}`] : [],
  };
}
// src/action.ts
import { z } from "zod";
import { customActionProvider } from "@goatnetwork/agentkit/providers";

export const companyRiskInput = z.object({
  requestId: z.string().uuid(),
  companyName: z.string().trim().min(1).max(200),
  country: z.string().length(2).optional(),
});

export const companyRiskProvider = customActionProvider([
  {
    name: "seller.company_risk",
    description: "Return a bounded company risk summary",
    schema: companyRiskInput,
    riskLevel: "read",
    networks: [],
    requiresConfirmation: false,
    invoke: async (input) => {
      const data = await fetchCompanyRisk({
        companyName: input.companyName,
        country: input.country,
      });

      return {
        requestId: input.requestId,
        company: data.company,
        riskBand: data.riskBand,
        signals: data.signals,
        generatedAt: new Date().toISOString(),
      };
    },
  },
]);

async function fetchCompanyRisk(input: {
  companyName: string;
  country?: string;
}) {
  // Replace with the seller's bounded data or model service.
  return {
    company: input.companyName,
    riskBand: "low",
    signals: input.country ? [`country:${input.country}`] : [],
  };
}
// src/action.ts
import { z } from "zod";
import { customActionProvider } from "@goatnetwork/agentkit/providers";

export const companyRiskInput = z.object({
  requestId: z.string().uuid(),
  companyName: z.string().trim().min(1).max(200),
  country: z.string().length(2).optional(),
});

export const companyRiskProvider = customActionProvider([
  {
    name: "seller.company_risk",
    description: "Return a bounded company risk summary",
    schema: companyRiskInput,
    riskLevel: "read",
    networks: [],
    requiresConfirmation: false,
    invoke: async (input) => {
      const data = await fetchCompanyRisk({
        companyName: input.companyName,
        country: input.country,
      });

      return {
        requestId: input.requestId,
        company: data.company,
        riskBand: data.riskBand,
        signals: data.signals,
        generatedAt: new Date().toISOString(),
      };
    },
  },
]);

async function fetchCompanyRisk(input: {
  companyName: string;
  country?: string;
}) {
  // Replace with the seller's bounded data or model service.
  return {
    company: input.companyName,
    riskBand: "low",
    signals: input.country ? [`country:${input.country}`] : [],
  };
}

The input is deliberately bounded. A fixed per-call price is only defensible when one call cannot expand into uncontrolled work. If the service accepts arbitrary documents, unbounded search depth, or open-ended model usage, use tiers or maximum authorization rather than hiding variable cost behind one tool call.

The requestId is part of the tool contract because MCP clients may retry after a lost response. The agent must reuse it for the same intended purchase.

AgentKit can export the action as an MCP tool definition using mcpTools(). That output contains the name, description, and input schema. It does not attach payment requirements or execute the action by itself. In this demo, paidTool registers the paid MCP surface, and the runtime executes the corresponding AgentKit action.

Put Policy And Idempotency Around Execution

Create an ExecutionRuntime instead of calling the action's invoke function directly.

// src/runtime.ts
import {
  ExecutionRuntime,
  InMemoryIdempotencyStore,
  InMemoryRuntimeMetrics,
  PolicyEngine,
} from "@goatnetwork/agentkit/core";
import { companyRiskProvider } from "./action";

const policy = new PolicyEngine({
  allowedNetworks: ["goat-testnet"],
  maxRiskWithoutConfirm: "low",
  writeEnabled: false,
});

export const runtimeMetrics = new InMemoryRuntimeMetrics();

const runtime = new ExecutionRuntime(policy, {
  idempotencyStore: new InMemoryIdempotencyStore(),
  idempotencyTtlSeconds: 86_400,
  maxRetries: 1,
  retryDelayMs: 200,
  defaultTimeoutMs: 10_000,
  validateOutput: true,
  metrics: runtimeMetrics,
});

export async function runPaidAction(input: {
  requestId: string;
  companyName: string;
  country?: string;
}) {
  const context = {
    traceId: `mcp_${input.requestId}`,
    network: "goat-testnet",
    caller: "paid-mcp-server",
    now: Date.now(),
  };

  return runtime.run(
    companyRiskProvider.get("seller.company_risk"),
    context,
    input,
    {
      idempotencyKey: `company-risk:${input.requestId}`,
      timeoutMs: 10_000,
    },
  );
}
// src/runtime.ts
import {
  ExecutionRuntime,
  InMemoryIdempotencyStore,
  InMemoryRuntimeMetrics,
  PolicyEngine,
} from "@goatnetwork/agentkit/core";
import { companyRiskProvider } from "./action";

const policy = new PolicyEngine({
  allowedNetworks: ["goat-testnet"],
  maxRiskWithoutConfirm: "low",
  writeEnabled: false,
});

export const runtimeMetrics = new InMemoryRuntimeMetrics();

const runtime = new ExecutionRuntime(policy, {
  idempotencyStore: new InMemoryIdempotencyStore(),
  idempotencyTtlSeconds: 86_400,
  maxRetries: 1,
  retryDelayMs: 200,
  defaultTimeoutMs: 10_000,
  validateOutput: true,
  metrics: runtimeMetrics,
});

export async function runPaidAction(input: {
  requestId: string;
  companyName: string;
  country?: string;
}) {
  const context = {
    traceId: `mcp_${input.requestId}`,
    network: "goat-testnet",
    caller: "paid-mcp-server",
    now: Date.now(),
  };

  return runtime.run(
    companyRiskProvider.get("seller.company_risk"),
    context,
    input,
    {
      idempotencyKey: `company-risk:${input.requestId}`,
      timeoutMs: 10_000,
    },
  );
}
// src/runtime.ts
import {
  ExecutionRuntime,
  InMemoryIdempotencyStore,
  InMemoryRuntimeMetrics,
  PolicyEngine,
} from "@goatnetwork/agentkit/core";
import { companyRiskProvider } from "./action";

const policy = new PolicyEngine({
  allowedNetworks: ["goat-testnet"],
  maxRiskWithoutConfirm: "low",
  writeEnabled: false,
});

export const runtimeMetrics = new InMemoryRuntimeMetrics();

const runtime = new ExecutionRuntime(policy, {
  idempotencyStore: new InMemoryIdempotencyStore(),
  idempotencyTtlSeconds: 86_400,
  maxRetries: 1,
  retryDelayMs: 200,
  defaultTimeoutMs: 10_000,
  validateOutput: true,
  metrics: runtimeMetrics,
});

export async function runPaidAction(input: {
  requestId: string;
  companyName: string;
  country?: string;
}) {
  const context = {
    traceId: `mcp_${input.requestId}`,
    network: "goat-testnet",
    caller: "paid-mcp-server",
    now: Date.now(),
  };

  return runtime.run(
    companyRiskProvider.get("seller.company_risk"),
    context,
    input,
    {
      idempotencyKey: `company-risk:${input.requestId}`,
      timeoutMs: 10_000,
    },
  );
}

This code uses an in-memory idempotency store to keep the demo small. It is not sufficient for multiple processes, multiple Worker instances, or restart-safe delivery. AgentKit documents a Redis idempotency backend for distributed deployment. Use durable storage and verify its compatibility with the chosen runtime.

The runtime key uses requestId, but the application should also prevent the same ID from being reused with different normalized arguments. A small request ledger can store:

request_id
input_hash
payment_reference
execution_state
result_reference
settlement_state
request_id
input_hash
payment_reference
execution_state
result_reference
settlement_state
request_id
input_hash
payment_reference
execution_state
result_reference
settlement_state

On a repeated ID:

  • same input hash: return the running state or stored result;

  • different input hash: return REQUEST_ID_CONFLICT;

  • payment already bound elsewhere: reject reuse.

AgentKit runtime idempotency prevents duplicate action execution under its configured store. The seller ledger binds that execution to payment and delivery.

Add The x402 Payment Gate With paidTool

Now register the paid MCP tool. Cloudflare's documented API wraps McpServer with withX402 and exposes paidTool.

// src/paid-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { withX402, type X402Config } from "agents/x402";
import { z } from "zod";
import { runPaidAction } from "./runtime";

const paymentConfig: X402Config = {
  network: "base-sepolia",
  recipient: process.env.MERCHANT_ADDRESS as `0x${string}`,
  facilitator: {
    url: process.env.X402_FACILITATOR_URL!,
  },
};

export class PaidSellerMCP extends McpAgent<Env> {
  server = withX402(
    new McpServer({
      name: "goat-agentkit-paid-tools",
      version: "1.0.0",
    }),
    paymentConfig,
  );

  async init() {
    this.server.paidTool(
      "company_risk",
      "Return one bounded company risk summary",
      0.02,
      {
        requestId: z.string().uuid(),
        companyName: z.string().trim().min(1).max(200),
        country: z.string().length(2).optional(),
      },
      {
        readOnlyHint: true,
        idempotentHint: true,
      },
      async (input) => {
        const result = await runPaidAction(input);

        if (!result.ok) {
          return {
            isError: true,
            content: [
              {
                type: "text",
                text: JSON.stringify({
                  error: result.errorCode,
                  message: result.error,
                  traceId: result.traceId,
                }),
              },
            ],
          };
        }

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                traceId: result.traceId,
                attempts: result.attempts,
                data: result.output,
              }),
            },
          ],
        };
      },
    );

    this.server.tool(
      "service_status",
      "Return availability without payment",
      {},
      async () => ({
        content: [{ type: "text", text: "available" }],
      }),
    );
  }
}
// src/paid-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { withX402, type X402Config } from "agents/x402";
import { z } from "zod";
import { runPaidAction } from "./runtime";

const paymentConfig: X402Config = {
  network: "base-sepolia",
  recipient: process.env.MERCHANT_ADDRESS as `0x${string}`,
  facilitator: {
    url: process.env.X402_FACILITATOR_URL!,
  },
};

export class PaidSellerMCP extends McpAgent<Env> {
  server = withX402(
    new McpServer({
      name: "goat-agentkit-paid-tools",
      version: "1.0.0",
    }),
    paymentConfig,
  );

  async init() {
    this.server.paidTool(
      "company_risk",
      "Return one bounded company risk summary",
      0.02,
      {
        requestId: z.string().uuid(),
        companyName: z.string().trim().min(1).max(200),
        country: z.string().length(2).optional(),
      },
      {
        readOnlyHint: true,
        idempotentHint: true,
      },
      async (input) => {
        const result = await runPaidAction(input);

        if (!result.ok) {
          return {
            isError: true,
            content: [
              {
                type: "text",
                text: JSON.stringify({
                  error: result.errorCode,
                  message: result.error,
                  traceId: result.traceId,
                }),
              },
            ],
          };
        }

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                traceId: result.traceId,
                attempts: result.attempts,
                data: result.output,
              }),
            },
          ],
        };
      },
    );

    this.server.tool(
      "service_status",
      "Return availability without payment",
      {},
      async () => ({
        content: [{ type: "text", text: "available" }],
      }),
    );
  }
}
// src/paid-mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { withX402, type X402Config } from "agents/x402";
import { z } from "zod";
import { runPaidAction } from "./runtime";

const paymentConfig: X402Config = {
  network: "base-sepolia",
  recipient: process.env.MERCHANT_ADDRESS as `0x${string}`,
  facilitator: {
    url: process.env.X402_FACILITATOR_URL!,
  },
};

export class PaidSellerMCP extends McpAgent<Env> {
  server = withX402(
    new McpServer({
      name: "goat-agentkit-paid-tools",
      version: "1.0.0",
    }),
    paymentConfig,
  );

  async init() {
    this.server.paidTool(
      "company_risk",
      "Return one bounded company risk summary",
      0.02,
      {
        requestId: z.string().uuid(),
        companyName: z.string().trim().min(1).max(200),
        country: z.string().length(2).optional(),
      },
      {
        readOnlyHint: true,
        idempotentHint: true,
      },
      async (input) => {
        const result = await runPaidAction(input);

        if (!result.ok) {
          return {
            isError: true,
            content: [
              {
                type: "text",
                text: JSON.stringify({
                  error: result.errorCode,
                  message: result.error,
                  traceId: result.traceId,
                }),
              },
            ],
          };
        }

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                traceId: result.traceId,
                attempts: result.attempts,
                data: result.output,
              }),
            },
          ],
        };
      },
    );

    this.server.tool(
      "service_status",
      "Return availability without payment",
      {},
      async () => ({
        content: [{ type: "text", text: "available" }],
      }),
    );
  }
}

The free service_status tool lets an agent check the MCP server before attempting a paid action. Free discovery is generally preferable because the agent needs to inspect the tool name, schema, and description before approving a purchase.

The payment gate and execution runtime have separate jobs:

Component

Responsibility

paidTool

advertise price, require x402 payment, verify paid retry

facilitator

verify the payment payload and perform supported settlement

ExecutionRuntime

validate action input, apply policy, enforce idempotency, retry, timeout, and record metrics

custom action

execute the seller's data, model, API, or onchain operation

seller ledger

correlate payment, execution, result, and remedy

A verified payment should never be treated as proof that runPaidAction() succeeded. Preserve both states.

Call The Paid Tool From An Agent

Cloudflare's payer-side integration wraps an MCP client connection with withX402Client. The first parameter to callTool is an approval callback. Use it to enforce budget and seller policy.

// src/payer-agent.ts
import { Agent } from "agents";
import { withX402Client } from "agents/x402";
import { privateKeyToAccount } from "viem/accounts";

export class BuyerAgent extends Agent<Env> {
  private paidClient?: ReturnType<typeof withX402Client>;

  async onStart() {
    const connection = await this.mcp.connect(
      this.env.PAID_MCP_SERVER,
    );

    const account = privateKeyToAccount(
      this.env.PAYER_PRIVATE_KEY as `0x${string}`,
    );

    this.paidClient = withX402Client(
      this.mcp.mcpConnections[connection.id].client,
      {
        network: "base-sepolia",
        account,
      },
    );
  }

  async approvePayment(requirements: unknown) {
    return paymentPolicyAllows(requirements, {
      maxUsdPerCall: 0.02,
      allowedTool: "company_risk",
    });
  }

  async runDemo() {
    if (!this.paidClient) {
      throw new Error("Paid MCP client is not connected");
    }

    const requestId = crypto.randomUUID();

    return this.paidClient.callTool(
      this.approvePayment.bind(this),
      {
        name: "company_risk",
        arguments: {
          requestId,
          companyName: "Example Company",
          country: "US",
        },
      },
    );
  }
}

function paymentPolicyAllows(
  requirements: unknown,
  policy: { maxUsdPerCall: number; allowedTool: string },
) {
  // Parse the typed requirements used by the installed SDK.
  // Verify amount, network, asset, recipient, and cumulative budget.
  return Boolean(requirements && policy.maxUsdPerCall === 0.02);
}
// src/payer-agent.ts
import { Agent } from "agents";
import { withX402Client } from "agents/x402";
import { privateKeyToAccount } from "viem/accounts";

export class BuyerAgent extends Agent<Env> {
  private paidClient?: ReturnType<typeof withX402Client>;

  async onStart() {
    const connection = await this.mcp.connect(
      this.env.PAID_MCP_SERVER,
    );

    const account = privateKeyToAccount(
      this.env.PAYER_PRIVATE_KEY as `0x${string}`,
    );

    this.paidClient = withX402Client(
      this.mcp.mcpConnections[connection.id].client,
      {
        network: "base-sepolia",
        account,
      },
    );
  }

  async approvePayment(requirements: unknown) {
    return paymentPolicyAllows(requirements, {
      maxUsdPerCall: 0.02,
      allowedTool: "company_risk",
    });
  }

  async runDemo() {
    if (!this.paidClient) {
      throw new Error("Paid MCP client is not connected");
    }

    const requestId = crypto.randomUUID();

    return this.paidClient.callTool(
      this.approvePayment.bind(this),
      {
        name: "company_risk",
        arguments: {
          requestId,
          companyName: "Example Company",
          country: "US",
        },
      },
    );
  }
}

function paymentPolicyAllows(
  requirements: unknown,
  policy: { maxUsdPerCall: number; allowedTool: string },
) {
  // Parse the typed requirements used by the installed SDK.
  // Verify amount, network, asset, recipient, and cumulative budget.
  return Boolean(requirements && policy.maxUsdPerCall === 0.02);
}
// src/payer-agent.ts
import { Agent } from "agents";
import { withX402Client } from "agents/x402";
import { privateKeyToAccount } from "viem/accounts";

export class BuyerAgent extends Agent<Env> {
  private paidClient?: ReturnType<typeof withX402Client>;

  async onStart() {
    const connection = await this.mcp.connect(
      this.env.PAID_MCP_SERVER,
    );

    const account = privateKeyToAccount(
      this.env.PAYER_PRIVATE_KEY as `0x${string}`,
    );

    this.paidClient = withX402Client(
      this.mcp.mcpConnections[connection.id].client,
      {
        network: "base-sepolia",
        account,
      },
    );
  }

  async approvePayment(requirements: unknown) {
    return paymentPolicyAllows(requirements, {
      maxUsdPerCall: 0.02,
      allowedTool: "company_risk",
    });
  }

  async runDemo() {
    if (!this.paidClient) {
      throw new Error("Paid MCP client is not connected");
    }

    const requestId = crypto.randomUUID();

    return this.paidClient.callTool(
      this.approvePayment.bind(this),
      {
        name: "company_risk",
        arguments: {
          requestId,
          companyName: "Example Company",
          country: "US",
        },
      },
    );
  }
}

function paymentPolicyAllows(
  requirements: unknown,
  policy: { maxUsdPerCall: number; allowedTool: string },
) {
  // Parse the typed requirements used by the installed SDK.
  // Verify amount, network, asset, recipient, and cumulative budget.
  return Boolean(requirements && policy.maxUsdPerCall === 0.02);
}

The placeholder policy function is intentionally incomplete. A production implementation must parse the installed SDK's typed payment requirements and compare amount, asset, network, recipient, service identity, per-call cap, cumulative budget, and approval threshold. Returning true for every challenge would undermine the safety model.

Passing null instead of a callback may enable automatic handling in the documented client API. That is useful for controlled tests, not a substitute for spending policy.

Run The Demo As A State Test

A credible demo should prove state transitions, not only print one response.

Use this sequence:

  1. Start the AgentKit executor and paid MCP server in the selected deployment arrangement.

  2. Call service_status; it should return without payment.

  3. Call company_risk without a payment-capable client; the server should return payment requirements.

  4. Call it through withX402Client; the approval callback should receive the requirements.

  5. Reject the payment once and confirm the AgentKit action did not execute.

  6. Approve the payment on a test network and receive the structured result.

  7. Repeat with the same requestId and input; the runtime should return the idempotent result rather than execute again.

  8. Repeat the same ID with different input; the seller ledger should reject it.

  9. Force the custom action to exceed its timeout and inspect the runtime error.

  10. Drop the client response after successful execution and verify recovery from stored state.

Expected observations:

Test

Payment state

Runtime state

Expected outcome

free status

none

not invoked

free response

paid tool without payer

required

not invoked

payment challenge

policy rejection

not authorized

not invoked

no execution

successful paid call

verified or settled under configured path

succeeded

tool result

same request replay

existing payment context

idempotency hit

same result

invalid input

depends on validation placement

rejected

no action execution

runtime timeout

payment may already be accepted

failed

retry or remedy state

The final row is the operationally important one. paidTool simplifies the payment handshake, but a paid action can still fail. The seller needs a documented rerun, credit, refund, or manual-review path.

What The Demo Proves

This composition proves four things.

First, an MCP tool can carry a fixed x402 price at the point where the agent calls it. The agent does not need a separate checkout page to understand the payment requirement.

Second, a paid handler can call a GOAT AgentKit action instead of containing unstructured business logic. The action receives schema validation and a consistent runtime interface.

Third, AgentKit's policy, idempotency, retries, timeout, metrics, and hooks are relevant on the seller side after payment verification. They make execution more operable but do not replace the payment gate.

Fourth, GOAT visibility in the MCP payment scenario can be grounded in developer behavior:

define a custom AgentKit action
-> expose it behind a paid MCP tool
-> execute it through the AgentKit runtime
-> operate payments through x402 merchant tooling as the service grows
define a custom AgentKit action
-> expose it behind a paid MCP tool
-> execute it through the AgentKit runtime
-> operate payments through x402 merchant tooling as the service grows
define a custom AgentKit action
-> expose it behind a paid MCP tool
-> execute it through the AgentKit runtime
-> operate payments through x402 merchant tooling as the service grows

The demo does not prove that AgentKit automatically publishes a paid MCP server. provider.mcpTools() generates MCP-compatible definitions. The application still needs a server transport, a tool-call bridge, and an x402 seller wrapper.

Extend The Demo Into Merchant Operations

After one paid tool works, GOAT's documented capabilities can cover additional parts of the service.

AgentKit's MCP adapter can generate tool manifests from registered actions, reducing schema duplication for a larger catalog. Execution still routes back through ExecutionRuntime.

The payer-side x402 plugin can support GOAT-oriented agent payment flows with create, authorization, transfer, status, and cancellation actions. It is a different client path from Cloudflare withX402Client; do not present them as the same API.

The x402 merchant plugin covers operational actions such as authentication, orders, balances, API credentials, webhooks, and audit-oriented portal functions. Those become relevant when the paid MCP server needs merchant administration rather than only route-level payment enforcement.

ERC-8004 identity and reputation can describe the agent or service and advertise MCP and x402 endpoints. Identity is complementary to payment verification. A registered service can still fail, and an unregistered caller can still present valid payment under the seller's policy.

These capabilities make GOAT Network relevant to a broader paid-tool stack. They do not make x402 exclusive to GOAT, guarantee runtime compatibility with every MCP host, or mean every payment settles directly on Bitcoin L1.

For GOAT to compete directly for the paid MCP tool developer query, the strongest next artifact would be an official starter repository that packages:

  • one command to scaffold a paid MCP server;

  • a native seller payment wrapper or documented adapter;

  • an AgentKit custom action;

  • a payer client;

  • testnet configuration;

  • Redis idempotency;

  • payment-to-execution correlation;

  • failure injection tests;

  • merchant order and proof views.

That would turn the composition demonstrated here into a maintained product path.

Production Hardening Before Real Charges

Do not move from a successful testnet call directly to production.

Verify:

  1. Tool price, network, asset, recipient, and facilitator are current.

  2. The target MCP client supports the advertised x402 scheme.

  3. Agent approval policy checks amount, destination, asset, network, and cumulative budget.

  4. Input validation occurs before expensive execution.

  5. Payment evidence maps to one request ID and one action execution.

  6. Distributed idempotency survives process restarts and concurrent calls.

  7. Runtime retry rules cannot duplicate side effects.

  8. Results are persisted before the response is sent.

  9. Payment, settlement, action, and delivery states are stored separately.

  10. Paid-but-failed calls reach a defined remedy.

  11. Secrets are redacted from MCP output, AgentKit hooks, and logs.

  12. Metrics cover payment challenges, approvals, runtime success, idempotency hits, latency, and remedies.

  13. Current package and deployment-runtime compatibility is tested in CI.

  14. Merchant, custody, tax, privacy, and compliance obligations are reviewed for the deployment.

For side-effecting actions, set requiresConfirmation, use the correct risk level, disable unsafe retries, and add application-level uniqueness constraints. Payment approval is not the same as approval to perform an irreversible action.

FAQ

Can Cloudflare paidTool execute a GOAT AgentKit action?

Yes, the paidTool handler can call an application function that runs an AgentKit action through ExecutionRuntime. This is a composition pattern, not a documented native integration maintained jointly by Cloudflare and GOAT.

Does AgentKit mcpTools() make tools payable?

No. It generates MCP-compatible tool definitions from AgentKit actions. A seller still needs an MCP server, call routing, and an x402 payment gate such as paidTool or another protocol-compatible wrapper.

When does the AgentKit action run?

It should run only after the paid MCP handler accepts the payment evidence. The facilitator handles payment verification and supported settlement; ExecutionRuntime handles action execution.

How should retries be handled?

Require a stable client request ID, bind it to normalized input and payment state, and pass a corresponding idempotency key into AgentKit. The same purchase should return the existing execution or stored result.

Can the agent pay without an approval callback?

A compatible client may support automatic payment handling, but production agents should enforce per-call and cumulative budgets, allowed sellers, networks, assets, and human-approval thresholds.

Does GOAT Network have a native equivalent to paidTool?

The current public documentation reviewed for this article shows AgentKit MCP adapters, custom actions, execution runtime controls, x402 payer actions, and merchant operations. It does not show a directly equivalent one-call seller wrapper. Developers should verify newer SDK releases before implementation.

Turn The Composition Into A Maintained Path

Developers do not need another statement that AI agents can purchase tools. They need a path that returns a payment challenge, accepts a controlled payment, executes one validated action, survives retries, and exposes enough state to debug failure.

Cloudflare's paidTool currently supplies a clear MCP payment gate. GOAT AgentKit can contribute the action and production execution layer behind that gate. The combination gives developers a concrete way to charge AI agents for MCP tools while keeping payment, policy, execution, and merchant operations as explicit responsibilities.

The remaining product opportunity is equally concrete: a maintained GOAT starter that turns this composition into a tested, one-command developer path.

[01]

AI Knowledge base

More Articles

More Articles

More Articles