HubClient SDK (Go)

The HubClient SDK is a Go client library for interacting with x402-hub. It provides type-safe access to protocol endpoints, authentication, and transaction history.

Installation

go get github.com/sigweihq/x402pay

Quick Start

import (
    "fmt"
    "github.com/sigweihq/x402pay/pkg/hubclient"
    x402paytypes "github.com/sigweihq/x402pay/pkg/types"
)

func main() {
    // Create client (uses https://hub.sigwei.com by default)
    client := hubclient.NewHubClient(nil)

    // Get authentication message
    msg, err := client.Auth.GetAuthMessage("0xYourWalletAddress")
    if err != nil {
        panic(err)
    }

    // Sign message with wallet (user does this)
    signature := signWithWallet(msg.Message)

    // Login
    auth, err := client.Auth.Login(msg.Message, signature)
    if err != nil {
        panic(err)
    }

    fmt.Println("Logged in:", auth.User.WalletAddress)

    // Get transaction history
    history, err := client.History.GetHistory(&x402paytypes.HistoryParams{
        Network: "base",
        Limit:   50,
    })

    for _, tx := range history.Transactions {
        fmt.Printf("Transaction: %s (%s)\n", tx.Status, tx.Network)
    }
}

Architecture

HubClient is organized into three main components:

HubClient
├─ FacilitatorClient (embedded)  → Standard x402 protocol
│  ├─ Verify()                   → Verify payment
│  └─ Settle()                   → Settle payment
├─ Auth                          → Wallet authentication
│  ├─ GetAuthMessage()           → Get SIWE message
│  ├─ Login()                    → Login with signature
│  ├─ RefreshToken()             → Refresh access token
│  ├─ GetMe()                    → Get current user
│  └─ Logout()                   → Logout
└─ History                       → Transaction history
   ├─ GetHistory()               → Query transactions
   └─ GetHistoryWithAutoRefresh() → Auto-refresh tokens

Hub-Specific Extensions

In addition to standard x402 protocol endpoints, HubClient provides:

  • SettleWithOptions() - Enhanced settle with confirm and useDbId parameters

  • Transfer() - Convenient endpoint for EVM transfers

  • Supported() - Query supported networks and schemes

Authentication

Basic Authentication Flow

// 1. Get auth message
msgResp, err := client.Auth.GetAuthMessage("0x1234...")
if err != nil {
    log.Fatal(err)
}

// 2. Sign message with wallet
signature := "0x..." // From wallet software

// 3. Login
auth, err := client.Auth.Login(msgResp.Message, signature)
if err != nil {
    log.Fatal(err)
}

// Tokens are automatically stored in client.Auth
fmt.Println("Access Token:", auth.AccessToken)
fmt.Println("User:", auth.User.WalletAddress)

Check Authentication Status

if client.Auth.IsAuthenticated() {
    fmt.Println("User is authenticated")
}

Get Current User

user, err := client.Auth.GetMe()
if err != nil {
    log.Fatal(err)
}
fmt.Println("Current user:", user.WalletAddress)

Token Refresh

// When access token expires
newTokens, err := client.Auth.RefreshToken()
if err != nil {
    log.Fatal(err)
}

Manual Token Management

// Set tokens manually (e.g., from storage)
client.Auth.SetTokens("access-token", "refresh-token")

// Get tokens (e.g., to persist)
accessToken := client.Auth.GetAccessToken()
refreshToken := client.Auth.GetRefreshToken()

// Clear tokens
client.Auth.ClearTokens()

Logout

if err := client.Auth.Logout(); err != nil {
    log.Fatal(err)
}
// Tokens are cleared

Transaction History

Basic Query

