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/x402payQuick 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 tokensHub-Specific Extensions
In addition to standard x402 protocol endpoints, HubClient provides:
SettleWithOptions() - Enhanced settle with
confirmanduseDbIdparametersTransfer() - 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 clearedTransaction 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 retryPagination
// 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
HubClientinstance
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
63% less code - 70 lines → 26 lines for auth
Type safety -
map[string]interface{}→*TransactionHistoryItemAutomatic token management - No manual extraction or storage
Better error handling - Structured errors with context
Auto token refresh - Use
GetHistoryWithAutoRefresh()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
Reuse client instances - Create once, use throughout your application
Handle token refresh - Use
GetHistoryWithAutoRefresh()or implement refresh logicStore tokens securely - Persist tokens in secure storage between sessions
Validate parameters - Check limits and offsets before making requests
Handle errors properly - Check for
HTTPErrortype for detailed error infoUse in tests - Replace manual HTTP calls with hubclient for cleaner, type-safe tests
Related Documentation
Source Code
GitHub: sigweihq/x402pay Package: github.com/sigweihq/x402pay/pkg/hubclient
Last updated