x402 payment protocol support for MCP-Go clients and servers.
This library provides:
- Client Transport: Automatic x402 payment handling for MCP clients
- Server Wrapper: Payment collection support for MCP servers
- 🔌 Drop-in replacement: Fully compatible with mcp-go transport interface
- 💰 Automatic payments: Handles 402 responses transparently
- 🔐 Multiple signers: Support for private keys, mnemonics, keystores
- 🎯 Payment control: Optional callback for payment approval
- 🧪 Testing support: Mock signers and payment recorders for easy testing
- 💳 Payment collection: Require payments for specific MCP tools
- 🔒 Payment verification: Automatic verification via x402 facilitator
- ⛓️ On-chain settlement: Automatic settlement of verified payments
- 🎛️ Flexible pricing: Set different prices for different tools
- 🔄 Mixed mode: Support both free and paid tools on same server
go get github.com/mark3labs/mcp-go-x402
package main
import (
"context"
"log"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
x402 "github.com/mark3labs/mcp-go-x402"
)
func main() {
// Create signer with your private key and explicit payment options
signer, err := x402.NewPrivateKeySigner(
"YOUR_PRIVATE_KEY_HEX",
x402.AcceptUSDCBase(), // Accept USDC on Base mainnet
)
if err != nil {
log.Fatal(err)
}
// Create x402 transport
transport, err := x402.New(x402.Config{
ServerURL: "https://paid-mcp-server.example.com",
Signer: signer,
})
if err != nil {
log.Fatal(err)
}
// Create MCP client with x402 transport
mcpClient := client.NewClient(transport)
ctx := context.Background()
if err := mcpClient.Start(ctx); err != nil {
log.Fatal(err)
}
defer mcpClient.Close()
// Initialize MCP session
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: "1.0.0",
ClientInfo: mcp.Implementation{
Name: "x402-client",
Version: "1.0.0",
},
},
})
if err != nil {
log.Fatal(err)
}
// Use MCP client normally - payments handled automatically!
tools, _ := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
log.Printf("Found %d tools", len(tools.Tools))
}
package main
import (
"context"
"log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
x402server "github.com/mark3labs/mcp-go-x402/server"
)
func main() {
// Configure x402 server
config := &x402server.Config{
FacilitatorURL: "https://facilitator.x402.rs",
VerifyOnly: false, // Set to true for testing without settlement
}
// Create x402 server
srv := x402server.NewX402Server("my-server", "1.0.0", config)
// Add a free tool
srv.AddTool(
mcp.NewTool("free-tool",
mcp.WithDescription("This tool is free")),
freeToolHandler,
)
// Add a paid tool with multiple payment options
srv.AddPayableTool(
mcp.NewTool("premium-tool",
mcp.WithDescription("Premium feature"),
mcp.WithString("input", mcp.Required())),
premiumToolHandler,
// Option 1: Pay with USDC on Base
x402server.RequireUSDCBase("0xYourWallet", "10000", "Premium feature via Base"),
// Option 2: Pay with USDC on Base Sepolia (testnet)
x402server.RequireUSDCBaseSepolia("0xYourWallet", "5000", "Premium feature via Base Sepolia (testnet)"),
)
// Start server
log.Println("Starting x402 MCP server on :8080")
if err := srv.Start(":8080"); err != nil {
log.Fatal(err)
}
}
func freeToolHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{mcp.NewTextContent("Free response")},
}, nil
}
func premiumToolHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
input := req.GetString("input", "")
// Process premium request
return &mcp.CallToolResult{
Content: []mcp.Content{mcp.NewTextContent("Premium response for: " + input)},
}, nil
}
// Create signer with explicit payment options
signer, err := x402.NewPrivateKeySigner(
privateKey,
x402.AcceptUSDCBase(), // Accept USDC on Base
x402.AcceptUSDCBaseSepolia(), // Accept USDC on Base Sepolia (testnet)
)
config := x402.Config{
ServerURL: "https://server.example.com",
Signer: signer,
}
config := x402.Config{
ServerURL: "https://server.example.com",
Signer: signer,
PaymentCallback: func(amount *big.Int, resource string) bool {
// Custom logic to approve/decline payments
// Return true to approve, false to decline
if amount.Cmp(big.NewInt(100000)) > 0 { // More than 0.1 USDC
fmt.Printf("Approve payment of %s for %s? ", amount, resource)
return getUserApproval()
}
return true // Auto-approve small amounts
},
}
config := x402.Config{
ServerURL: "https://server.example.com",
Signer: signer,
OnPaymentAttempt: func(event x402.PaymentEvent) {
log.Printf("Attempting payment: %s to %s", event.Amount, event.Recipient)
},
OnPaymentSuccess: func(event x402.PaymentEvent) {
log.Printf("Payment successful: tx %s", event.Transaction)
},
OnPaymentFailure: func(event x402.PaymentEvent, err error) {
log.Printf("Payment failed: %v", err)
},
}
config := &x402server.Config{
FacilitatorURL: "https://facilitator.x402.rs",
VerifyOnly: false, // Set to true for testing without settlement
}
Servers can now offer multiple payment options per tool, allowing clients to choose their preferred network or take advantage of discounts:
srv.AddPayableTool(
mcp.NewTool("analytics",
mcp.WithDescription("Advanced analytics"),
mcp.WithString("query", mcp.Required())),
analyticsHandler,
// Base mainnet - standard price
x402server.RequireUSDCBase("0xYourWallet", "100000", "Analytics via Base - 0.1 USDC"),
// Base Sepolia - testnet option
x402server.RequireUSDCBaseSepolia("0xYourWallet", "50000", "Analytics via Base Sepolia (testnet) - 0.05 USDC"),
// Custom network example - Ethereum mainnet
x402server.PaymentRequirement{
Scheme: "exact",
Network: "ethereum",
Asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
PayTo: "0xYourWallet",
MaxAmountRequired: "100000", // 0.1 USDC
Description: "Analytics via Ethereum",
MaxTimeoutSeconds: 60,
},
)
When a client requests a paid tool without payment, they receive all available payment options and can choose the one that works best for them based on:
- Network preference (gas fees, speed)
- Available balance on different chains
- Price differences (discounts for certain networks)
// If you already have an MCP server, wrap it with x402
mcpServer := server.NewMCPServer("existing", "1.0")
httpServer := server.NewStreamableHTTPServer(mcpServer)
// Wrap with x402 handler
x402Handler := x402server.NewX402Handler(httpServer, config)
// Use as http.Handler
http.Handle("/", x402Handler)
http.ListenAndServe(":8080", nil)
signer, err := x402.NewPrivateKeySigner(
"0xYourPrivateKeyHex",
x402.AcceptUSDCBase(), // Must specify at least one payment option
)
signer, err := x402.NewMnemonicSigner(
"your twelve word mnemonic phrase here ...",
"m/44'/60'/0'/0/0", // Optional: derivation path
x402.AcceptUSDCBase(),
)
keystoreJSON, _ := os.ReadFile("keystore.json")
signer, err := x402.NewKeystoreSigner(
keystoreJSON,
"password",
x402.AcceptUSDCBase(),
)
signer, err := x402.NewPrivateKeySigner(
privateKey,
// Priority 1: Prefer Base (cheap & fast)
x402.AcceptUSDCBase().WithPriority(1),
// Priority 2: Fallback to Base Sepolia (testnet)
x402.AcceptUSDCBaseSepolia().WithPriority(2),
)
signer, err := x402.NewPrivateKeySigner(
privateKey,
x402.AcceptUSDCBase()
.WithMaxAmount("100000") // Max 0.1 USDC per payment
.WithMinBalance("1000000"), // Keep 1 USDC reserve
)
type MyCustomSigner struct {
paymentOptions []x402.ClientPaymentOption
}
func NewMyCustomSigner() *MyCustomSigner {
return &MyCustomSigner{
paymentOptions: []x402.ClientPaymentOption{
x402.AcceptUSDCBase(),
x402.AcceptUSDCBaseSepolia(),
},
}
}
func (s *MyCustomSigner) SignPayment(ctx context.Context, req x402.PaymentRequirement) (*x402.PaymentPayload, error) {
// Your custom signing logic
// Could use hardware wallet, remote signer, etc.
}
func (s *MyCustomSigner) GetAddress() string {
return "0xYourAddress"
}
func (s *MyCustomSigner) SupportsNetwork(network string) bool {
for _, opt := range s.paymentOptions {
if opt.Network == network {
return true
}
}
return false
}
func (s *MyCustomSigner) HasAsset(asset, network string) bool {
for _, opt := range s.paymentOptions {
if opt.Network == network && opt.Asset == asset {
return true
}
}
return false
}
func (s *MyCustomSigner) GetPaymentOption(network, asset string) *x402.ClientPaymentOption {
for _, opt := range s.paymentOptions {
if opt.Network == network && opt.Asset == asset {
optCopy := opt
return &optCopy
}
}
return nil
}
func TestMyMCPClient(t *testing.T) {
// No real wallet needed for tests
signer := x402.NewMockSigner(
"0xTestWallet",
x402.AcceptUSDCBaseSepolia(), // Mock signer for testing
)
transport, _ := x402.New(x402.Config{
ServerURL: "https://test-server.example.com",
Signer: signer,
})
// Test your MCP client
client := client.NewClient(transport)
// ...
}
func TestPaymentFlow(t *testing.T) {
// Create mock signer and recorder
signer := x402.NewMockSigner(
"0xTestWallet",
x402.AcceptUSDCBaseSepolia(),
)
recorder := x402.NewPaymentRecorder()
transport, _ := x402.New(x402.Config{
ServerURL: testServer.URL,
Signer: signer,
})
// Attach the recorder using the helper function
x402.WithPaymentRecorder(recorder)(transport)
// Make requests...
// Verify payments
assert.Equal(t, 2, recorder.PaymentCount()) // Attempt + Success events
lastPayment := recorder.LastPayment()
assert.Equal(t, x402.PaymentEventSuccess, lastPayment.Type)
assert.Equal(t, "20000", recorder.TotalAmount())
}
Currently, the library includes built-in helper functions for:
base
- Base Mainnet (viaAcceptUSDCBase()
)base-sepolia
- Base Sepolia Testnet (viaAcceptUSDCBaseSepolia()
)
Additional networks can be supported by manually configuring ClientPaymentOption
objects with the appropriate network, asset, and scheme parameters.
- Private keys: Never hardcode private keys. Use environment variables or secure key management.
- Payment approval: Use
PaymentCallback
to control payment approval based on amount or resource. - Network verification: The library verifies network and asset compatibility before signing.
- Per-option limits: Use
.WithMaxAmount()
on payment options to set per-network spending limits.
See the examples directory for more detailed examples:
- Client - Simple client that can pay for tool use (see main.go)
- Server - Server that collects payments for tool use (see main.go)
- Client makes MCP request through x402 transport
- If server returns 402 Payment Required, transport extracts payment requirements
- Transport uses configured signer to create payment authorization
- Transport retries request with X-PAYMENT header
- Server verifies and settles payment, then returns response
- Server receives MCP request
- Checks if requested tool requires payment
- If no payment provided, returns 402 with payment requirements
- If payment provided, verifies with facilitator service
- Settles payment on-chain (unless in verify-only mode)
- Executes tool and returns response with payment confirmation
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.
- MCP-Go for the excellent MCP implementation
- x402 Protocol (GitHub) for the payment specification
- go-ethereum for crypto utilities