npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@solana-payment-channel/server

v0.2.2

Published

Server-side middleware and integrations for x402 payment channels on Solana (Express, NestJS, Fastify)

Readme

@solana-payment-channel/server

npm version npm downloads GitHub issues License: MIT Node.js Version

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

Quick 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

  1. Client sends request with payment headers:

    • x-channel-payment: Base64-encoded payment authorization
    • x-channel-id: Channel identifier
    • x-payment-amount: Amount in smallest units
    • x-payment-nonce: Nonce for replay protection
  2. 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)
  3. If valid: Claim payment and continue request

    • Update channel state (off-chain)
    • Attach payment result to request
    • Execute route handler
  4. If invalid: Fall back to x402

    • Check for x-solana-signature header
    • Verify on-chain transaction
    • If valid, continue request
    • If invalid, return 402 Payment Required

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

  1. Always provide server keypair for channel claiming
  2. Expose capabilities endpoint for client discovery
  3. Monitor payment events for analytics and debugging
  4. Set appropriate cache TTL based on your needs
  5. Protect stats endpoints in production
  6. Use dynamic pricing for flexible monetization
  7. Handle 402 responses gracefully on client side
  8. Monitor channel depletion events to notify clients
  9. Test with both payment methods (channel + x402)
  10. 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=3000

Security Considerations

  1. Server Keypair: Keep server keypair secret and never expose it
  2. Rate Limiting: Implement rate limiting to prevent abuse
  3. Signature Verification: All payments are cryptographically verified
  4. Nonce Checking: Prevents replay attacks
  5. Balance Validation: Prevents overspending
  6. 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