@solana-payment-channel/client
v0.2.2
Published
Client SDK for automatic payment channel management with x402 protocol on Solana
Downloads
9
Maintainers
Readme
@solana-payment-channel/client
Drop-in replacement for
fetch()with automatic payment channel management
A production-ready client SDK that seamlessly handles payment channels and falls back to x402 protocol. Get started in 5 minutes with zero configuration - the client automatically optimizes between instant, free channel payments and on-chain x402 payments based on usage patterns.
Features
- 🔄 Drop-in Replacement: Works exactly like
fetch()but handles payments automatically - 💳 Overdraft/Credit Limit: Set customizable credit limits (up to any amount) for users
- 🔄 Auto-Settlement: Automatically pays off debt when funds are added
- 🧠 Intelligent Routing: Automatically chooses between channel and x402 based on usage patterns
- ⚡️ Zero Configuration: Works out-of-the-box with sensible defaults
- 🎛️ Full Control: Advanced users can manage channels manually
- 📊 Event System: Monitor payments, channel lifecycle, and errors
- 🔒 Type Safety: Full TypeScript support with comprehensive types
- 🚨 Error Handling: Graceful fallbacks and clear error messages
- ⚡️ High Performance: Request caching, capability caching, efficient payment routing
- 🌐 Universal: Works in Node.js and browsers (with wallet adapter)
Installation
npm install @solana-payment-channel/client @bober3r/solana-payment-channels-core @solana/web3.js
# or
yarn add @solana-payment-channel/client @bober3r/solana-payment-channels-core @solana/web3.js
# or
pnpm add @solana-payment-channel/client @bober3r/solana-payment-channels-core @solana/web3.jsQuick Start (5 Minutes)
1. Initialize the Client
import { createClient } from '@solana-payment-channel/client';
import { Keypair } from '@solana/web3.js';
// Load your wallet (from env, file, or generate)
const wallet = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(process.env.WALLET_SECRET_KEY))
);
// Create client with minimal configuration
const client = createClient({
wallet,
rpcUrl: 'https://api.devnet.solana.com',
network: 'devnet',
});2. Use Like Regular fetch()
// That's it! Use client.fetch() instead of fetch()
const response = await client.fetch('https://api.example.com/premium');
const data = await response.json();
// The client automatically:
// 1. Detects 402 response
// 2. Checks if server supports channels
// 3. Opens channel if beneficial (high-frequency use)
// 4. Uses channel payment (instant, free)
// 5. Falls back to x402 for single payments
// 6. Retries request with payment3. That's It!
Your application now supports automatic payment channels with zero additional code. The client tracks usage patterns and optimizes payment routing for you.
Basic Usage Examples
Simple API Calls
import { createClient } from '@solana-payment-channel/client';
const client = createClient({ wallet, rpcUrl, network: 'devnet' });
// Single request
const data = await client.fetch('https://api.example.com/data').then(r => r.json());
// Multiple requests (client automatically optimizes)
for (let i = 0; i < 100; i++) {
const result = await client.fetch('https://api.example.com/query');
console.log(await result.json());
}POST Requests with Body
const response = await client.fetch('https://api.example.com/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'solana', limit: 10 }),
});
const results = await response.json();Error Handling
try {
const response = await client.fetch('https://api.example.com/data');
if (!response.ok) {
console.error(`HTTP error: ${response.status}`);
return;
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Request failed:', error);
}Configuration Options
Basic Configuration
import { createClient } from '@solana-payment-channel/client';
const client = createClient({
// Required
wallet: myKeypair, // Your Solana keypair
rpcUrl: 'https://api.devnet.solana.com',
network: 'devnet', // 'devnet' | 'mainnet-beta'
// Optional - Channel Management
channelThreshold: 10, // Open channel after N req/hour (default: 10)
defaultChannelDeposit: BigInt(10_000_000), // 10 USDC (default)
defaultCreditLimit: BigInt(0), // Credit limit - 0 = no overdraft (default)
autoRefillThreshold: BigInt(1_000_000), // Refill at 1 USDC (default)
autoRefillAmount: BigInt(10_000_000), // Refill with 10 USDC (default)
channelExpiry: 7 * 24 * 60 * 60, // 7 days (default)
// Optional - Behavior
autoManageChannels: true, // Enable auto-management (default: true)
trackRequests: true, // Track for optimization (default: true)
debug: false, // Enable debug logging (default: false)
// Optional - Performance
capabilitiesCacheTTL: 300000, // Cache TTL 5 min (default)
requestTimeout: 30000, // Request timeout 30s (default)
});Advanced Configuration
import { createClient } from '@solana-payment-channel/client';
import { PublicKey } from '@solana/web3.js';
const client = createClient({
wallet: myKeypair,
rpcUrl: 'https://api.mainnet-beta.solana.com',
network: 'mainnet-beta',
// Custom program ID (if not using default)
programId: new PublicKey('YourProgramId...'),
// Custom USDC mint (if needed)
usdcMint: new PublicKey('CustomUSDCMint...'),
// Aggressive channel opening (lower threshold)
channelThreshold: 5,
// Larger deposits for high-volume usage
defaultChannelDeposit: BigInt(100_000_000), // 100 USDC
// More aggressive refilling
autoRefillThreshold: BigInt(10_000_000), // 10 USDC
autoRefillAmount: BigInt(50_000_000), // 50 USDC
// Longer channel lifetime
channelExpiry: 30 * 24 * 60 * 60, // 30 days
// Enable debug mode
debug: true,
});Manual Channel Management
For advanced use cases, you can manage channels manually:
Opening Channels
// Open a channel for a specific server
const channelId = await client.openChannel(
'https://api.example.com',
BigInt(10_000_000), // 10 USDC deposit
BigInt(5_000_000), // 5 USDC credit limit (optional)
);
console.log(`Channel opened: ${channelId}`);
console.log('Total spendable: 15 USDC (10 deposit + 5 credit)');
// Now all requests to this server use the channel
const response = await client.fetch('https://api.example.com/data');Checking Channel Balance
const balance = await client.getChannelBalance(channelId);
console.log(`Remaining balance: ${balance}`);
if (balance < BigInt(1_000_000)) {
console.log('Balance is low!');
}Refilling Channels
// Manual refill
await client.autoRefillChannel(channelId);
console.log('Channel refilled');
// Refill with custom amount
await client.autoRefillChannel(channelId, BigInt(20_000_000)); // 20 USDCClosing Channels
// Close channel and get refund
await client.closeChannel(channelId);
console.log('Channel closed, remaining balance refunded');Getting Channel Information
const info = client.getChannelInfo(channelId);
console.log(`Server: ${info.serverUrl}`);
console.log(`Balance: ${info.currentBalance}`);
console.log(`Payments made: ${info.paymentCount}`);
console.log(`Expires: ${info.expiry}`);Listing All Channels
const channels = client.getAllChannels();
console.log(`Active channels: ${channels.length}`);
for (const channel of channels) {
console.log(`${channel.serverUrl}: ${channel.currentBalance} remaining`);
}Event Monitoring
Monitor payments, channels, and errors in real-time:
Channel Lifecycle Events
// Channel opened
client.on('channel_opened', ({ channelId, serverUrl, deposit, expiry }) => {
console.log(`✅ Opened channel ${channelId}`);
console.log(` Server: ${serverUrl}`);
console.log(` Deposit: ${deposit}`);
console.log(` Expires: ${expiry}`);
});
// Channel closed
client.on('channel_closed', ({ channelId, serverUrl, refundedAmount }) => {
console.log(`🔒 Closed channel ${channelId}`);
console.log(` Refunded: ${refundedAmount}`);
});
// Channel refilled
client.on('channel_refilled', ({ channelId, addedAmount, newBalance }) => {
console.log(`💰 Refilled channel ${channelId}`);
console.log(` Added: ${addedAmount}`);
console.log(` New balance: ${newBalance}`);
});
// Channel depleted (low balance)
client.on('channel_depleted', ({ channelId, remainingBalance, threshold }) => {
console.log(`⚠️ Channel ${channelId} low on funds`);
console.log(` Remaining: ${remainingBalance}`);
console.log(` Threshold: ${threshold}`);
});Payment Events
// Payment required (402 detected)
client.on('payment_required', ({ serverUrl, amount, requirement }) => {
console.log(`💳 Payment required for ${serverUrl}`);
console.log(` Amount: ${amount} ${requirement.currency}`);
});
// Payment made
client.on('payment_made', ({ method, amount, serverUrl, signature }) => {
console.log(`✅ Paid ${amount} to ${serverUrl}`);
console.log(` Method: ${method}`);
console.log(` Signature: ${signature}`);
});
// Payment failed
client.on('payment_failed', ({ method, serverUrl, error, amount }) => {
console.error(`❌ Payment failed to ${serverUrl}`);
console.error(` Method: ${method}`);
console.error(` Error: ${error}`);
});Capability Detection Events
// Server capabilities detected
client.on('capabilities_detected', ({ serverUrl, capabilities }) => {
console.log(`🔍 Detected capabilities for ${serverUrl}`);
console.log(` Supports channels: ${capabilities.supportsChannels}`);
console.log(` Supports x402: ${capabilities.supportsX402}`);
if (capabilities.supportsChannels) {
console.log(` Min deposit: ${capabilities.minChannelAmount}`);
}
});Complete Event Monitoring Example
import { createClient } from '@solana-payment-channel/client';
const client = createClient({ wallet, rpcUrl, network: 'devnet' });
// Set up all event listeners
client.on('channel_opened', (data) => {
console.log('Channel opened:', data);
});
client.on('payment_made', (data) => {
console.log('Payment made:', data);
});
client.on('channel_depleted', async (data) => {
console.warn('Channel low on funds:', data);
// Auto-refill
await client.autoRefillChannel(data.channelId);
});
client.on('payment_failed', (data) => {
console.error('Payment failed:', data);
// Handle failure (e.g., retry, notify user)
});
// Now make requests with full observability
await client.fetch('https://api.example.com/data');Analytics & Monitoring
Track usage patterns and costs:
Basic Analytics
const analytics = client.getAnalytics();
console.log('=== Usage Statistics ===');
console.log(`Total requests: ${analytics.totalRequests}`);
console.log(`Total payments: ${analytics.totalPayments}`);
console.log(`Total spent: ${analytics.totalSpent} lamports`);
console.log(`Active channels: ${analytics.activeChannels}`);
console.log(`Success rate: ${(analytics.successRate * 100).toFixed(2)}%`);
console.log('\n=== Payment Methods ===');
console.log(`Channel payments: ${analytics.channelPayments}`);
console.log(`x402 payments: ${analytics.x402Payments}`);
console.log(`Total savings: ${analytics.totalSavings} lamports`);Per-Domain Statistics
const analytics = client.getAnalytics();
console.log('\n=== Per-Domain Statistics ===');
for (const [domain, stats] of analytics.domainStats) {
console.log(`\n${domain}:`);
console.log(` Total requests: ${stats.totalRequests}`);
console.log(` Paid requests: ${stats.paidRequests}`);
console.log(` Total paid: ${stats.totalPaid}`);
console.log(` Requests/hour: ${stats.requestsPerHour.toFixed(2)}`);
console.log(` Has channel: ${stats.hasActiveChannel}`);
if (stats.channelId) {
const channel = client.getChannelInfo(stats.channelId);
console.log(` Channel balance: ${channel?.currentBalance}`);
}
}Cost Analysis
import { AutoPaymentManager } from '@solana-payment-channel/client';
// Get the auto-payment manager for advanced analytics
const manager = new AutoPaymentManager();
// Track some requests...
manager.trackRequest('https://api.example.com/data', {
paymentRequired: true,
amount: BigInt(100_000),
method: 'x402',
statusCode: 200,
responseTime: 150,
});
// Analyze costs
const analysis = manager.analyzeCosts('https://api.example.com');
console.log('=== Cost Analysis ===');
console.log(`Total requests: ${analysis.totalRequests}`);
console.log(`Channel setup cost: ${analysis.channelSetupCost} lamports`);
console.log(`x402 per-payment cost: ${analysis.x402PaymentCost} lamports`);
console.log(`Total x402 cost: ${analysis.totalX402Cost} lamports`);
console.log(`Total channel cost: ${analysis.totalChannelCost} lamports`);
console.log(`Estimated savings: ${analysis.estimatedSavings} lamports`);
console.log(`Break-even at: ${analysis.breakEvenRequests} requests`);
console.log(`Recommendation: ${analysis.recommendation}`);Payment Flow Diagrams
Automatic Payment Flow
1. Request is made
↓
2. Is 402 response?
↓ Yes
3. Check server capabilities
↓
4. Supports channels?
├─ Yes → Check request frequency
│ ↓
│ High frequency (>10 req/hr)?
│ ├─ Yes → Open channel → Use channel payment (free, instant)
│ └─ No → Use x402 (on-chain payment)
│
└─ No → Use x402 (on-chain payment)
↓
5. Retry request with payment
↓
6. Return successful responseChannel Payment Flow
1. Create payment authorization
↓
2. Sign with client wallet (off-chain)
↓
3. Attach authorization to request headers
↓
4. Server verifies signature (instant)
↓
5. Server serves request (no blockchain interaction)
↓
6. Server claims payment later (batched on-chain)x402 Payment Flow
1. Parse payment requirement from 402 response
↓
2. Create Solana transaction
↓
3. Sign and send transaction
↓
4. Wait for confirmation
↓
5. Attach transaction signature to request
↓
6. Server verifies on-chain
↓
7. Server serves requestBest Practices
1. Use Auto-Management for Most Cases
// ✅ Good: Let the client manage everything
const client = createClient({
wallet,
rpcUrl,
network: 'devnet',
autoManageChannels: true, // Default
});
await client.fetch(url); // Automatic optimization2. Pre-Open Channels for Known High-Frequency APIs
// ✅ Good: Open channel upfront for known usage
const client = createClient({ wallet, rpcUrl, network: 'devnet' });
// If you know you'll make many requests
const channelId = await client.openChannel(
'https://api.example.com',
BigInt(50_000_000) // 50 USDC for high volume
);
// Now make requests (all use channel)
for (let i = 0; i < 10000; i++) {
await client.fetch('https://api.example.com/data');
}3. Monitor Low Balances
// ✅ Good: Monitor and handle low balances
client.on('channel_depleted', async ({ channelId, remainingBalance }) => {
console.warn(`Channel ${channelId} low: ${remainingBalance}`);
// Automatic refill
await client.autoRefillChannel(channelId);
// Or close and open new one
// await client.closeChannel(channelId);
// await client.openChannel(serverUrl, BigInt(20_000_000));
});4. Handle Errors Gracefully
// ✅ Good: Comprehensive error handling
try {
const response = await client.fetch(url);
if (response.status === 402) {
// Payment required but failed
console.error('Payment failed or insufficient funds');
return;
}
if (!response.ok) {
console.error(`HTTP error: ${response.status}`);
return;
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request timed out');
} else if (error instanceof TypeError) {
console.error('Network error');
} else {
console.error('Request failed:', error);
}
}5. Clean Up Channels When Done
// ✅ Good: Close channels to recover funds
const channels = client.getAllChannels();
for (const channel of channels) {
// Close expired or unused channels
if (channel.expiry < new Date() || channel.paymentCount === 0) {
await client.closeChannel(channel.channelId);
console.log(`Closed channel ${channel.channelId}, refunded ${channel.currentBalance}`);
}
}6. Use Debug Mode During Development
// ✅ Good: Enable debug logging during development
const client = createClient({
wallet,
rpcUrl,
network: 'devnet',
debug: true, // Shows detailed logs
});
// Debug logs will show:
// - Server capability detection
// - Payment decisions
// - Channel operations
// - Request tracking7. Configure Based on Usage Patterns
// ✅ Good: Tune configuration to your use case
// High-frequency API calls (>50 req/hr)
const highFrequencyClient = createClient({
wallet, rpcUrl, network: 'devnet',
channelThreshold: 5, // Lower threshold
defaultChannelDeposit: BigInt(100_000_000), // 100 USDC
autoRefillThreshold: BigInt(20_000_000), // 20 USDC
channelExpiry: 30 * 24 * 60 * 60, // 30 days
});
// Occasional API calls (<5 req/hr)
const lowFrequencyClient = createClient({
wallet, rpcUrl, network: 'devnet',
channelThreshold: 20, // Higher threshold
defaultChannelDeposit: BigInt(5_000_000), // 5 USDC
autoManageChannels: false, // Manual management
});Troubleshooting
Payment Required (402) Not Being Handled
Problem: Requests return 402 but payment isn't automatic.
Solution:
// Check if skipAutoPayment is set
const response = await client.fetch(url, {
skipAutoPayment: false, // Ensure auto-payment is enabled
});
// Check server capabilities
const capabilities = await fetchServerCapabilities(url);
console.log('Supports channels:', capabilities.supportsChannels);
console.log('Supports x402:', capabilities.supportsX402);Channel Not Being Opened
Problem: High-frequency requests still using x402.
Solution:
// Check request frequency
const stats = client.getAnalytics();
for (const [domain, domainStats] of stats.domainStats) {
console.log(`${domain}: ${domainStats.requestsPerHour} req/hr`);
}
// Lower threshold if needed
const client = createClient({
wallet, rpcUrl, network: 'devnet',
channelThreshold: 5, // Lower from default 10
});
// Or open manually
await client.openChannel(url, BigInt(10_000_000));Insufficient Channel Balance
Problem: Channel payments failing due to low balance.
Solution:
// Enable auto-refill
const client = createClient({
wallet, rpcUrl, network: 'devnet',
autoManageChannels: true,
autoRefillThreshold: BigInt(2_000_000), // Refill at 2 USDC
autoRefillAmount: BigInt(10_000_000), // Refill with 10 USDC
});
// Or monitor and refill manually
client.on('channel_depleted', async ({ channelId }) => {
await client.autoRefillChannel(channelId);
});Server Doesn't Support Channels
Problem: Server returns 402 but doesn't support channels.
Solution:
// Client will automatically fall back to x402
// Verify by checking capabilities
const capabilities = await fetchServerCapabilities(url);
if (!capabilities.supportsChannels) {
console.log('Server only supports x402 - will use on-chain payments');
}
// Ensure x402 client is properly configured
// (Integration with @x402-solana/client should be set up)Request Timeouts
Problem: Requests timing out during payment.
Solution:
// Increase timeout
const client = createClient({
wallet, rpcUrl, network: 'devnet',
requestTimeout: 60000, // 60 seconds
});
// Or use custom timeout per request
const response = await client.fetch(url, {
signal: AbortSignal.timeout(90000), // 90 seconds
});API Reference
See TypeScript definitions for complete API documentation.
Main Classes
PaymentChannelClient- Main client for automatic payment handlingAutoPaymentManager- Intelligent payment routing and cost analysis
Key Methods
createClient(config)- Create a new client instanceclient.fetch(url, options)- Drop-in fetch replacementclient.openChannel(serverUrl, deposit)- Open a payment channelclient.closeChannel(channelId)- Close a channelclient.getChannelBalance(channelId)- Check channel balanceclient.autoRefillChannel(channelId)- Refill a channelclient.getAnalytics()- Get usage analytics
Utility Functions
fetchServerCapabilities(url)- Fetch server capabilitiesparsePaymentRequirements(response)- Parse 402 responsecreateChannelPaymentHeaders(...)- Create channel payment headerscreateX402PaymentHeaders(...)- Create x402 payment headers
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
MIT License - see LICENSE for details.
Support
- 📚 Documentation: https://docs.x402.dev
- 💬 Discord: https://discord.gg/x402
- 🐛 Issues: GitHub Issues
- 📧 Email: [email protected]
Related Packages
- @bober3r/solana-payment-channels-core - Core payment channel management
- @bober3r/solana-payment-channels-server - Server SDK for accepting payments
- @x402-solana/client - x402 protocol client
Built with ❤️ by the x402 team