history, err := client.History.GetHistory(&x402paytypes.HistoryParams{
    Network: "base",    // Optional: filter by network
    Limit:   50,        // 1-100, defaults to 50
    Offset:  0,         // For pagination
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Total: %d, Returned: %d\n",
    history.Pagination.TotalCount,
    len(history.History))

for _, tx := range history.History {
    fmt.Printf("Transaction %d: %s (%s)\n",
        tx.ID, tx.Status, tx.Network)
    if tx.TransactionHash != nil {
        fmt.Printf("  Hash: %s\n", *tx.TransactionHash)
    }
    fmt.Printf("  Amount: %s wei\n", tx.Amount)
}

Auto-Refresh

Use GetHistoryWithAutoRefresh() to automatically refresh expired tokens:

history, err := client.History.GetHistoryWithAutoRefresh(&x402paytypes.HistoryParams{
    Limit: 50,
})
// If token is expired, it will automatically refresh and retry

Pagination

// Page 1
page1, err := client.History.GetHistory(&x402paytypes.HistoryParams{
    Limit:  20,
    Offset: 0,
})

// Page 2
page2, err := client.History.GetHistory(&x402paytypes.HistoryParams{
    Limit:  20,
    Offset: 20,
})

x402 Protocol Operations

Check Supported Networks

supported, err := client.Supported()
if err != nil {
    log.Fatal(err)
}

for _, kind := range supported.Kinds {
    fmt.Printf("%s on %s\n", kind.Scheme, kind.Network)
}

Verify Payment

resp, err := client.Verify(paymentPayload, paymentRequirements)
if err != nil {
    log.Fatal(err)
}

if resp.IsValid {
    fmt.Println("Payment is valid")
} else {
    fmt.Println("Invalid:", resp.InvalidReason)
}

Settle Payment (Standard)

// Standard settle (from FacilitatorClient)
resp, err := client.Settle(paymentPayload, paymentRequirements)
if err != nil {
    log.Fatal(err)
}

fmt.Println("Transaction:", resp.Transaction)

Settle Payment with Options

// Hub-enhanced settle with options
resp, err := client.SettleWithOptions(
    paymentPayload,
    paymentRequirements,
    true,  // confirm: enable on-chain verification
    false, // useDbId: return DB ID instead of tx hash
)

Transfer (Hub-Specific)

import "github.com/coinbase/x402/go/pkg/types"

payload := &types.ExactEvmPayload{
    Signature: "0x...",
    Authorization: types.Authorization{
        From:        "0xSender",
        To:          "0xRecipient",
        Value:       "1000000", // 1 USDC in wei (6 decimals)
        ValidAfter:  "0",
        ValidBefore: "9999999999",
        Nonce:       "0x...",
    },
}

resp, err := client.Transfer(
    payload,
    "base",                                    // network
    "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC contract
    true,                                      // confirm
)

Configuration

Default Configuration

// Uses default hub URL: https://hub.sigwei.com
client := hubclient.NewHubClient(nil)

Custom Configuration

import (
    "time"
    "github.com/coinbase/x402/go/pkg/types"
)

config := &types.FacilitatorConfig{
    URL: "https://custom-hub.example.com",
    Timeout: func() time.Duration {
        return 30 * time.Second
    },
}

client := hubclient.NewHubClient(config)

Error Handling

HTTP Errors

_, err := client.Auth.Login("invalid", "invalid")
if err != nil {
    if httpErr, ok := err.(*hubclient.HTTPError); ok {
        fmt.Printf("HTTP %d: %s\n", httpErr.StatusCode, httpErr.Error())

        if httpErr.IsUnauthorized() {
            fmt.Println("Authentication failed")
        }
    }
}

Common Errors

  • 401 Unauthorized: Invalid credentials or expired token

    • Solution: Refresh token or re-authenticate

  • 403 Forbidden: Insufficient permissions

  • 400 Bad Request: Invalid parameters

  • 500 Internal Server Error: Server error

Type Reference

Auth Types

type MessageResponse struct {
    Message    string `json:"message"`
    APIVersion string `json:"apiVersion"`
    Timestamp  string `json:"timestamp"`
}

type AuthResponse struct {
    User         *User  `json:"user"`
    AccessToken  string `json:"accessToken"`
    RefreshToken string `json:"refreshToken"`
    APIVersion   string `json:"apiVersion"`
    Timestamp    string `json:"timestamp"`
}

type User struct {
    ID            uint64    `json:"id"`
    WalletAddress string    `json:"walletAddress"`
    CreatedAt     time.Time `json:"createdAt"`
    UpdatedAt     time.Time `json:"updatedAt"`
}

type TokenPair struct {
    AccessToken  string `json:"accessToken"`
    RefreshToken string `json:"refreshToken"`
    APIVersion   string `json:"apiVersion"`
    Timestamp    string `json:"timestamp"`
}

History Types

type HistoryParams struct {
    Network string `json:"network,omitempty"` // Optional: "base" or "base-sepolia"
    Limit   int    `json:"limit"`             // 1-100
    Offset  int    `json:"offset"`            // For pagination
}

type HistoryResponse struct {
    Success    bool                       `json:"success"`
    History    []*TransactionHistoryItem  `json:"history"`
    Pagination *PaginationInfo            `json:"pagination"`
    APIVersion string                     `json:"apiVersion"`
    Timestamp  string                     `json:"timestamp"`
}

type TransactionHistoryItem struct {
    ID              int64            `json:"id"`
    CreatedAt       time.Time        `json:"createdAt"`
    UpdatedAt       time.Time        `json:"updatedAt"`
    SignerAddress   string           `json:"signerAddress"`
    Amount          string           `json:"amount"` // Wei units
    Network         string           `json:"network"`
    ChainID         int              `json:"chainId"`
    TransactionHash *string          `json:"transactionHash,omitempty"`
    Status          string           `json:"status"`
    Error           *string          `json:"error,omitempty"`
    Type            string           `json:"type"`
    X402Data        *X402DataHistory `json:"x402Data,omitempty"`
    PurchaseData    *PurchaseData    `json:"purchaseData,omitempty"`
}

type PaginationInfo struct {
    TotalCount int  `json:"totalCount"`
    Limit      int  `json:"limit"`
    Offset     int  `json:"offset"`
    HasNext    bool `json:"hasNext"`
    HasPrev    bool `json:"hasPrev"`
}

Thread Safety

  • Auth token management is thread-safe (uses sync.RWMutex)

  • HTTP client is thread-safe (from net/http)

  • Multiple goroutines can safely share a single HubClient instance

Testing with HubClient

HubClient significantly simplifies E2E and integration tests by eliminating manual HTTP calls and JSON parsing.

Before: Manual HTTP Calls (~70 lines)

func authenticateUser(account *TestAccount) (string, error) {
    // Manual HTTP call to /api/v1/auth/message
    client := &http.Client{Timeout: 10 * time.Second}
    messageUrl := fmt.Sprintf("%s/api/v1/auth/message?walletAddress=%s",
        baseURL, account.Address)
    resp, err := client.Get(messageUrl)
    // ... JSON parsing
    var messageResponse map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&messageResponse)
    message := messageResponse["message"].(string)

    // Sign message
    signature, _ := account.SignPersonalMessage(t, message)

    // Manual HTTP call to /api/v1/auth/login
    authBody, _ := json.Marshal(map[string]interface{}{
        "message": message,
        "signature": signature,
    })
    authResp, _ := client.Post(
        fmt.Sprintf("%s/api/v1/auth/login", baseURL), ...)
    // ... more JSON parsing
    // ~70 lines total
}

