Go Client SDK
This guide covers how to build payment-enabled clients in Go using the t402 package.
Overview
A t402 client is an application that makes HTTP requests to payment-protected resources. The client automatically:
- Detects when a resource requires payment (402 response)
- Creates a payment payload using registered mechanisms
- Retries the request with payment headers
- Receives the protected resource
Quick Start
Installation
go get github.com/t402-io/t402/sdks/goBasic HTTP Client
package main
import (
"net/http"
t402 "github.com/t402-io/t402/sdks/go"
t402http "github.com/t402-io/t402/sdks/go/http"
evm "github.com/t402-io/t402/sdks/go/mechanisms/evm/exact/client"
evmsigners "github.com/t402-io/t402/sdks/go/signers/evm"
)
func main() {
// 1. Create signer from private key
signer, _ := evmsigners.NewClientSignerFromPrivateKey("0x...")
// 2. Create t402 client and register schemes
client := t402.Newt402Client().
Register("eip155:*", evm.NewExactEvmScheme(signer))
// 3. Wrap HTTP client
httpClient := t402http.WrapHTTPClientWithPayment(
http.DefaultClient,
t402http.Newt402HTTPClient(client),
)
// 4. Make requests - payments handled automatically
resp, _ := httpClient.Get("https://api.example.com/data")
defer resp.Body.Close()
}Core Concepts
1. Signers
Signers create cryptographic signatures for payment payloads.
EVM Signer
import evmsigners "github.com/t402-io/t402/sdks/go/signers/evm"
signer, err := evmsigners.NewClientSignerFromPrivateKey("0x1234...")
if err != nil {
log.Fatal(err)
}
fmt.Println("Address:", signer.Address())SVM Signer
import svmsigners "github.com/t402-io/t402/sdks/go/signers/svm"
signer, err := svmsigners.NewClientSignerFromPrivateKey("5J7W...")
if err != nil {
log.Fatal(err)
}
fmt.Println("Address:", signer.Address())2. Client Core (t402.X402Client)
The core client manages payment scheme registration and payload creation.
Key Methods:
client := t402.Newt402Client()
// Register payment schemes for networks
client.Register(network, schemeClient)
// Create payment payload (called automatically by HTTP wrapper)
payload, err := client.CreatePaymentPayload(ctx, requirements, resource, extensions)3. Mechanism Registration
Register mechanisms to enable payment creation for different networks.
Wildcard Registration (Recommended)
// All EVM networks
client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner))
// All Solana networks
client.Register("solana:*", svm.NewExactSvmScheme(svmSigner))Specific Network Registration
// Ethereum Mainnet
client.Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner))
// Base Mainnet
client.Register("eip155:8453", evm.NewExactEvmScheme(baseSigner))
// Base Sepolia
client.Register("eip155:84532", evm.NewExactEvmScheme(testnetSigner))Registration Precedence
More specific registrations override wildcards:
client.
Register("eip155:*", evm.NewExactEvmScheme(defaultSigner)). // Fallback
Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner)) // Override for mainnet4. HTTP Integration
The HTTP layer adds automatic payment handling to standard HTTP clients.
// Create HTTP-aware t402 client
httpClient := t402http.Newt402HTTPClient(client)
// Wrap any http.Client
wrappedClient := t402http.WrapHTTPClientWithPayment(http.DefaultClient, httpClient)
// Make requests
resp, err := wrappedClient.Get(url)What the wrapper does:
- Intercepts all HTTP requests
- Detects 402 Payment Required responses
- Extracts payment requirements from headers/body
- Creates payment using registered mechanisms
- Retries request with payment signature
- Returns final response to caller
Lifecycle Hooks
Hooks allow you to run custom logic during payment creation.
Available Hooks
client.OnBeforePaymentCreation(func(ctx t402.PaymentCreationContext) (*t402.BeforePaymentCreationResult, error) {
// Called before payment creation
// Can abort by returning: &BeforePaymentCreationResult{Abort: true, Reason: "..."}
fmt.Printf("Creating payment for %s\n", ctx.Requirements.Network)
return nil, nil
})
client.OnAfterPaymentCreation(func(ctx t402.PaymentCreationResultContext) error {
// Called after successful payment creation
// Use for logging, metrics, etc.
fmt.Printf("Payment created: %d bytes\n", len(ctx.PayloadBytes))
return nil
})
client.OnPaymentCreationFailure(func(ctx t402.PaymentCreationFailureContext) (*t402.PaymentCreationFailureResult, error) {
// Called when payment creation fails
// Can recover by returning: &PaymentCreationFailureResult{Recovered: true, Payload: ...}
fmt.Printf("Payment failed: %v\n", ctx.Error)
return nil, nil
})Hook Use Cases
Logging:
client.OnAfterPaymentCreation(func(ctx PaymentCreationResultContext) error {
log.Printf("Payment created for %s: %s", ctx.Requirements.Network, ctx.Requirements.Scheme)
return nil
})Metrics:
client.OnAfterPaymentCreation(func(ctx PaymentCreationResultContext) error {
metrics.IncrementCounter("payments.created", map[string]string{
"network": string(ctx.Requirements.Network),
"scheme": ctx.Requirements.Scheme,
})
return nil
})Aborting Payments:
client.OnBeforePaymentCreation(func(ctx PaymentCreationContext) (*BeforePaymentCreationResult, error) {
// Don't pay for resources over a certain price
if exceedsLimit(ctx.Requirements.Amount) {
return &BeforePaymentCreationResult{
Abort: true,
Reason: "Price exceeds user limit",
}, nil
}
return nil, nil
})Advanced Patterns
Multi-Network Client
Support multiple blockchains with different signers:
evmSigner, _ := evmsigners.NewClientSignerFromPrivateKey(evmKey)
svmSigner, _ := svmsigners.NewClientSignerFromPrivateKey(svmKey)
client := t402.Newt402Client().
Register("eip155:*", evm.NewExactEvmScheme(evmSigner)).
Register("solana:*", svm.NewExactEvmScheme(svmSigner))Custom HTTP Transport
Add retry logic, timeouts, or other custom behavior:
// Custom transport with retry logic
customTransport := &RetryTransport{
Transport: http.DefaultTransport,
MaxRetries: 3,
}
// Wrap with payment handling
httpClient := t402http.WrapHTTPClientWithPayment(
&http.Client{Transport: customTransport},
t402http.Newt402HTTPClient(client),
)Concurrent Requests
Make multiple paid requests in parallel:
urls := []string{
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, _ := httpClient.Get(u)
defer resp.Body.Close()
// Process response
}(url)
}
wg.Wait()API Reference
t402.X402Client
Constructor:
func Newt402Client() *X402ClientRegistration Methods:
func (c *X402Client) Register(network Network, scheme SchemeNetworkClient) *X402ClientHook Methods:
func (c *X402Client) OnBeforePaymentCreation(hook BeforePaymentCreationHook) *X402Client
func (c *X402Client) OnAfterPaymentCreation(hook AfterPaymentCreationHook) *X402Client
func (c *X402Client) OnPaymentCreationFailure(hook PaymentCreationFailureHook) *X402ClientPayment Methods:
func (c *X402Client) CreatePaymentPayload(ctx context.Context, requirements PaymentRequirements, resource *ResourceInfo, extensions map[string]interface{}) (PaymentPayload, error)
func (c *X402Client) SelectPaymentRequirements(accepts []PaymentRequirements) (PaymentRequirements, error)t402http.HTTPClient
Constructor:
func Newt402HTTPClient(client *X402Client) *t402HTTPClientWrapper:
func WrapHTTPClientWithPayment(client *http.Client, t402Client *t402HTTPClient) *http.ClientConvenience Methods:
func (c *t402HTTPClient) GetWithPayment(ctx context.Context, url string) (*http.Response, error)
func (c *t402HTTPClient) PostWithPayment(ctx context.Context, url string, body io.Reader) (*http.Response, error)
func (c *t402HTTPClient) DoWithPayment(ctx context.Context, req *http.Request) (*http.Response, error)Error Handling
Common Errors
No Registered Mechanism:
// Error: "no client registered for network eip155:1 and scheme exact"
// Solution: Register the mechanism
client.Register("eip155:1", evm.NewExactEvmScheme(signer))Invalid Signer:
// Error: "invalid private key"
// Solution: Check private key format (0x-prefixed hex for EVM, base58 for SVM)
signer, err := evmsigners.NewClientSignerFromPrivateKey(key)
if err != nil {
log.Fatalf("Invalid key: %v", err)
}Payment Retry Limit:
// Error: "payment retry limit exceeded"
// Cause: Server rejected payment twice
// Check: Server logs for rejection reasonError Recovery
Use hooks to implement custom error recovery:
client.OnPaymentCreationFailure(func(ctx PaymentCreationFailureContext) (*PaymentCreationFailureResult, error) {
// Log error
log.Printf("Payment failed: %v", ctx.Error)
// Could implement fallback logic here
// e.g., try different network, notify user, etc.
return nil, nil // Let it fail
})Best Practices
1. Use Signer Helpers
Do use the signer helpers:
signer, _ := evmsigners.NewClientSignerFromPrivateKey(os.Getenv("PRIVATE_KEY"))2. Register Wildcards
Use wildcards for simplicity:
client.Register("eip155:*", evm.NewExactEvmScheme(signer))3. Reuse HTTP Clients
Reuse wrapped clients:
// Create once
httpClient := t402http.WrapHTTPClientWithPayment(http.DefaultClient, t402http.Newt402HTTPClient(client))
// Reuse many times
resp1, _ := httpClient.Get(url1)
resp2, _ := httpClient.Get(url2)4. Handle Contexts Properly
Propagate contexts with timeouts:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, _ := httpClient.Do(req)5. Secure Private Keys
Load from environment or secure vaults:
privateKey := os.Getenv("EVM_PRIVATE_KEY")
signer, _ := evmsigners.NewClientSignerFromPrivateKey(privateKey)Performance Considerations
Connection Pooling
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
httpClient := t402http.WrapHTTPClientWithPayment(
&http.Client{Transport: transport},
t402http.Newt402HTTPClient(client),
)Concurrent Requests
The wrapped HTTP client is safe for concurrent use:
// Single client, multiple goroutines
for _, url := range urls {
go func(u string) {
resp, _ := httpClient.Get(u) // Safe!
defer resp.Body.Close()
}(url)
}Payment Caching
Payment payloads are created fresh for each 402 response. They are not cached because:
- Each payment should be unique (nonce, timestamp)
- Prevents replay attacks
- Ensures fresh blockchain state
Testing
Unit Tests
import (
"testing"
t402 "github.com/t402-io/t402/sdks/go"
evm "github.com/t402-io/t402/sdks/go/mechanisms/evm/exact/client"
)
func TestClientRegistration(t *testing.T) {
signer, _ := evmsigners.NewClientSignerFromPrivateKey(testKey)
client := t402.Newt402Client().
Register("eip155:*", evm.NewExactEvmScheme(signer))
// Test client behavior
}Troubleshooting
Payment Not Created
Problem: Client makes request but doesn’t create payment on 402
Check:
- Is mechanism registered?
client.Register("eip155:*", ...) - Does network match? Ensure server’s network has a registered mechanism
- Are there errors? Add hooks to log payment creation attempts
Wrong Network Selected
Problem: Client uses wrong network for payment
Solution: The server determines the network. Client must have that network registered:
// If server requires eip155:84532, client needs:
client.Register("eip155:84532", evm.NewExactEvmScheme(signer))
// OR
client.Register("eip155:*", evm.NewExactEvmScheme(signer))Signature Verification Fails
Problem: Server/facilitator rejects payment signature
Check:
- Correct private key format
- Signer address matches payment requirements
- Clock synchronization (for time-based signatures)
Migration from V1
If you’re migrating from t402 v1:
Client Changes
V1:
client.RegisterV1("base-sepolia", evmv1.NewExactEvmSchemeV1(signer))V2:
client.Register("eip155:84532", evm.NewExactEvmScheme(signer))Network Identifiers
- V1:
"base-sepolia","ethereum","solana-devnet" - V2:
"eip155:84532","eip155:1","solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
Backward Compatibility
V2 clients can support both protocols:
client.
Register("eip155:*", evm.NewExactEvmScheme(signer)). // V2
RegisterV1("base-sepolia", evmv1.NewExactEvmSchemeV1(signer)) // V1 fallbackRelated Documentation
- Overview - Package overview
- Server SDK - Building servers
- Facilitator - Building facilitators