@solana-payment-channel/server
v0.2.2
Published
Server-side middleware and integrations for x402 payment channels on Solana (Express, NestJS, Fastify)
Maintainers
Readme
@solana-payment-channel/server
Server-side middleware and integrations for x402 payment channels on Solana. Accept instant, free off-chain payments via payment channels with automatic fallback to on-chain x402 protocol.
Features
- Off-chain Payment Channels: Process payments instantly with zero transaction fees
- Overdraft/Credit System: Support customizable credit limits for users (up to any amount)
- Auto-Settlement: Automatically receive debt payments when users add funds
- Automatic x402 Fallback: Seamlessly fall back to on-chain payments when channels unavailable
- Multi-Framework Support: Native integrations for Express, NestJS, and Fastify
- Type-Safe: Built with TypeScript for complete type safety
- Production-Ready: Comprehensive error handling, validation, and event system
- Easy Integration: Simple middleware/guard/plugin patterns for each framework
- Server Capabilities: Automatic discovery endpoint for client configuration
Installation
npm install @x402-channels/server @x402-channels/core @solana/web3.jsQuick Start
Express
import express from 'express';
import { Keypair, PublicKey } from '@solana/web3.js';
import {
ChannelPaymentService,
channelAuthMiddleware
} from '@x402-channels/server/express';
const app = express();
app.use(express.json());
// Initialize payment service
const paymentService = new ChannelPaymentService({
rpcUrl: process.env.SOLANA_RPC_URL!,
network: 'devnet',
programId: new PublicKey(process.env.CHANNEL_PROGRAM_ID!),
usdcMint: new PublicKey(process.env.USDC_MINT!),
recipientWallet: new PublicKey(process.env.RECIPIENT_WALLET!),
serverKeypair: serverKeypair // Optional: for channel claiming
});
// Expose capabilities for client discovery
app.get('/.well-known/x402-capabilities', (req, res) => {
res.json(paymentService.getCapabilities());
});
// Protected endpoint - requires 1 USDC payment
app.get('/api/premium',
channelAuthMiddleware(paymentService, {
amount: BigInt(1_000_000) // 1 USDC
}),
(req, res) => {
res.json({
content: 'Premium content',
payment: req.payment
});
}
);
app.listen(3000);NestJS
import { Module, Controller, Get, UseGuards, Inject } from '@nestjs/common';
import { Keypair, PublicKey } from '@solana/web3.js';
import {
ChannelPaymentService,
ChannelPaymentGuard,
RequirePayment,
Payment,
PaymentResult
} from '@x402-channels/server/nestjs';
// Configure module
@Module({
providers: [
{
provide: 'CHANNEL_PAYMENT_SERVICE',
useFactory: () => {
return new ChannelPaymentService({
rpcUrl: process.env.SOLANA_RPC_URL!,
network: 'devnet',
programId: new PublicKey(process.env.CHANNEL_PROGRAM_ID!),
usdcMint: new PublicKey(process.env.USDC_MINT!),
recipientWallet: serverKeypair.publicKey,
serverKeypair: serverKeypair
});
}
},
ChannelPaymentGuard
],
controllers: [AppController, ApiController]
})
export class AppModule {}
// Public controller
@Controller()
export class AppController {
constructor(
@Inject('CHANNEL_PAYMENT_SERVICE')
private readonly paymentService: ChannelPaymentService
) {}
@Get('.well-known/x402-capabilities')
getCapabilities() {
return this.paymentService.getCapabilities();
}
}
// Protected controller
@Controller('api')
@UseGuards(ChannelPaymentGuard)
export class ApiController {
@Get('premium')
@RequirePayment(1_000_000n) // 1 USDC
getPremiumContent(@Payment() payment: PaymentResult) {
return {
content: 'Premium content',
method: payment.method,
balance: payment.remainingBalance?.toString()
};
}
}Fastify
import Fastify from 'fastify';
import { Keypair, PublicKey } from '@solana/web3.js';
import channelPaymentPlugin from '@x402-channels/server/fastify';
const fastify = Fastify({ logger: true });
// Register payment plugin
await fastify.register(channelPaymentPlugin, {
rpcUrl: process.env.SOLANA_RPC_URL!,
network: 'devnet',
programId: new PublicKey(process.env.CHANNEL_PROGRAM_ID!),
usdcMint: new PublicKey(process.env.USDC_MINT!),
recipientWallet: serverKeypair.publicKey,
serverKeypair: serverKeypair,
exposeCapabilities: true // Automatically adds /.well-known/x402-capabilities
});
// Protected route
fastify.get('/api/premium', {
preHandler: fastify.requirePayment({ amount: 1_000_000n })
}, async (request, reply) => {
return {
content: 'Premium content',
payment: request.payment
};
});
await fastify.listen({ port: 3000 });Configuration
ChannelPaymentServiceConfig
interface ChannelPaymentServiceConfig {
// Required
rpcUrl: string; // Solana RPC endpoint
network: 'devnet' | 'mainnet-beta';
programId: PublicKey; // Payment channel program ID
usdcMint: PublicKey; // USDC token mint address
recipientWallet: PublicKey; // Server's recipient wallet
// Optional
serverKeypair?: Keypair; // Required for claiming channel payments
defaultExpiry?: number; // Default: 604800 (7 days)
minBalance?: bigint; // Default: 1_000_000 (1 USDC)
enableFallback?: boolean; // Default: true
cacheTTL?: number; // Default: 30000 (30 seconds)
}Express API
channelAuthMiddleware
Middleware that enforces payment requirements:
import { channelAuthMiddleware } from '@x402-channels/server/express';
// Fixed price
app.get('/api/data',
channelAuthMiddleware(paymentService, {
amount: BigInt(1_000_000)
}),
handler
);
// Dynamic pricing
app.post('/api/process',
channelAuthMiddleware(paymentService, {
amount: async (req) => {
const items = req.body.items || [];
return BigInt(items.length * 100_000); // 0.1 USDC per item
}
}),
handler
);
// Channel-only (no x402 fallback)
app.get('/api/channel-only',
channelAuthMiddleware(paymentService, {
amount: BigInt(1_000_000),
requireChannel: true
}),
handler
);
// Custom error handling
app.get('/api/custom',
channelAuthMiddleware(paymentService, {
amount: BigInt(1_000_000),
onError: (error, req, res) => {
res.status(402).json({ error: error.message });
}
}),
handler
);extractPaymentMiddleware
Extract payment without enforcing it:
import { extractPaymentMiddleware } from '@x402-channels/server/express';
app.get('/api/content',
extractPaymentMiddleware(paymentService),
(req, res) => {
if (req.payment?.success) {
res.json({ content: 'Premium', tier: 'paid' });
} else {
res.json({ content: 'Basic', tier: 'free' });
}
}
);Helpers
import {
getPaymentResult,
hasValidPayment,
getPaymentMethod
} from '@x402-channels/server/express';
app.get('/api/info', (req, res) => {
const payment = getPaymentResult(req);
const isValid = hasValidPayment(req);
const method = getPaymentMethod(req);
res.json({ payment, isValid, method });
});NestJS API
ChannelPaymentGuard
Guard that enforces payment requirements:
import {
ChannelPaymentGuard,
RequirePayment,
Payment,
PaymentResult
} from '@x402-channels/server/nestjs';
@Controller('api')
@UseGuards(ChannelPaymentGuard)
export class ApiController {
// Fixed price
@Get('premium')
@RequirePayment(1_000_000n)
getPremium(@Payment() payment: PaymentResult) {
return { content: 'Premium', payment };
}
// Dynamic pricing
@Post('process')
@RequirePayment((context) => {
const request = context.switchToHttp().getRequest();
return BigInt(request.body.items.length * 100_000);
})
processData(@Body() body: any) {
return { processed: body.items.length };
}
// Channel-only
@Get('channel-only')
@RequirePayment(1_000_000n, { requireChannel: true })
getChannelOnly() {
return { content: 'Channel-only' };
}
}Decorators
import {
Payment,
PaymentMethod,
ChannelId,
RemainingBalance
} from '@x402-channels/server/nestjs';
@Controller('api')
@UseGuards(ChannelPaymentGuard)
export class ApiController {
// Inject full payment result
@Get('info1')
@RequirePayment(1_000_000n)
getInfo1(@Payment() payment: PaymentResult) {
return payment;
}
// Inject specific properties
@Get('info2')
@RequirePayment(1_000_000n)
getInfo2(
@PaymentMethod() method: string,
@ChannelId() channelId: string,
@RemainingBalance() balance: bigint
) {
return { method, channelId, balance: balance?.toString() };
}
}Class-level Payment Requirements
// Apply to entire controller
@Controller('premium')
@UseGuards(ChannelPaymentGuard)
@RequirePayment(1_000_000n) // All routes require 1 USDC
export class PremiumController {
@Get('content1')
getContent1() {
return { data: 'Content 1' };
}
@Get('content2')
getContent2() {
return { data: 'Content 2' };
}
// Override with different price
@Get('vip')
@RequirePayment(5_000_000n)
getVip() {
return { data: 'VIP content' };
}
}Fastify API
Plugin Registration
import channelPaymentPlugin from '@x402-channels/server/fastify';
await fastify.register(channelPaymentPlugin, {
rpcUrl: process.env.SOLANA_RPC_URL!,
network: 'devnet',
programId: new PublicKey(process.env.PROGRAM_ID!),
usdcMint: new PublicKey(process.env.USDC_MINT!),
recipientWallet: serverPublicKey,
serverKeypair: serverKeypair,
exposeCapabilities: true, // Add /.well-known/x402-capabilities
exposeStats: false // Add /payment/stats (protect in production!)
});Route Protection
// Fixed price
fastify.get('/api/premium', {
preHandler: fastify.requirePayment({ amount: 1_000_000n })
}, async (request, reply) => {
return { content: 'Premium', payment: request.payment };
});
// Dynamic pricing
fastify.post('/api/process', {
preHandler: fastify.requirePayment({
amount: async (req) => {
const body = req.body as any;
return BigInt(body.items.length * 100_000);
}
})
}, async (request, reply) => {
return { processed: true };
});
// Optional payment
fastify.get('/api/content', {
preHandler: fastify.extractPayment()
}, async (request, reply) => {
if (request.payment?.success) {
return { content: 'Premium', tier: 'paid' };
}
return { content: 'Basic', tier: 'free' };
});Helpers
import {
getPayment,
hasValidPayment,
getPaymentMethod
} from '@x402-channels/server/fastify';
fastify.get('/api/info', async (request, reply) => {
const payment = getPayment(request);
const isValid = hasValidPayment(request);
const method = getPaymentMethod(request);
return { payment, isValid, method };
});Payment Flow
Client sends request with payment headers:
x-channel-payment: Base64-encoded payment authorizationx-channel-id: Channel identifierx-payment-amount: Amount in smallest unitsx-payment-nonce: Nonce for replay protection
Server validates payment authorization:
- Extract authorization from headers
- Verify signature against client's public key
- Check channel status (open, not expired)
- Verify sufficient balance
- Validate nonce (must increment)
If valid: Claim payment and continue request
- Update channel state (off-chain)
- Attach payment result to request
- Execute route handler
If invalid: Fall back to x402
- Check for
x-solana-signatureheader - Verify on-chain transaction
- If valid, continue request
- If invalid, return 402 Payment Required
- Check for
Payment Result
interface PaymentResult {
success: boolean;
method: 'channel' | 'x402' | 'none';
amount: bigint;
signature?: string; // TX signature or authorization ID
newNonce?: bigint; // New nonce (channels)
remainingBalance?: bigint; // Remaining balance (channels)
channelId?: string; // Channel ID (channels)
error?: string; // Error message if failed
timestamp: Date;
}402 Response Format
When payment is required or invalid:
{
"statusCode": 402,
"error": "Payment Required",
"message": "Valid payment required to access this resource",
"amount": "1000000",
"recipient": "8xKj...",
"network": "devnet",
"methods": [
{
"type": "channel",
"supported": true,
"details": {
"programId": "9xQe...",
"network": "devnet",
"recipient": "8xKj..."
}
},
{
"type": "x402",
"supported": true,
"details": {
"network": "devnet",
"recipient": "8xKj...",
"usdcMint": "Gh9Z..."
}
}
],
"channelSetup": {
"programId": "9xQe...",
"minDeposit": "1000000",
"recommendedDeposit": "10000000"
}
}Event System
Listen to payment events for analytics and monitoring:
paymentService.onPaymentEvent((event) => {
console.log(`Event: ${event.type}`);
switch (event.type) {
case 'payment_received':
console.log(`Received ${event.amount} via ${event.method}`);
// Log to analytics, update database, etc.
break;
case 'channel_depleted':
console.log(`Channel ${event.channelId} balance low`);
// Send notification to client to refill
break;
case 'fallback_triggered':
console.log(`Fallback to x402: ${event.error}`);
// Monitor fallback usage
break;
case 'payment_failed':
console.log(`Payment failed: ${event.error}`);
// Log failures for debugging
break;
}
});Payment Statistics
Track payment metrics:
const stats = paymentService.getStats();
console.log({
totalPayments: stats.totalPayments,
channelPayments: stats.channelPayments,
x402Payments: stats.x402Payments,
failedPayments: stats.failedPayments,
totalAmount: stats.totalAmount.toString(),
averageAmount: stats.averageAmount.toString(),
channelSavings: stats.channelSavings.toString() // Est. tx fees saved
});
// Reset statistics
paymentService.resetStats();Server Capabilities Endpoint
Expose capabilities for client discovery at /.well-known/x402-capabilities:
{
"supportsChannels": true,
"supportsX402": true,
"channelProgramId": "9xQe...",
"minChannelDeposit": "1000000",
"maxChannelExpiry": 604800,
"recipientWallet": "8xKj...",
"network": "devnet",
"usdcMint": "Gh9Z..."
}Best Practices
- Always provide server keypair for channel claiming
- Expose capabilities endpoint for client discovery
- Monitor payment events for analytics and debugging
- Set appropriate cache TTL based on your needs
- Protect stats endpoints in production
- Use dynamic pricing for flexible monetization
- Handle 402 responses gracefully on client side
- Monitor channel depletion events to notify clients
- Test with both payment methods (channel + x402)
- Use TypeScript for type safety
Environment Variables
# Solana Configuration
SOLANA_RPC_URL=https://api.devnet.solana.com
CHANNEL_PROGRAM_ID=9xQe...
USDC_MINT=Gh9Z...
RECIPIENT_WALLET=8xKj...
SERVER_KEYPAIR=[...] # JSON array of keypair secret key
# Optional
NODE_ENV=production
PORT=3000Security Considerations
- Server Keypair: Keep server keypair secret and never expose it
- Rate Limiting: Implement rate limiting to prevent abuse
- Signature Verification: All payments are cryptographically verified
- Nonce Checking: Prevents replay attacks
- Balance Validation: Prevents overspending
- Error Handling: Never expose sensitive error details in production
TypeScript Support
Fully typed with complete type definitions:
import type {
ChannelPaymentServiceConfig,
PaymentResult,
PaymentRequirement,
PaymentStats,
ServerCapabilities,
PaymentHeaders
} from '@x402-channels/server';License
MIT
Contributing
See CONTRIBUTING.md for details.
Support
- GitHub Issues: https://github.com/BOBER3r/solana-payment-channel-kit/issues
- Documentation: https://docs.x402.dev
- Discord: https://discord.gg/x402