After: Using HubClient (~15 lines)

func authenticateUser(account *TestAccount) (*hubclient.HubClient, error) {
    hubClient := hubclient.NewHubClient(&types.FacilitatorConfig{
        URL: baseURL,
    })

    msgResp, _ := hubClient.Auth.GetAuthMessage(account.Address)
    signature, _ := account.SignPersonalMessage(t, msgResp.Message)
    _, err := hubClient.Auth.Login(msgResp.Message, signature)
    // Tokens auto-stored in hubClient.Auth
    return hubClient, err
}

Benefits in Tests

  1. 63% less code - 70 lines → 26 lines for auth

  2. Type safety - map[string]interface{}*TransactionHistoryItem

  3. Automatic token management - No manual extraction or storage

  4. Better error handling - Structured errors with context

  5. Auto token refresh - Use GetHistoryWithAutoRefresh()

  6. Single source of truth - All auth/history logic in hubclient

Complete Examples

See the HubClient README for comprehensive examples including:

  • Full authentication workflow

  • Transaction history queries

  • Error handling

  • Token refresh

  • Protocol endpoints

Best Practices

  1. Reuse client instances - Create once, use throughout your application

  2. Handle token refresh - Use GetHistoryWithAutoRefresh() or implement refresh logic

  3. Store tokens securely - Persist tokens in secure storage between sessions

  4. Validate parameters - Check limits and offsets before making requests

  5. Handle errors properly - Check for HTTPError type for detailed error info

  6. Use in tests - Replace manual HTTP calls with hubclient for cleaner, type-safe tests

Source Code

GitHub: sigweihq/x402pay Package: github.com/sigweihq/x402pay/pkg/hubclient

Last updated