Go Server SDK
This guide covers how to build payment-accepting servers in Go using the t402 package.
Overview
A t402 server is an application that protects HTTP resources with payment requirements. The server:
- Defines which routes require payment
- Returns 402 Payment Required for unpaid requests
- Verifies payment signatures (via facilitator)
- Settles payments on-chain (via facilitator)
- Returns the protected resource after successful payment
Quick Start
Installation
go get github.com/t402-io/t402/sdks/goBasic Gin Server
package main
import (
"github.com/gin-gonic/gin"
t402 "github.com/t402-io/t402/sdks/go"
t402http "github.com/t402-io/t402/sdks/go/http"
ginmw "github.com/t402-io/t402/sdks/go/http/gin"
evm "github.com/t402-io/t402/sdks/go/mechanisms/evm/exact/server"
)
func main() {
r := gin.Default()
// 1. Configure payment routes
routes := t402http.RoutesConfig{
"GET /data": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact",
PayTo: "0x...",
Price: "$0.001",
Network: "eip155:84532",
},
},
Description: "Get data",
MimeType: "application/json",
},
}
// 2. Create facilitator client
facilitator := t402http.NewHTTPFacilitatorClient(&t402http.FacilitatorConfig{
URL: "https://facilitator.t402.io",
})
// 3. Add payment middleware
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: facilitator,
Schemes: []ginmw.SchemeConfig{
{Network: "eip155:84532", Server: evm.NewExactEvmScheme()},
},
}))
// 4. Protected endpoint handler
r.GET("/data", func(c *gin.Context) {
c.JSON(200, gin.H{"result": "protected data"})
})
r.Run(":8080")
}Core Concepts
1. Route Configuration
Routes define payment requirements for specific endpoints.
routes := t402http.RoutesConfig{
"GET /resource": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact", // Payment scheme (exact, upto, etc.)
PayTo: "0x...", // Payment recipient address
Price: "$0.001", // Price in USD
Network: "eip155:84532", // Blockchain network
},
},
Description: "Resource description",
MimeType: "application/json",
},
}Pattern Matching
Route keys use pattern matching:
routes := t402http.RoutesConfig{
"GET /exact-match": {...}, // Exact path match
"GET /users/*": {...}, // Wildcard suffix
"*": {...}, // All routes
}2. Resource Server Core (t402.X402ResourceServer)
The core server manages payment verification and requirements.
Key Methods:
server := t402.Newt402ResourceServer(
t402.WithFacilitatorClient(facilitator),
t402.WithSchemeServer(network, schemeServer),
)
// Build payment requirements for a resource
requirements, _ := server.BuildPaymentRequirements(ctx, config)
// Verify payment
verifyResult, _ := server.VerifyPayment(ctx, payload, requirements)
// Settle payment
settleResult, _ := server.SettlePayment(ctx, payload, requirements)3. HTTP Integration
The HTTP layer adds request/response handling.
// Create HTTP resource server
httpServer := t402http.Newt402HTTPResourceServer(
routes,
t402.WithFacilitatorClient(facilitator),
t402.WithSchemeServer(network, schemeServer),
)
// Process HTTP requests
result := httpServer.ProcessHTTPRequest(ctx, reqCtx, nil)
// Handle settlement
headers, _ := httpServer.ProcessSettlement(ctx, payload, requirements, statusCode)4. Facilitator Client
Servers use facilitator clients to verify and settle payments.
facilitator := t402http.NewHTTPFacilitatorClient(&t402http.FacilitatorConfig{
URL: "https://facilitator.t402.io",
})
// Verify payment (called by middleware)
verifyResp, err := facilitator.Verify(ctx, payloadBytes, requirementsBytes)
// Settle payment (called by middleware)
settleResp, err := facilitator.Settle(ctx, payloadBytes, requirementsBytes)Middleware
Gin Middleware
import ginmw "github.com/t402-io/t402/sdks/go/http/gin"
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: facilitator,
Schemes: schemes,
Timeout: 30 * time.Second,
}))Configuration Options:
Routes- Payment requirements per routeFacilitator- Facilitator client for verification/settlementSchemes- Scheme servers to registerInitialize- Query facilitator on startupTimeout- Context timeout for operationsErrorHandler- Custom error handlingSettlementHandler- Called after successful settlement
Custom Middleware
Implement custom middleware using the HTTP server directly:
func customPaymentMiddleware(server *t402http.HTTPServer) gin.HandlerFunc {
return func(c *gin.Context) {
adapter := NewGinAdapter(c)
reqCtx := t402http.HTTPRequestContext{
Adapter: adapter,
Path: c.Request.URL.Path,
Method: c.Request.Method,
}
result := server.ProcessHTTPRequest(ctx, reqCtx, nil)
switch result.Type {
case t402http.ResultNoPaymentRequired:
c.Next()
case t402http.ResultPaymentError:
// Return 402 with payment requirements
case t402http.ResultPaymentVerified:
// Continue and settle
}
}
}Advanced Features
Dynamic Pricing
Charge different amounts based on request context:
routes := t402http.RoutesConfig{
"GET /data": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact",
PayTo: "0x...",
Network: "eip155:84532",
Price: t402http.DynamicPriceFunc(func(ctx context.Context, reqCtx t402http.HTTPRequestContext) (t402.Price, error) {
tier := extractTierFromRequest(reqCtx)
if tier == "premium" {
return "$0.005", nil
}
return "$0.001", nil
}),
},
},
},
}Dynamic PayTo
Route payments to different addresses:
routes := t402http.RoutesConfig{
"GET /marketplace/item/*": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact",
Price: "$10.00",
Network: "eip155:84532",
PayTo: t402http.DynamicPayToFunc(func(ctx context.Context, reqCtx t402http.HTTPRequestContext) (string, error) {
sellerID := extractSellerFromPath(reqCtx.Path)
return getSellerAddress(sellerID)
}),
},
},
},
}Custom Money Parser
Use alternative tokens for payments:
evmScheme := evm.NewExactEvmScheme().RegisterMoneyParser(
func(amount float64, network t402.Network) (*t402.AssetAmount, error) {
// Use DAI for large amounts
if amount > 100 {
return &t402.AssetAmount{
Amount: fmt.Sprintf("%.0f", amount*1e18),
Asset: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", // DAI
Extra: map[string]interface{}{"token": "DAI"},
}, nil
}
return nil, nil // Use default USDC for small amounts
},
)Lifecycle Hooks
Run custom logic during payment processing:
server := t402.Newt402ResourceServer(
t402.WithFacilitatorClient(facilitator),
t402.WithSchemeServer(network, schemeServer),
)
server.OnBeforeVerify(func(ctx t402.VerifyContext) (*t402.BeforeHookResult, error) {
log.Printf("Verifying payment for %s", ctx.Requirements.Network)
return nil, nil
})
server.OnAfterSettle(func(ctx t402.SettleResultContext) error {
log.Printf("Payment settled: %s", ctx.Result.Transaction)
return nil
})Extensions
Add protocol extensions like Bazaar discovery:
import (
"github.com/t402-io/t402/sdks/go/extensions/bazaar"
"github.com/t402-io/t402/sdks/go/extensions/types"
)
discoveryExt, _ := bazaar.DeclareDiscoveryExtension(
bazaar.MethodGET,
map[string]interface{}{"city": "San Francisco"},
&types.InputConfig{...},
"",
&types.OutputConfig{...},
)
routes := t402http.RoutesConfig{
"GET /weather": {
Accepts: t402http.PaymentOptions{
{Scheme: "exact", PayTo: "0x...", Price: "$0.001", Network: "eip155:84532"},
},
Extensions: map[string]interface{}{
types.BAZAAR: discoveryExt,
},
},
}API Reference
t402.X402ResourceServer
Constructor:
func Newt402ResourceServer(opts ...ResourceServerOption) *X402ResourceServerOptions:
func WithFacilitatorClient(client FacilitatorClient) ResourceServerOption
func WithSchemeServer(network Network, server SchemeNetworkServer) ResourceServerOptionHook Methods:
func (s *X402ResourceServer) OnBeforeVerify(hook BeforeVerifyHook) *X402ResourceServer
func (s *X402ResourceServer) OnAfterVerify(hook AfterVerifyHook) *X402ResourceServer
func (s *X402ResourceServer) OnVerifyFailure(hook OnVerifyFailureHook) *X402ResourceServer
func (s *X402ResourceServer) OnBeforeSettle(hook BeforeSettleHook) *X402ResourceServer
func (s *X402ResourceServer) OnAfterSettle(hook AfterSettleHook) *X402ResourceServer
func (s *X402ResourceServer) OnSettleFailure(hook OnSettleFailureHook) *X402ResourceServerPayment Methods:
func (s *X402ResourceServer) BuildPaymentRequirements(ctx context.Context, config ResourceConfig) ([]PaymentRequirements, error)
func (s *X402ResourceServer) VerifyPayment(ctx context.Context, payload PaymentPayload, requirements PaymentRequirements) (VerifyResponse, error)
func (s *X402ResourceServer) SettlePayment(ctx context.Context, payload PaymentPayload, requirements PaymentRequirements) (SettleResponse, error)t402http.RoutesConfig
type RoutesConfig map[string]RouteConfig
type RouteConfig struct {
Accepts []PaymentOption // Payment options for this route
Description string // Resource description
MimeType string // Response content type
Extensions map[string]interface{} // Protocol extensions
}
type PaymentOption struct {
Scheme string // "exact", etc.
PayTo interface{} // string or DynamicPayToFunc
Price interface{} // t402.Price or DynamicPriceFunc
Network t402.Network // "eip155:84532", etc.
}ginmw.Config
type Config struct {
Routes RoutesConfig
Facilitator FacilitatorClient
Facilitators []FacilitatorClient
Schemes []SchemeConfig
Initialize bool
Timeout time.Duration
ErrorHandler func(*gin.Context, error)
SettlementHandler func(*gin.Context, SettleResponse)
}Error Handling
Custom Error Handler
r.Use(ginmw.X402Payment(ginmw.Config{
// ... config ...
ErrorHandler: func(c *gin.Context, err error) {
log.Printf("Payment error: %v", err)
c.JSON(http.StatusPaymentRequired, gin.H{
"error": "Payment failed",
"details": err.Error(),
})
},
}))Settlement Handler
r.Use(ginmw.X402Payment(ginmw.Config{
// ... config ...
SettlementHandler: func(c *gin.Context, resp t402.SettleResponse) {
log.Printf("Payment settled: tx=%s, payer=%s", resp.Transaction, resp.Payer)
// Store in database, emit metrics, etc.
db.RecordPayment(resp.Transaction, resp.Payer)
},
}))Best Practices
1. Initialize on Startup
Query facilitator capabilities during startup:
r.Use(ginmw.X402Payment(ginmw.Config{
SyncFacilitatorOnStart: true, // Query /supported endpoint on start
// ...
}))2. Set Appropriate Timeouts
r.Use(ginmw.X402Payment(ginmw.Config{
Timeout: 30 * time.Second, // Payment operations timeout
// ...
}))3. Use Descriptive Routes
routes := t402http.RoutesConfig{
"GET /api/weather": {
Accepts: t402http.PaymentOptions{
{Scheme: "exact", PayTo: "0x...", Price: "$0.001", Network: "eip155:84532"},
},
Description: "Get current weather data for a city",
MimeType: "application/json",
},
}4. Handle Both Success and Failure
r.Use(ginmw.X402Payment(ginmw.Config{
ErrorHandler: func(c *gin.Context, err error) {
// Log and notify on errors
},
SettlementHandler: func(c *gin.Context, resp t402.SettleResponse) {
// Record successful payments
},
// ...
}))5. Protect Specific Routes
Don’t protect everything:
routes := t402http.RoutesConfig{
// Protected
"GET /api/premium": {Accepts: t402http.PaymentOptions{{Price: "$1.00", ...}}},
"POST /api/compute": {Accepts: t402http.PaymentOptions{{Price: "$5.00", ...}}},
// Leave /health, /docs, etc. unprotected
}Payment Flow
- Client Request - Server receives request
- Route Matching - Check if route requires payment
- Payment Check - Look for payment headers
- Decision:
- No payment required - Continue to handler
- No payment provided - Return 402 with requirements
- Payment provided - Verify with facilitator
- Verification - Facilitator checks signature validity
- Handler Execution - Run protected endpoint
- Settlement - Submit payment transaction on-chain
- Response - Return resource with settlement headers
Advanced Patterns
Multiple Networks
Support payments on multiple blockchains:
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: facilitator,
Schemes: []ginmw.SchemeConfig{
{Network: "eip155:84532", Server: evm.NewExactEvmScheme()},
{Network: "eip155:8453", Server: evm.NewExactEvmScheme()},
{Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", Server: svm.NewExactSvmScheme()},
},
}))Per-Route Configuration
Different prices for different endpoints:
routes := t402http.RoutesConfig{
"GET /api/basic": {Accepts: t402http.PaymentOptions{{Price: "$0.001", ...}}}, // Cheap
"GET /api/premium": {Accepts: t402http.PaymentOptions{{Price: "$0.10", ...}}}, // Medium
"POST /api/compute": {Accepts: t402http.PaymentOptions{{Price: "$1.00", ...}}}, // Expensive
}Tiered Pricing
Implement dynamic pricing based on request context:
routes := t402http.RoutesConfig{
"GET /api/data": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact",
PayTo: "0x...",
Network: "eip155:84532",
Price: t402http.DynamicPriceFunc(func(ctx context.Context, reqCtx t402http.HTTPRequestContext) (t402.Price, error) {
tier := getUserTier(reqCtx)
switch tier {
case "free":
return "$0.10", nil
case "premium":
return "$0.01", nil
case "enterprise":
return "$0.001", nil
default:
return "$0.10", nil
}
}),
},
},
},
}Marketplace Payment Routing
Route payments to different sellers:
routes := t402http.RoutesConfig{
"GET /marketplace/item/*": {
Accepts: t402http.PaymentOptions{
{
Scheme: "exact",
Price: "$10.00",
Network: "eip155:84532",
PayTo: t402http.DynamicPayToFunc(func(ctx context.Context, reqCtx t402http.HTTPRequestContext) (string, error) {
itemID := extractItemID(reqCtx.Path)
seller, err := db.GetItemSeller(itemID)
if err != nil {
return "", err
}
return seller.WalletAddress, nil
}),
},
},
},
}Lifecycle Hooks
Server-Side Hooks
server.OnBeforeVerify(func(ctx VerifyContext) (*BeforeHookResult, error) {
// Custom validation before verification
log.Printf("Verifying payment from %s", ctx.Payload.Payer)
return nil, nil
})
server.OnAfterSettle(func(ctx SettleResultContext) error {
// Record transaction in database
db.RecordPayment(ctx.Result.Transaction, ctx.Result.Payer)
return nil
})Use Cases
Database Logging:
server.OnAfterSettle(func(ctx SettleResultContext) error {
return db.InsertPayment(Payment{
Transaction: ctx.Result.Transaction,
Payer: ctx.Result.Payer,
Network: ctx.Result.Network,
Amount: ctx.Requirements.Amount,
Timestamp: time.Now(),
})
})Metrics:
server.OnAfterVerify(func(ctx VerifyResultContext) error {
metrics.IncrementCounter("payments.verified")
return nil
})Access Control:
server.OnBeforeSettle(func(ctx SettleContext) (*BeforeHookResult, error) {
if isBlacklisted(ctx.Payload.Payer) {
return &BeforeHookResult{
Abort: true,
Reason: "Payer not allowed",
}, nil
}
return nil, nil
})Testing
Testing Protected Endpoints
func TestProtectedEndpoint(t *testing.T) {
// Create test server
r := gin.Default()
// Add mock middleware
r.Use(mockPaymentMiddleware())
r.GET("/protected", handler)
// Test with valid payment
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Payment-Signature", validPayment)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("Expected 200, got %d", w.Code)
}
}Testing Without Payment
func TestUnpaidRequest(t *testing.T) {
r := gin.Default()
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: mockFacilitator,
Schemes: schemes,
}))
r.GET("/protected", handler)
req := httptest.NewRequest("GET", "/protected", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// Should return 402 Payment Required
if w.Code != 402 {
t.Errorf("Expected 402, got %d", w.Code)
}
}Deployment Considerations
Production Checklist
- Use production facilitator URL
- Set appropriate timeouts (30s recommended)
- Implement error and settlement handlers
- Monitor facilitator health
- Rate limit endpoints
- Log payment events
- Set up alerts for payment failures
- Use HTTPS in production
Facilitator Selection
Testnet:
facilitator := t402http.NewHTTPFacilitatorClient(&t402http.FacilitatorConfig{
URL: "https://facilitator.t402.io", // Testnet
})Self-Hosted:
facilitator := t402http.NewHTTPFacilitatorClient(&t402http.FacilitatorConfig{
URL: "https://your-facilitator.example.com",
})Migration from V1
Route Configuration
V1:
routes := t402gin.Routes{
"GET /data": {
Network: "base-sepolia",
// ...
},
}V2:
routes := t402http.RoutesConfig{
"GET /data": {
Network: "eip155:84532", // CAIP-2 format
// ...
},
}Import Paths
V1:
import "github.com/t402-io/t402/sdks/go/middleware/gin"V2:
import ginmw "github.com/t402-io/t402/sdks/go/http/gin"Related Documentation
- Overview - Package overview
- Client SDK - Building clients
- Facilitator - Building facilitators