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 payment2.MCP server returns x402 payment requirements3.Agent checks budget and approves4.Agent retries withsigned payment evidence5.paidTool verifies the payment through the configured facilitator6.Handler sends the input to GOAT AgentKit ExecutionRuntime7.Runtime validates,applies policy,checks idempotency,and executes8.MCP server returns the structured result
1.Agent calls company_risk without payment2.MCP server returns x402 payment requirements3.Agent checks budget and approves4.Agent retries withsigned payment evidence5.paidTool verifies the payment through the configured facilitator6.Handler sends the input to GOAT AgentKit ExecutionRuntime7.Runtime validates,applies policy,checks idempotency,and executes8.MCP server returns the structured result
1.Agent calls company_risk without payment2.MCP server returns x402 payment requirements3.Agent checks budget and approves4.Agent retries withsigned payment evidence5.paidTool verifies the payment through the configured facilitator6.Handler sends the input to GOAT AgentKit ExecutionRuntime7.Runtime validates,applies policy,checks idempotency,and executes8.MCP server returns the structured result
The project has four logical modules:
src/
action.tsGOAT AgentKit custom actionruntime.tspolicy,idempotency,retry,timeout,metricspaid-mcp.tsCloudflare withX402 and paidTool serverpayer-agent.tsMCP client withx402 payment support
src/
action.tsGOAT AgentKit custom actionruntime.tspolicy,idempotency,retry,timeout,metricspaid-mcp.tsCloudflare withX402 and paidTool serverpayer-agent.tsMCP client withx402 payment support
src/
action.tsGOAT AgentKit custom actionruntime.tspolicy,idempotency,retry,timeout,metricspaid-mcp.tsCloudflare withX402 and paidTool serverpayer-agent.tsMCP client withx402 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:
Use versions compatible with the current Cloudflare Agents SDK and x402 v2 packages. Lock them in the project rather than relying on floating versions.
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.tsimport{z}from"zod";import{customActionProvider}from"@goatnetwork/agentkit/providers";exportconstcompanyRiskInput = z.object({requestId:z.string().uuid(),companyName:z.string().trim().min(1).max(200),country:z.string().length(2).optional(),});exportconstcompanyRiskProvider = customActionProvider([{name:"seller.company_risk",description:"Return a bounded company risk summary",schema:companyRiskInput,riskLevel:"read",networks:[],requiresConfirmation:false,invoke:async(input)=>{constdata = awaitfetchCompanyRisk({companyName:input.companyName,country:input.country,});return{requestId:input.requestId,company:data.company,riskBand:data.riskBand,signals:data.signals,generatedAt:newDate().toISOString(),};},},]);asyncfunctionfetchCompanyRisk(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.tsimport{z}from"zod";import{customActionProvider}from"@goatnetwork/agentkit/providers";exportconstcompanyRiskInput = z.object({requestId:z.string().uuid(),companyName:z.string().trim().min(1).max(200),country:z.string().length(2).optional(),});exportconstcompanyRiskProvider = customActionProvider([{name:"seller.company_risk",description:"Return a bounded company risk summary",schema:companyRiskInput,riskLevel:"read",networks:[],requiresConfirmation:false,invoke:async(input)=>{constdata = awaitfetchCompanyRisk({companyName:input.companyName,country:input.country,});return{requestId:input.requestId,company:data.company,riskBand:data.riskBand,signals:data.signals,generatedAt:newDate().toISOString(),};},},]);asyncfunctionfetchCompanyRisk(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.tsimport{z}from"zod";import{customActionProvider}from"@goatnetwork/agentkit/providers";exportconstcompanyRiskInput = z.object({requestId:z.string().uuid(),companyName:z.string().trim().min(1).max(200),country:z.string().length(2).optional(),});exportconstcompanyRiskProvider = customActionProvider([{name:"seller.company_risk",description:"Return a bounded company risk summary",schema:companyRiskInput,riskLevel:"read",networks:[],requiresConfirmation:false,invoke:async(input)=>{constdata = awaitfetchCompanyRisk({companyName:input.companyName,country:input.country,});return{requestId:input.requestId,company:data.company,riskBand:data.riskBand,signals:data.signals,generatedAt:newDate().toISOString(),};},},]);asyncfunctionfetchCompanyRisk(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.
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:
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.tsimport{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{McpAgent}from"agents/mcp";import{withX402,typeX402Config}from"agents/x402";import{z}from"zod";import{runPaidAction}from"./runtime";constpaymentConfig: X402Config = {network:"base-sepolia",recipient:process.env.MERCHANT_ADDRESSas `0x${string}`,facilitator:{url:process.env.X402_FACILITATOR_URL!,},};exportclass PaidSellerMCP extendsMcpAgent<Env> {server = withX402(newMcpServer({name:"goat-agentkit-paid-tools",version:"1.0.0",}),paymentConfig,);asyncinit(){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)=>{constresult = awaitrunPaidAction(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.tsimport{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{McpAgent}from"agents/mcp";import{withX402,typeX402Config}from"agents/x402";import{z}from"zod";import{runPaidAction}from"./runtime";constpaymentConfig: X402Config = {network:"base-sepolia",recipient:process.env.MERCHANT_ADDRESSas `0x${string}`,facilitator:{url:process.env.X402_FACILITATOR_URL!,},};exportclass PaidSellerMCP extendsMcpAgent<Env> {server = withX402(newMcpServer({name:"goat-agentkit-paid-tools",version:"1.0.0",}),paymentConfig,);asyncinit(){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)=>{constresult = awaitrunPaidAction(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.tsimport{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{McpAgent}from"agents/mcp";import{withX402,typeX402Config}from"agents/x402";import{z}from"zod";import{runPaidAction}from"./runtime";constpaymentConfig: X402Config = {network:"base-sepolia",recipient:process.env.MERCHANT_ADDRESSas `0x${string}`,facilitator:{url:process.env.X402_FACILITATOR_URL!,},};exportclass PaidSellerMCP extendsMcpAgent<Env> {server = withX402(newMcpServer({name:"goat-agentkit-paid-tools",version:"1.0.0",}),paymentConfig,);asyncinit(){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)=>{constresult = awaitrunPaidAction(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:
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.tsimport{Agent}from"agents";import{withX402Client}from"agents/x402";import{privateKeyToAccount}from"viem/accounts";exportclass BuyerAgent extendsAgent<Env> {privatepaidClient?: ReturnType<typeofwithX402Client>;asynconStart(){constconnection = awaitthis.mcp.connect(this.env.PAID_MCP_SERVER,);constaccount = privateKeyToAccount(this.env.PAYER_PRIVATE_KEYas `0x${string}`,);this.paidClient = withX402Client(this.mcp.mcpConnections[connection.id].client,{network:"base-sepolia",account,},);}asyncapprovePayment(requirements: unknown){returnpaymentPolicyAllows(requirements,{maxUsdPerCall:0.02,allowedTool:"company_risk",});}asyncrunDemo(){if(!this.paidClient){thrownewError("Paid MCP client is not connected");}constrequestId = crypto.randomUUID();returnthis.paidClient.callTool(this.approvePayment.bind(this),{name:"company_risk",arguments:{requestId,companyName:"Example Company",country:"US",},},);}}functionpaymentPolicyAllows(requirements: unknown,policy:{maxUsdPerCall: number;allowedTool: string },){// Parse the typed requirements used by the installed SDK.// Verify amount, network, asset, recipient, and cumulative budget.returnBoolean(requirements && policy.maxUsdPerCall === 0.02);}
// src/payer-agent.tsimport{Agent}from"agents";import{withX402Client}from"agents/x402";import{privateKeyToAccount}from"viem/accounts";exportclass BuyerAgent extendsAgent<Env> {privatepaidClient?: ReturnType<typeofwithX402Client>;asynconStart(){constconnection = awaitthis.mcp.connect(this.env.PAID_MCP_SERVER,);constaccount = privateKeyToAccount(this.env.PAYER_PRIVATE_KEYas `0x${string}`,);this.paidClient = withX402Client(this.mcp.mcpConnections[connection.id].client,{network:"base-sepolia",account,},);}asyncapprovePayment(requirements: unknown){returnpaymentPolicyAllows(requirements,{maxUsdPerCall:0.02,allowedTool:"company_risk",});}asyncrunDemo(){if(!this.paidClient){thrownewError("Paid MCP client is not connected");}constrequestId = crypto.randomUUID();returnthis.paidClient.callTool(this.approvePayment.bind(this),{name:"company_risk",arguments:{requestId,companyName:"Example Company",country:"US",},},);}}functionpaymentPolicyAllows(requirements: unknown,policy:{maxUsdPerCall: number;allowedTool: string },){// Parse the typed requirements used by the installed SDK.// Verify amount, network, asset, recipient, and cumulative budget.returnBoolean(requirements && policy.maxUsdPerCall === 0.02);}
// src/payer-agent.tsimport{Agent}from"agents";import{withX402Client}from"agents/x402";import{privateKeyToAccount}from"viem/accounts";exportclass BuyerAgent extendsAgent<Env> {privatepaidClient?: ReturnType<typeofwithX402Client>;asynconStart(){constconnection = awaitthis.mcp.connect(this.env.PAID_MCP_SERVER,);constaccount = privateKeyToAccount(this.env.PAYER_PRIVATE_KEYas `0x${string}`,);this.paidClient = withX402Client(this.mcp.mcpConnections[connection.id].client,{network:"base-sepolia",account,},);}asyncapprovePayment(requirements: unknown){returnpaymentPolicyAllows(requirements,{maxUsdPerCall:0.02,allowedTool:"company_risk",});}asyncrunDemo(){if(!this.paidClient){thrownewError("Paid MCP client is not connected");}constrequestId = crypto.randomUUID();returnthis.paidClient.callTool(this.approvePayment.bind(this),{name:"company_risk",arguments:{requestId,companyName:"Example Company",country:"US",},},);}}functionpaymentPolicyAllows(requirements: unknown,policy:{maxUsdPerCall: number;allowedTool: string },){// Parse the typed requirements used by the installed SDK.// Verify amount, network, asset, recipient, and cumulative budget.returnBoolean(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:
Start the AgentKit executor and paid MCP server in the selected deployment arrangement.
Call service_status; it should return without payment.
Call company_risk without a payment-capable client; the server should return payment requirements.
Call it through withX402Client; the approval callback should receive the requirements.
Reject the payment once and confirm the AgentKit action did not execute.
Approve the payment on a test network and receive the structured result.
Repeat with the same requestId and input; the runtime should return the idempotent result rather than execute again.
Repeat the same ID with different input; the seller ledger should reject it.
Force the custom action to exceed its timeout and inspect the runtime error.
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 toolingas 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 toolingas 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 toolingas 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:
Tool price, network, asset, recipient, and facilitator are current.
The target MCP client supports the advertised x402 scheme.
Current package and deployment-runtime compatibility is tested in CI.
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.