mirror of
https://github.com/go-micro/go-micro.git
synced 2026-06-15 19:35:13 +02:00
feat(x402): opt-in agent-native payments for tools (#2964)
Integrate the x402 payment protocol (HTTP 402) so a tool can require a stablecoin payment and an agent can settle it — the next step after autonomous agents (blog 21): agents that act, and pay. - wrapper/x402: HTTP middleware enforcing the 402 challenge/verify flow, with a pluggable Facilitator interface. Go Micro carries no chain or crypto code — verification/settlement is delegated to a facilitator (Coinbase CDP, Alchemy, self-hosted), so Base and Solana are just different facilitators behind one interface. HTTPFacilitator default; tests cover challenge / accept / reject via a mock facilitator. - MCP gateway: optional Options.Payment gates /mcp/call (listing tools and health stay free); off unless configured. - micro mcp serve and micro-mcp-gateway: opt-in --x402-pay-to/-price/ -network/-facilitator flags (env vars on the standalone binary). - blog/22 'Integrating x402: Payments for Agents'; README feature row. Pricing is flat per call for now; richer models and an agent-side spend cap (next to MaxSteps/ApproveTool) are follow-ups. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -61,3 +61,4 @@ examples/mcp/hello/hello
|
||||
# Built example/harness binaries (go build ./path/... drops these at repo root)
|
||||
/plan-delegate
|
||||
/agent-plan-delegate
|
||||
/micro-mcp-gateway
|
||||
|
||||
@@ -223,6 +223,7 @@ agent := micro.NewAgent("assistant",
|
||||
| Guardrails | `MaxSteps` (stopping condition) and `ApproveTool` (human-in-the-loop) on every agent |
|
||||
| Workflows | `micro.NewFlow()` — event-driven; runs a step or triggers an agent |
|
||||
| MCP gateway | Every endpoint is an AI tool automatically |
|
||||
| Payments (x402) | Opt-in per-call payments for tools via the x402 standard; pluggable facilitator (Base, Solana, …) |
|
||||
| 7 LLM providers | Anthropic, OpenAI, Gemini, Groq, Mistral, Together, Atlas Cloud |
|
||||
| Interactive console | `micro run` includes a chat console for talking to services |
|
||||
| Service generation | `micro run --prompt` — describe a system, get running services |
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"go-micro.dev/v5/registry"
|
||||
"go-micro.dev/v5/registry/consul"
|
||||
"go-micro.dev/v5/registry/etcd"
|
||||
"go-micro.dev/v5/wrapper/x402"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -66,6 +67,27 @@ func main() {
|
||||
Usage: "Registry address (e.g., consul:8500, etcd:2379)",
|
||||
EnvVars: []string{"MICRO_REGISTRY_ADDRESS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402-pay-to",
|
||||
Usage: "Enable x402 payments for tool calls; the address payments are sent to",
|
||||
EnvVars: []string{"X402_PAY_TO"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402-price",
|
||||
Usage: "Per-call price in the asset's smallest unit (e.g. 10000 = 0.01 USDC)",
|
||||
EnvVars: []string{"X402_PRICE"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402-network",
|
||||
Usage: "Payment network: base (default), solana, ...",
|
||||
Value: "base",
|
||||
EnvVars: []string{"X402_NETWORK"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402-facilitator",
|
||||
Usage: "x402 facilitator URL (Coinbase CDP, Alchemy, or self-hosted)",
|
||||
EnvVars: []string{"X402_FACILITATOR"},
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "rate-limit",
|
||||
Usage: "Requests per second per tool (0 = unlimited)",
|
||||
@@ -131,6 +153,16 @@ func run(c *cli.Context) error {
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
// Opt-in x402 payments: enabled when a pay-to address is given.
|
||||
if payTo := c.String("x402-pay-to"); payTo != "" {
|
||||
opts.Payment = &x402.Config{
|
||||
PayTo: payTo,
|
||||
Price: c.String("x402-price"),
|
||||
Network: c.String("x402-network"),
|
||||
FacilitatorURL: c.String("x402-facilitator"),
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if rps := c.Float64("rate-limit"); rps > 0 {
|
||||
opts.RateLimit = &mcp.RateLimitConfig{
|
||||
|
||||
+77
-50
@@ -18,6 +18,7 @@ import (
|
||||
"go-micro.dev/v5/codec/bytes"
|
||||
"go-micro.dev/v5/gateway/mcp"
|
||||
"go-micro.dev/v5/registry"
|
||||
"go-micro.dev/v5/wrapper/x402"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -84,6 +85,23 @@ Examples:
|
||||
Name: "registry_address",
|
||||
Usage: "Registry address (e.g., consul:8500)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402_pay_to",
|
||||
Usage: "Enable x402 payments for tool calls; the address payments are sent to",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402_price",
|
||||
Usage: "Per-call price in the asset's smallest unit (e.g. 10000 = 0.01 USDC)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402_network",
|
||||
Usage: "Payment network: base (default), solana, ...",
|
||||
Value: "base",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "x402_facilitator",
|
||||
Usage: "x402 facilitator URL (Coinbase CDP, Alchemy, or self-hosted)",
|
||||
},
|
||||
},
|
||||
Action: serveAction,
|
||||
},
|
||||
@@ -233,6 +251,16 @@ func serveAction(ctx *cli.Context) error {
|
||||
Logger: log.Default(),
|
||||
}
|
||||
|
||||
// Opt-in x402 payments: enabled when a pay-to address is given.
|
||||
if payTo := ctx.String("x402_pay_to"); payTo != "" {
|
||||
opts.Payment = &x402.Config{
|
||||
PayTo: payTo,
|
||||
Price: ctx.String("x402_price"),
|
||||
Network: ctx.String("x402_network"),
|
||||
FacilitatorURL: ctx.String("x402_facilitator"),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shutdown gracefully
|
||||
ctx2, cancel := context.WithCancel(opts.Context)
|
||||
opts.Context = ctx2
|
||||
@@ -359,7 +387,7 @@ func testAction(ctx *cli.Context) error {
|
||||
|
||||
serviceName := parts[0]
|
||||
endpointName := parts[1]
|
||||
|
||||
|
||||
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
|
||||
if len(parts) == 3 {
|
||||
endpointName = parts[1] + "." + parts[2]
|
||||
@@ -401,10 +429,10 @@ func testAction(ctx *cli.Context) error {
|
||||
if c == nil {
|
||||
c = client.DefaultClient
|
||||
}
|
||||
|
||||
|
||||
// Create request with bytes frame
|
||||
req := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
|
||||
|
||||
|
||||
// Make the call
|
||||
var rsp bytes.Frame
|
||||
if err := c.Call(opts.Context, req, &rsp); err != nil {
|
||||
@@ -415,7 +443,7 @@ func testAction(ctx *cli.Context) error {
|
||||
// Parse and display response
|
||||
fmt.Println("✅ Call successful!")
|
||||
fmt.Println("\nResponse:")
|
||||
|
||||
|
||||
// Try to pretty-print JSON response
|
||||
var result interface{}
|
||||
if err := json.Unmarshal(rsp.Data, &result); err == nil {
|
||||
@@ -442,7 +470,7 @@ func parseTool(toolName string) []string {
|
||||
func docsAction(ctx *cli.Context) error {
|
||||
// Get registry
|
||||
reg := registry.DefaultRegistry
|
||||
|
||||
|
||||
// Create temporary MCP server to discover tools
|
||||
opts := mcp.Options{
|
||||
Registry: reg,
|
||||
@@ -472,15 +500,15 @@ func docsAction(ctx *cli.Context) error {
|
||||
|
||||
// Collect all tools with metadata
|
||||
type ToolDoc struct {
|
||||
Name string `json:"name"`
|
||||
Service string `json:"service"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Description string `json:"description"`
|
||||
Example string `json:"example,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Service string `json:"service"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Description string `json:"description"`
|
||||
Example string `json:"example,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
var tools []ToolDoc
|
||||
for _, svc := range services {
|
||||
fullSvcs, err := opts.Registry.GetService(svc.Name)
|
||||
@@ -496,22 +524,22 @@ func docsAction(ctx *cli.Context) error {
|
||||
Description: fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name),
|
||||
Metadata: ep.Metadata,
|
||||
}
|
||||
|
||||
|
||||
// Extract description from metadata if available
|
||||
if desc, ok := ep.Metadata["description"]; ok {
|
||||
toolDoc.Description = desc
|
||||
}
|
||||
|
||||
|
||||
// Extract example from metadata if available
|
||||
if example, ok := ep.Metadata["example"]; ok {
|
||||
toolDoc.Example = example
|
||||
}
|
||||
|
||||
|
||||
// Extract scopes from metadata if available
|
||||
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
|
||||
toolDoc.Scopes = strings.Split(scopesStr, ",")
|
||||
}
|
||||
|
||||
|
||||
tools = append(tools, toolDoc)
|
||||
}
|
||||
}
|
||||
@@ -525,38 +553,37 @@ func docsAction(ctx *cli.Context) error {
|
||||
"tools": tools,
|
||||
"count": len(tools),
|
||||
})
|
||||
|
||||
|
||||
case "markdown":
|
||||
fmt.Fprintf(writer, "# MCP Tools Documentation\n\n")
|
||||
fmt.Fprintf(writer, "Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(writer, "Total Tools: %d\n\n", len(tools))
|
||||
|
||||
|
||||
// Group by service
|
||||
serviceMap := make(map[string][]ToolDoc)
|
||||
for _, tool := range tools {
|
||||
serviceMap[tool.Service] = append(serviceMap[tool.Service], tool)
|
||||
}
|
||||
|
||||
|
||||
for service, serviceTools := range serviceMap {
|
||||
fmt.Fprintf(writer, "## Service: %s\n\n", service)
|
||||
|
||||
|
||||
for _, tool := range serviceTools {
|
||||
fmt.Fprintf(writer, "### %s\n\n", tool.Name)
|
||||
fmt.Fprintf(writer, "**Description:** %s\n\n", tool.Description)
|
||||
|
||||
|
||||
if len(tool.Scopes) > 0 {
|
||||
fmt.Fprintf(writer, "**Required Scopes:** %s\n\n", strings.Join(tool.Scopes, ", "))
|
||||
}
|
||||
|
||||
|
||||
if tool.Example != "" {
|
||||
fmt.Fprintf(writer, "**Example Input:**\n```json\n%s\n```\n\n", tool.Example)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s (supported: markdown, json)", format)
|
||||
}
|
||||
@@ -569,10 +596,10 @@ func exportAction(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
exportFormat := ctx.Args().First()
|
||||
|
||||
|
||||
// Get registry
|
||||
reg := registry.DefaultRegistry
|
||||
|
||||
|
||||
// Create temporary MCP server to discover tools
|
||||
opts := mcp.Options{
|
||||
Registry: reg,
|
||||
@@ -619,7 +646,7 @@ func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Opt
|
||||
fmt.Fprintf(writer, "import requests\nimport json\n\n")
|
||||
fmt.Fprintf(writer, "# Configure your MCP gateway endpoint\n")
|
||||
fmt.Fprintf(writer, "MCP_GATEWAY_URL = 'http://localhost:3000/mcp'\n\n")
|
||||
|
||||
|
||||
fmt.Fprintf(writer, "def call_mcp_tool(tool_name, arguments):\n")
|
||||
fmt.Fprintf(writer, " \"\"\"Call an MCP tool via HTTP gateway\"\"\"\n")
|
||||
fmt.Fprintf(writer, " response = requests.post(\n")
|
||||
@@ -628,10 +655,10 @@ func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Opt
|
||||
fmt.Fprintf(writer, " )\n")
|
||||
fmt.Fprintf(writer, " response.raise_for_status()\n")
|
||||
fmt.Fprintf(writer, " return response.json()\n\n")
|
||||
|
||||
|
||||
fmt.Fprintf(writer, "# Define tools\n")
|
||||
fmt.Fprintf(writer, "tools = []\n\n")
|
||||
|
||||
|
||||
for _, svc := range services {
|
||||
fullSvcs, err := opts.Registry.GetService(svc.Name)
|
||||
if err != nil || len(fullSvcs) == 0 {
|
||||
@@ -641,19 +668,19 @@ func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Opt
|
||||
for _, ep := range fullSvcs[0].Endpoints {
|
||||
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
|
||||
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
|
||||
|
||||
|
||||
if desc, ok := ep.Metadata["description"]; ok {
|
||||
description = desc
|
||||
}
|
||||
|
||||
|
||||
// Generate Python function name (replace dots with underscores)
|
||||
funcName := strings.ReplaceAll(toolName, ".", "_")
|
||||
|
||||
|
||||
fmt.Fprintf(writer, "def %s(arguments: str) -> str:\n", funcName)
|
||||
fmt.Fprintf(writer, " \"\"\"% s\"\"\"\n", description)
|
||||
fmt.Fprintf(writer, " args = json.loads(arguments) if isinstance(arguments, str) else arguments\n")
|
||||
fmt.Fprintf(writer, " return json.dumps(call_mcp_tool('%s', args))\n\n", toolName)
|
||||
|
||||
|
||||
fmt.Fprintf(writer, "tools.append(Tool(\n")
|
||||
fmt.Fprintf(writer, " name='%s',\n", toolName)
|
||||
fmt.Fprintf(writer, " func=%s,\n", funcName)
|
||||
@@ -661,7 +688,7 @@ func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Opt
|
||||
fmt.Fprintf(writer, "))\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fmt.Fprintf(writer, "# Example usage:\n")
|
||||
fmt.Fprintf(writer, "# from langchain.agents import initialize_agent, AgentType\n")
|
||||
fmt.Fprintf(writer, "# from langchain.llms import OpenAI\n")
|
||||
@@ -669,7 +696,7 @@ func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Opt
|
||||
fmt.Fprintf(writer, "# llm = OpenAI(temperature=0)\n")
|
||||
fmt.Fprintf(writer, "# agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)\n")
|
||||
fmt.Fprintf(writer, "# agent.run('Your query here')\n")
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -690,9 +717,9 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
},
|
||||
"paths": make(map[string]interface{}),
|
||||
}
|
||||
|
||||
|
||||
paths := spec["paths"].(map[string]interface{})
|
||||
|
||||
|
||||
for _, svc := range services {
|
||||
fullSvcs, err := opts.Registry.GetService(svc.Name)
|
||||
if err != nil || len(fullSvcs) == 0 {
|
||||
@@ -702,12 +729,12 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
for _, ep := range fullSvcs[0].Endpoints {
|
||||
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
|
||||
path := fmt.Sprintf("/mcp/call/%s", strings.ReplaceAll(toolName, ".", "/"))
|
||||
|
||||
|
||||
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
|
||||
if desc, ok := ep.Metadata["description"]; ok {
|
||||
description = desc
|
||||
}
|
||||
|
||||
|
||||
operation := map[string]interface{}{
|
||||
"summary": toolName,
|
||||
"description": description,
|
||||
@@ -735,7 +762,7 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// Add scope security if available
|
||||
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
|
||||
operation["security"] = []map[string]interface{}{
|
||||
@@ -744,13 +771,13 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
paths[path] = map[string]interface{}{
|
||||
"post": operation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add security schemes
|
||||
spec["components"] = map[string]interface{}{
|
||||
"securitySchemes": map[string]interface{}{
|
||||
@@ -760,7 +787,7 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
enc := json.NewEncoder(writer)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(spec)
|
||||
@@ -769,7 +796,7 @@ func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Optio
|
||||
// exportJSON exports raw tool definitions as JSON
|
||||
func exportJSON(writer *os.File, services []*registry.Service, opts mcp.Options) error {
|
||||
var tools []map[string]interface{}
|
||||
|
||||
|
||||
for _, svc := range services {
|
||||
fullSvcs, err := opts.Registry.GetService(svc.Name)
|
||||
if err != nil || len(fullSvcs) == 0 {
|
||||
@@ -783,23 +810,23 @@ func exportJSON(writer *os.File, services []*registry.Service, opts mcp.Options)
|
||||
"endpoint": ep.Name,
|
||||
"metadata": ep.Metadata,
|
||||
}
|
||||
|
||||
|
||||
if desc, ok := ep.Metadata["description"]; ok {
|
||||
tool["description"] = desc
|
||||
}
|
||||
|
||||
|
||||
if example, ok := ep.Metadata["example"]; ok {
|
||||
tool["example"] = example
|
||||
}
|
||||
|
||||
|
||||
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
|
||||
tool["scopes"] = strings.Split(scopesStr, ",")
|
||||
}
|
||||
|
||||
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enc := json.NewEncoder(writer)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(map[string]interface{}{
|
||||
|
||||
+14
-2
@@ -32,6 +32,7 @@ import (
|
||||
"go-micro.dev/v5/metadata"
|
||||
"go-micro.dev/v5/registry"
|
||||
"go-micro.dev/v5/store"
|
||||
"go-micro.dev/v5/wrapper/x402"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -151,6 +152,11 @@ type Options struct {
|
||||
// TraceProvider: tp,
|
||||
// })
|
||||
TraceProvider trace.TracerProvider
|
||||
|
||||
// Payment, when set, requires an x402 payment for tool calls
|
||||
// (the /mcp/call endpoint). Listing tools and health stay free.
|
||||
// Opt-in: leave nil to disable payments.
|
||||
Payment *x402.Config
|
||||
}
|
||||
|
||||
// Server represents a running MCP gateway
|
||||
@@ -553,9 +559,15 @@ func (s *Server) watchServices() {
|
||||
func (s *Server) serveHTTP() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// MCP endpoints
|
||||
// MCP endpoints. Tool calls can be gated behind an x402 payment;
|
||||
// listing tools and health stay free.
|
||||
var call http.Handler = http.HandlerFunc(s.handleCallTool)
|
||||
if s.opts.Payment != nil {
|
||||
call = x402.Middleware(*s.opts.Payment)(call)
|
||||
s.opts.Logger.Printf("[mcp] x402 payments enabled (network=%s, payTo=%s)", s.opts.Payment.Network, s.opts.Payment.PayTo)
|
||||
}
|
||||
mux.HandleFunc("/mcp/tools", s.handleListTools)
|
||||
mux.HandleFunc("/mcp/call", s.handleCallTool)
|
||||
mux.Handle("/mcp/call", call)
|
||||
mux.HandleFunc("/health", s.handleHealth)
|
||||
|
||||
// WebSocket endpoint for bidirectional streaming
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
layout: blog
|
||||
title: "Integrating x402: Payments for Agents"
|
||||
permalink: /blog/22
|
||||
description: "Agents that act on their own eventually need to pay on their own. Go Micro now speaks x402 — the HTTP 402 payment standard — so a tool can require a stablecoin payment and an agent can settle it, with the chain pluggable behind a facilitator."
|
||||
---
|
||||
|
||||
# Integrating x402: Payments for Agents
|
||||
|
||||
*June 15, 2026 • Asim Aslam*
|
||||
|
||||
The [last post](/blog/21) was about agents that run on their own — triggered by an event, acting without a human prompt. Follow that one step further and you reach something agents can't do yet in most systems: pay. An autonomous agent that calls an API, rents compute, or uses another agent's service will, sooner or later, need to settle for it — without a person reaching for a credit card. There is now a standard for exactly that, and we're integrating it.
|
||||
|
||||
## What x402 is
|
||||
|
||||
x402 is an open payment protocol built on the HTTP **402 Payment Required** status code. The flow is simple: a client requests a resource, the server answers `402` with machine-readable payment requirements (amount, asset, network, where to pay), the client pays and retries with an `X-PAYMENT` header, and the server verifies the payment and serves the resource. It's designed for stablecoins and for machine-to-machine use — agents paying for things, per request.
|
||||
|
||||
It started at Coinbase, it's multi-chain (Base, Solana, Ethereum, Polygon, and more), and as of April 2026 it's governed by the **x402 Foundation under the Linux Foundation**, with founding members including Google, Visa, Stripe, AWS, Mastercard, Circle, and Shopify. That governance is why we're comfortable integrating it: it's an open standard with the payments industry behind it, not a single vendor's API.
|
||||
|
||||
## How Go Micro integrates it
|
||||
|
||||
The same way it integrates everything else — interface-first, with a default, and pluggable.
|
||||
|
||||
The core is HTTP middleware in `wrapper/x402`. It enforces the 402 challenge and verifies payments, but it carries **no chain or crypto code**. Verification and settlement are delegated to a pluggable **Facilitator**:
|
||||
|
||||
```go
|
||||
type Facilitator interface {
|
||||
Verify(ctx context.Context, payment string, req Requirements) (Result, error)
|
||||
}
|
||||
```
|
||||
|
||||
So Go Micro stays chain-agnostic. "Base through Coinbase" and "Solana through Alchemy" are not two integrations — they're the same middleware pointed at two facilitators. The facilitator does the on-chain work; the framework speaks the protocol.
|
||||
|
||||
```go
|
||||
pay := x402.Middleware(x402.Config{
|
||||
PayTo: "0xYourAddress", // where payments go
|
||||
Network: "solana", // or "base", ...
|
||||
Price: "10000", // smallest units, e.g. 0.01 USDC
|
||||
})
|
||||
mux.Handle("/paid", pay(handler))
|
||||
```
|
||||
|
||||
## Opt-in, at the gateway
|
||||
|
||||
Because every Go Micro endpoint is already an AI-callable tool through the MCP gateway, that's the natural place to charge: a tool call is the thing worth a payment. So x402 is wired into both the built-in `micro mcp serve` and the standalone `micro-mcp-gateway`, and it is strictly **opt-in** — off unless you set a pay-to address.
|
||||
|
||||
```bash
|
||||
micro mcp serve --address :3000 \
|
||||
--x402-pay-to 0xYourAddress \
|
||||
--x402-network solana \
|
||||
--x402-price 10000 \
|
||||
--x402-facilitator https://facilitator.example
|
||||
```
|
||||
|
||||
With payments enabled, the `/mcp/call` endpoint requires a verified payment; listing tools and health checks stay free. Without the flag, nothing changes. The standalone gateway takes the same options via flags or environment variables, so you can put a paid gateway in front of services you didn't write.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Go Micro's premise has been that every service is a tool an agent can call. x402 adds one word: a *paid* tool. That turns "tools as services" into something with an economic side — a service can charge per call, and an agent can pay for it, with no human in the loop on either end. It gives the services people build a native way to be paid for, and it gives Go Micro a place in the supply side of an agent economy: the rails for agents to act *and* transact.
|
||||
|
||||
## Honest about the edges
|
||||
|
||||
- **It's opt-in and dependency-light.** No pay-to address, no payments. Go Micro pulls in no chain libraries — the facilitator does that work.
|
||||
- **Pricing starts simple.** A flat price per tool call today; richer models — per-tool pricing, metered usage, subscriptions — are the harder design work to come.
|
||||
- **A paying agent needs a budget.** Blog 21 argued that unattended agents need guardrails; an unattended agent that spends money needs them most. A spend cap belongs next to `MaxSteps` and `ApproveTool`, and it's the next piece to build on the agent side.
|
||||
|
||||
Agents that act, and now can pay. Services, agents, workflows, and payments — the substrate for software that operates, and transacts, on its own.
|
||||
|
||||
---
|
||||
|
||||
Sources: [x402 — Coinbase Developer Docs](https://docs.cdp.coinbase.com/x402/welcome), [What is x402? — Alchemy](https://www.alchemy.com/blog/how-x402-brings-real-time-crypto-payments-to-the-web), [x402 on Solana — solana.com](https://solana.com/x402/what-is-x402).
|
||||
@@ -11,6 +11,13 @@ permalink: /blog/
|
||||
|
||||
<div class="posts">
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/22">Integrating x402: Payments for Agents</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">June 15, 2026</p>
|
||||
<p>Agents that act on their own eventually need to pay on their own. Go Micro now speaks x402 — the HTTP 402 payment standard — so a tool can require a stablecoin payment and an agent can settle it, with the chain pluggable behind a facilitator.</p>
|
||||
<a href="/blog/22">Read more →</a>
|
||||
</article>
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/21">When the Event Is the Prompt</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">June 15, 2026</p>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// Package x402 implements the server side of the x402 payment protocol
|
||||
// (the HTTP 402 "Payment Required" standard) as pluggable middleware.
|
||||
//
|
||||
// It lets a service or gateway require a stablecoin payment per request
|
||||
// and verify it through a pluggable Facilitator (Coinbase CDP, Alchemy,
|
||||
// or self-hosted), so AI agents can pay for tools and APIs autonomously.
|
||||
// Go Micro stays chain-agnostic and free of crypto dependencies: it
|
||||
// speaks the HTTP protocol and delegates verification and settlement to
|
||||
// the facilitator, which does the on-chain work.
|
||||
//
|
||||
// pay := x402.Middleware(x402.Config{
|
||||
// PayTo: "0xYourAddress", // where payments go
|
||||
// Network: "base", // or "solana", ...
|
||||
// Price: "10000", // smallest units (e.g. 0.01 USDC)
|
||||
// })
|
||||
// mux.Handle("/paid", pay(handler))
|
||||
//
|
||||
// x402 is governed by the x402 Foundation (Linux Foundation). See
|
||||
// https://x402.org and https://docs.cdp.coinbase.com/x402.
|
||||
package x402
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Version is the x402 protocol version this package speaks.
|
||||
const Version = 1
|
||||
|
||||
// Header names defined by the protocol.
|
||||
const (
|
||||
PaymentHeader = "X-PAYMENT" // request: the client's payment payload
|
||||
PaymentResponseHeader = "X-PAYMENT-RESPONSE" // response: settlement details
|
||||
)
|
||||
|
||||
// Requirements describes what a client must pay to access a resource —
|
||||
// the body of a 402 response (one entry of "accepts").
|
||||
type Requirements struct {
|
||||
Scheme string `json:"scheme"` // payment scheme, e.g. "exact"
|
||||
Network string `json:"network"` // chain, e.g. "base", "solana"
|
||||
MaxAmountRequired string `json:"maxAmountRequired"` // amount in the asset's smallest unit
|
||||
Resource string `json:"resource"` // the resource being paid for
|
||||
Description string `json:"description,omitempty"` // human/agent-readable description
|
||||
PayTo string `json:"payTo"` // receiving address
|
||||
Asset string `json:"asset,omitempty"` // token contract/mint (default: network USDC)
|
||||
MaxTimeoutSeconds int `json:"maxTimeoutSeconds,omitempty"` // how long the client has to pay
|
||||
}
|
||||
|
||||
// challenge is the JSON body returned with a 402 response.
|
||||
type challenge struct {
|
||||
X402Version int `json:"x402Version"`
|
||||
Accepts []Requirements `json:"accepts"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Result is the outcome of verifying a payment.
|
||||
type Result struct {
|
||||
Valid bool // whether the payment satisfies the requirements
|
||||
Payer string // the paying address, if known
|
||||
Reason string // why the payment was rejected, if not valid
|
||||
Settlement string // settlement reference (e.g. tx hash), set into X-PAYMENT-RESPONSE
|
||||
}
|
||||
|
||||
// Facilitator verifies (and optionally settles) a payment a client
|
||||
// presented against the stated requirements. Implementations talk to a
|
||||
// chain or a hosted facilitator; the gateway stays chain-agnostic, so a
|
||||
// Base facilitator and a Solana facilitator are just different
|
||||
// implementations behind this interface.
|
||||
type Facilitator interface {
|
||||
Verify(ctx context.Context, payment string, req Requirements) (Result, error)
|
||||
}
|
||||
|
||||
// Config configures payment enforcement for a set of routes.
|
||||
type Config struct {
|
||||
// PayTo is the address payments are sent to. Required.
|
||||
PayTo string
|
||||
// Network is the chain to settle on (default "base").
|
||||
Network string
|
||||
// Asset is the token contract/mint (default: the network's USDC).
|
||||
Asset string
|
||||
// Price is the amount required per request, in the asset's smallest
|
||||
// unit (e.g. "10000" for 0.01 USDC at 6 decimals).
|
||||
Price string
|
||||
// Description is shown to the paying client/agent.
|
||||
Description string
|
||||
// Facilitator verifies payments. Defaults to an HTTPFacilitator
|
||||
// pointed at FacilitatorURL.
|
||||
Facilitator Facilitator
|
||||
// FacilitatorURL is the verify/settle endpoint used when Facilitator
|
||||
// is nil (e.g. Coinbase CDP or Alchemy).
|
||||
FacilitatorURL string
|
||||
}
|
||||
|
||||
func (c Config) network() string {
|
||||
if c.Network == "" {
|
||||
return "base"
|
||||
}
|
||||
return c.Network
|
||||
}
|
||||
|
||||
func (c Config) requirements(r *http.Request) Requirements {
|
||||
return Requirements{
|
||||
Scheme: "exact",
|
||||
Network: c.network(),
|
||||
MaxAmountRequired: c.Price,
|
||||
Resource: r.URL.Path,
|
||||
Description: c.Description,
|
||||
PayTo: c.PayTo,
|
||||
Asset: c.Asset,
|
||||
MaxTimeoutSeconds: 60,
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware returns HTTP middleware that requires an x402 payment before
|
||||
// the wrapped handler runs. A request without a valid X-PAYMENT header
|
||||
// receives a 402 with the payment requirements; once a payment verifies,
|
||||
// the request is served.
|
||||
func Middleware(cfg Config) func(http.Handler) http.Handler {
|
||||
fac := cfg.Facilitator
|
||||
if fac == nil {
|
||||
fac = &HTTPFacilitator{URL: cfg.FacilitatorURL}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := cfg.requirements(r)
|
||||
|
||||
payment := r.Header.Get(PaymentHeader)
|
||||
if payment == "" {
|
||||
writeChallenge(w, req, "payment required")
|
||||
return
|
||||
}
|
||||
|
||||
res, err := fac.Verify(r.Context(), payment, req)
|
||||
if err != nil {
|
||||
writeChallenge(w, req, "payment verification failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
if !res.Valid {
|
||||
reason := res.Reason
|
||||
if reason == "" {
|
||||
reason = "payment invalid"
|
||||
}
|
||||
writeChallenge(w, req, reason)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Settlement != "" {
|
||||
w.Header().Set(PaymentResponseHeader, res.Settlement)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeChallenge(w http.ResponseWriter, req Requirements, reason string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusPaymentRequired) // 402
|
||||
_ = json.NewEncoder(w).Encode(challenge{
|
||||
X402Version: Version,
|
||||
Accepts: []Requirements{req},
|
||||
Error: reason,
|
||||
})
|
||||
}
|
||||
|
||||
// HTTPFacilitator verifies payments by POSTing to an x402 facilitator's
|
||||
// verify endpoint (Coinbase CDP, Alchemy, or self-hosted). It carries no
|
||||
// chain or crypto code itself.
|
||||
type HTTPFacilitator struct {
|
||||
URL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func (f *HTTPFacilitator) Verify(ctx context.Context, payment string, req Requirements) (Result, error) {
|
||||
if f.URL == "" {
|
||||
return Result{}, fmt.Errorf("no facilitator configured")
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"x402Version": Version,
|
||||
"paymentPayload": payment,
|
||||
"paymentRequirements": req,
|
||||
})
|
||||
hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, f.URL+"/verify", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
hreq.Header.Set("Content-Type", "application/json")
|
||||
cl := f.Client
|
||||
if cl == nil {
|
||||
cl = http.DefaultClient
|
||||
}
|
||||
resp, err := cl.Do(hreq)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
InvalidReason string `json:"invalidReason"`
|
||||
Payer string `json:"payer"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{Valid: out.IsValid, Reason: out.InvalidReason, Payer: out.Payer}, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package x402
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockFacilitator struct {
|
||||
valid bool
|
||||
reason string
|
||||
}
|
||||
|
||||
func (m mockFacilitator) Verify(ctx context.Context, payment string, req Requirements) (Result, error) {
|
||||
return Result{Valid: m.valid, Reason: m.reason, Payer: "0xpayer", Settlement: "0xtx"}, nil
|
||||
}
|
||||
|
||||
func paidHandler(cfg Config) http.Handler {
|
||||
served := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
return Middleware(cfg)(served)
|
||||
}
|
||||
|
||||
// No payment yields a 402 with the requirements describing where to pay.
|
||||
func TestChallengeWhenNoPayment(t *testing.T) {
|
||||
h := paidHandler(Config{PayTo: "0xabc", Network: "solana", Price: "10000", Facilitator: mockFacilitator{valid: true}})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tool", nil))
|
||||
|
||||
if rec.Code != http.StatusPaymentRequired {
|
||||
t.Fatalf("status = %d, want 402", rec.Code)
|
||||
}
|
||||
var ch challenge
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &ch); err != nil {
|
||||
t.Fatalf("challenge body: %v", err)
|
||||
}
|
||||
if ch.X402Version != Version || len(ch.Accepts) != 1 {
|
||||
t.Fatalf("unexpected challenge: %+v", ch)
|
||||
}
|
||||
req := ch.Accepts[0]
|
||||
if req.PayTo != "0xabc" || req.Network != "solana" || req.MaxAmountRequired != "10000" {
|
||||
t.Errorf("requirements not advertised correctly: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
// A payment the facilitator accepts lets the request through, and the
|
||||
// settlement is surfaced on the response.
|
||||
func TestServesWhenPaymentValid(t *testing.T) {
|
||||
h := paidHandler(Config{PayTo: "0xabc", Price: "10000", Facilitator: mockFacilitator{valid: true}})
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/tool", nil)
|
||||
r.Header.Set(PaymentHeader, "base64-payment-payload")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, r)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != "ok" {
|
||||
t.Errorf("handler not served, body = %q", rec.Body.String())
|
||||
}
|
||||
if rec.Header().Get(PaymentResponseHeader) != "0xtx" {
|
||||
t.Errorf("settlement not surfaced in %s", PaymentResponseHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// A payment the facilitator rejects gets a 402 with the reason.
|
||||
func TestChallengeWhenPaymentInvalid(t *testing.T) {
|
||||
h := paidHandler(Config{PayTo: "0xabc", Price: "10000", Facilitator: mockFacilitator{valid: false, reason: "insufficient amount"}})
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/tool", nil)
|
||||
r.Header.Set(PaymentHeader, "bad-payment")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, r)
|
||||
|
||||
if rec.Code != http.StatusPaymentRequired {
|
||||
t.Fatalf("status = %d, want 402", rec.Code)
|
||||
}
|
||||
var ch challenge
|
||||
json.Unmarshal(rec.Body.Bytes(), &ch)
|
||||
if ch.Error != "insufficient amount" {
|
||||
t.Errorf("reason not surfaced: %q", ch.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Network defaults to base when unset.
|
||||
func TestNetworkDefault(t *testing.T) {
|
||||
if got := (Config{}).network(); got != "base" {
|
||||
t.Errorf("default network = %q, want base", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user