SDKsGoClient SDK

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:

  1. Detects when a resource requires payment (402 response)
  2. Creates a payment payload using registered mechanisms
  3. Retries the request with payment headers
  4. Receives the protected resource

Quick Start

Installation

go get github.com/t402-io/t402/sdks/go

Basic 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.

// 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 mainnet

4. 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:

  1. Intercepts all HTTP requests
  2. Detects 402 Payment Required responses
  3. Extracts payment requirements from headers/body
  4. Creates payment using registered mechanisms
  5. Retries request with payment signature
  6. 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() *X402Client

Registration Methods:

func (c *X402Client) Register(network Network, scheme SchemeNetworkClient) *X402Client

Hook Methods:

func (c *X402Client) OnBeforePaymentCreation(hook BeforePaymentCreationHook) *X402Client
func (c *X402Client) OnAfterPaymentCreation(hook AfterPaymentCreationHook) *X402Client
func (c *X402Client) OnPaymentCreationFailure(hook PaymentCreationFailureHook) *X402Client

Payment 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) *t402HTTPClient

Wrapper:

func WrapHTTPClientWithPayment(client *http.Client, t402Client *t402HTTPClient) *http.Client

Convenience 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 reason

Error 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 fallback