@payai/x402-solana
v2.0.0
Published
A framework-agnostic implementation of the x402 payment protocol v2 for Solana clients (browsers) and servers
Downloads
803
Readme
x402-solana
A reusable, framework-agnostic implementation of the x402 payment protocol v2 for Solana clients (browsers) and servers.
Features
✅ x402 Protocol v2 - Full support for the latest x402 v2 specification
✅ CAIP-2 Networks - Uses standardized chain identifiers (solana:chainId)
✅ Client-side - Automatic 402 payment handling with any wallet provider
✅ Server-side - Payment verification and settlement with facilitator
✅ Framework agnostic - Works with any wallet provider (Privy, Phantom, etc.)
✅ HTTP framework agnostic - Works with Next.js, Express, Fastify, etc.
✅ TypeScript - Full type safety with Zod validation
✅ Web3.js - Built on @solana/web3.js and @solana/spl-token
Installation
pnpm add @payai/x402-solanaOr with npm:
npm install @payai/x402-solanaOr with yarn:
yarn add @payai/x402-solanax402 Protocol v2
This package implements x402 protocol v2. Key features:
| Feature | v2 Specification |
| --------------------- | ------------------------------------------------- |
| Network Format | CAIP-2 (solana:chainId) |
| Payment Header | PAYMENT-SIGNATURE |
| Amount Field | amount |
| Payload Structure | Includes resource and accepted fields |
| Response Body | PaymentRequired with x402Version: 2 |
Usage
Client Side (React/Frontend)
The x402-solana client works with any wallet provider that implements the WalletAdapter interface. Below are examples using both Solana Wallet Adapter and Privy.
Option 1: Using Solana Wallet Adapter (Recommended)
First, install the required packages:
npm install @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/wallet-adapter-baseSetup your wallet provider in your app root (e.g., _app.tsx or layout.tsx):
import { useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
BackpackWalletAdapter,
} from '@solana/wallet-adapter-wallets';
// Import styles
import '@solana/wallet-adapter-react-ui/styles.css';
export default function App({ Component, pageProps }) {
const network = WalletAdapterNetwork.Devnet; // or Mainnet
const endpoint = useMemo(() => 'https://api.devnet.solana.com', []);
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new BackpackWalletAdapter(),
],
[]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<Component {...pageProps} />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}Use in your component:
import { createX402Client } from '@payai/x402-solana/client';
import { useWallet } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
function MyComponent() {
const wallet = useWallet();
const handlePaidRequest = async () => {
if (!wallet.connected || !wallet.publicKey) {
console.error('Wallet not connected');
return;
}
// Create x402 client (v2)
const client = createX402Client({
wallet: {
address: wallet.publicKey.toString(),
signTransaction: async (tx) => {
if (!wallet.signTransaction) throw new Error('Wallet does not support signing');
return await wallet.signTransaction(tx);
},
},
network: 'solana-devnet', // Simple format - automatically converted to CAIP-2
amount: BigInt(10_000_000), // Optional: max 10 USDC safety limit
});
// Make a paid request - automatically handles 402 payments
const response = await client.fetch('/api/paid-endpoint', {
method: 'POST',
body: JSON.stringify({ data: 'your request' }),
});
const result = await response.json();
console.log('Result:', result);
};
return (
<div>
<WalletMultiButton />
<button onClick={handlePaidRequest} disabled={!wallet.connected}>
Make Paid Request
</button>
</div>
);
}Option 2: Using Privy
import { createX402Client } from '@payai/x402-solana/client';
import { useSolanaWallets } from '@privy-io/react-auth/solana';
function MyComponent() {
const { wallets } = useSolanaWallets();
const wallet = wallets[0];
// Create x402 client (v2)
const client = createX402Client({
wallet,
network: 'solana-devnet',
amount: BigInt(10_000_000), // Optional: max 10 USDC
});
// Make a paid request - automatically handles 402 payments
const response = await client.fetch('/api/paid-endpoint', {
method: 'POST',
body: JSON.stringify({ data: 'your request' }),
});
const result = await response.json();
}Using with a Proxy Server (CORS Bypass)
If you're making requests from a browser to external APIs and encountering CORS issues, you can provide a custom fetch function that routes requests through your proxy server:
import { createX402Client } from '@payai/x402-solana/client';
import { useWallet } from '@solana/wallet-adapter-react';
function MyComponent() {
const wallet = useWallet();
// Create a custom fetch function that uses your proxy
const createProxyFetch = () => {
const proxyUrl = process.env.NEXT_PUBLIC_PROXY_URL || 'http://localhost:3001/api/proxy';
return async (url: string | RequestInfo, init?: RequestInit): Promise<Response> => {
// Send request through proxy server
const response = await fetch(proxyUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: typeof url === 'string' ? url : url.toString(),
method: init?.method || 'GET',
headers: init?.headers || {},
body: init?.body
})
});
const proxyData = await response.json();
// Reconstruct Response object with original status
return new Response(
typeof proxyData.data === 'string' ? proxyData.data : JSON.stringify(proxyData.data),
{
status: proxyData.status,
statusText: proxyData.statusText || '',
headers: new Headers(proxyData.headers || {})
}
);
};
};
const handlePaidRequest = async () => {
if (!wallet.connected || !wallet.publicKey) {
console.error('Wallet not connected');
return;
}
// Create x402 client with custom fetch (v2)
const client = createX402Client({
wallet: {
address: wallet.publicKey.toString(),
signTransaction: async (tx) => {
if (!wallet.signTransaction) throw new Error('Wallet does not support signing');
return await wallet.signTransaction(tx);
},
},
network: 'solana-devnet',
amount: BigInt(10_000_000),
customFetch: createProxyFetch() // Use proxy for all requests
});
// All requests now go through your proxy server
const response = await client.fetch('https://external-api.com/endpoint', {
method: 'POST',
body: JSON.stringify({ data: 'your request' }),
});
const result = await response.json();
console.log('Result:', result);
};
return (
<button onClick={handlePaidRequest} disabled={!wallet.connected}>
Make Paid Request (via Proxy)
</button>
);
}Benefits of using a proxy:
- Bypasses browser CORS restrictions
- Allows requests to any external x402 endpoint
- Enables custom request/response logging
- Provides a single point for request monitoring
Note: You need to set up your own proxy server. The customFetch parameter is optional - if not provided, the SDK uses the native fetch function.
Proxy Server Implementation
To use customFetch with a proxy, you need to implement a proxy server endpoint. Here's a complete example:
Next.js API Route (app/api/proxy/route.ts):
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const { url, method, headers, body } = await req.json();
// Validate inputs
if (!url || !method) {
return NextResponse.json({ error: 'url and method required' }, { status: 400 });
}
// Prepare headers (preserve x402 v2 payment headers)
const requestHeaders: Record<string, string> = {
'Content-Type': headers?.['Content-Type'] || 'application/json',
'User-Agent': 'x402-solana-proxy/2.0',
...(headers || {}),
};
// Remove problematic headers
delete requestHeaders['host'];
delete requestHeaders['content-length'];
// Make request to target endpoint
const fetchOptions: RequestInit = {
method: method.toUpperCase(),
headers: requestHeaders,
};
if (method.toUpperCase() !== 'GET' && body) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
const response = await fetch(url, fetchOptions);
// Parse response
const contentType = response.headers.get('content-type') || '';
let responseData: unknown;
if (contentType.includes('application/json')) {
responseData = await response.json();
} else {
responseData = await response.text();
}
// Prepare response headers
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
if (
!['content-encoding', 'transfer-encoding', 'content-length'].includes(key.toLowerCase())
) {
responseHeaders[key] = value;
}
});
// IMPORTANT: Return 200 with real status in body
// This allows proper x402 402 Payment Required handling
return NextResponse.json(
{
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
data: responseData,
contentType,
},
{ status: 200 }
);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('[Proxy] Error:', message);
return NextResponse.json(
{
error: 'Proxy request failed',
details: message,
},
{ status: 500 }
);
}
}Express Server (server.js):
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/proxy', async (req, res) => {
try {
const { url, method, headers, body } = req.body;
if (!url || !method) {
return res.status(400).json({ error: 'url and method required' });
}
const requestHeaders = {
'Content-Type': headers?.['Content-Type'] || 'application/json',
...(headers || {}),
};
delete requestHeaders['host'];
delete requestHeaders['content-length'];
const fetchOptions = {
method: method.toUpperCase(),
headers: requestHeaders,
};
if (method.toUpperCase() !== 'GET' && body) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
const response = await fetch(url, fetchOptions);
const contentType = response.headers.get('content-type') || '';
let responseData;
if (contentType.includes('application/json')) {
responseData = await response.json();
} else {
responseData = await response.text();
}
const responseHeaders = {};
response.headers.forEach((value, key) => {
if (
!['content-encoding', 'transfer-encoding', 'content-length'].includes(key.toLowerCase())
) {
responseHeaders[key] = value;
}
});
// Return 200 with real status in body for x402 v2 compatibility
res.status(200).json({
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
data: responseData,
contentType,
});
} catch (error) {
console.error('[Proxy] Error:', error.message);
res.status(500).json({
error: 'Proxy request failed',
details: error.message,
});
}
});
app.listen(3001, () => console.log('Proxy server running on port 3001'));Key Points:
- Always return HTTP 200 from proxy, with real status code in the response body
- This is critical for x402 v2 402 Payment Required responses to work correctly
- Preserve x402 v2 headers (
PAYMENT-SIGNATURE,PAYMENT-RESPONSE) - Remove problematic headers (
host,content-length)
Server Side (Next.js API Route)
import { NextRequest, NextResponse } from 'next/server';
import { X402PaymentHandler } from '@payai/x402-solana/server';
const x402 = new X402PaymentHandler({
network: 'solana-devnet', // Simple format - automatically converted to CAIP-2
treasuryAddress: process.env.TREASURY_WALLET_ADDRESS!,
facilitatorUrl: 'https://facilitator.payai.network',
});
export async function POST(req: NextRequest) {
const resourceUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/api/chat`;
// 1. Extract payment header (v2 uses PAYMENT-SIGNATURE)
const paymentHeader = x402.extractPayment(req.headers);
// 2. Create payment requirements (v2 format)
const paymentRequirements = await x402.createPaymentRequirements(
{
amount: '2500000', // $2.50 USDC (in atomic units, as string)
asset: {
address: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // USDC devnet
decimals: 6,
},
description: 'AI Chat Request',
},
resourceUrl
);
if (!paymentHeader) {
// Return 402 with v2 payment requirements
const response = x402.create402Response(paymentRequirements, resourceUrl);
return NextResponse.json(response.body, { status: response.status });
}
// 3. Verify payment
const verified = await x402.verifyPayment(paymentHeader, paymentRequirements);
if (!verified.isValid) {
return NextResponse.json(
{
error: 'Invalid payment',
reason: verified.invalidReason,
},
{ status: 402 }
);
}
// 4. Process your business logic
const result = await yourBusinessLogic(req);
// 5. Settle payment
const settlement = await x402.settlePayment(paymentHeader, paymentRequirements);
if (!settlement.success) {
console.error('Settlement failed:', settlement.errorReason);
}
// 6. Return response
return NextResponse.json(result);
}Server Side (Express)
import express from 'express';
import { X402PaymentHandler } from '@payai/x402-solana/server';
const app = express();
const x402 = new X402PaymentHandler({
network: 'solana-devnet',
treasuryAddress: process.env.TREASURY_WALLET_ADDRESS!,
facilitatorUrl: 'https://facilitator.payai.network',
});
app.post('/api/paid-endpoint', async (req, res) => {
const resourceUrl = `${process.env.BASE_URL}/api/paid-endpoint`;
const paymentHeader = x402.extractPayment(req.headers);
const paymentRequirements = await x402.createPaymentRequirements(
{
amount: '2500000', // $2.50 USDC
asset: {
address: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // USDC devnet
decimals: 6,
},
description: 'API Request',
},
resourceUrl
);
if (!paymentHeader) {
const response = x402.create402Response(paymentRequirements, resourceUrl);
return res.status(response.status).json(response.body);
}
const verified = await x402.verifyPayment(paymentHeader, paymentRequirements);
if (!verified.isValid) {
return res.status(402).json({
error: 'Invalid payment',
reason: verified.invalidReason,
});
}
const result = await yourBusinessLogic(req);
await x402.settlePayment(paymentHeader, paymentRequirements);
res.json(result);
});API Reference
Client
createX402Client(config)
Creates a new x402 client instance.
Config:
{
wallet: WalletAdapter; // Wallet with signTransaction method
network: 'solana' | 'solana-devnet'; // Simple network format
rpcUrl?: string; // Optional custom RPC
amount?: bigint; // Optional safety limit (max payment)
customFetch?: typeof fetch; // Optional custom fetch for proxy support
verbose?: boolean; // Optional debug logging
}Methods:
client.fetch(input, init)- Make a fetch request with automatic payment handling
Server
new X402PaymentHandler(config)
Creates a new payment handler instance.
Config:
{
network: 'solana' | 'solana-devnet'; // Simple network format
treasuryAddress: string; // Where payments are sent
facilitatorUrl: string; // Facilitator service URL
rpcUrl?: string; // Optional custom RPC
defaultToken?: TokenAsset; // Optional default token (auto-detected)
defaultDescription?: string; // Optional default description
defaultTimeoutSeconds?: number; // Optional timeout (default: 300)
}Methods:
extractPayment(headers)- Extract PAYMENT-SIGNATURE header from requestcreatePaymentRequirements(routeConfig, resourceUrl)- Create v2 payment requirements objectcreate402Response(requirements, resourceUrl)- Create v2 402 response bodyverifyPayment(header, requirements)- Verify payment with facilitatorsettlePayment(header, requirements)- Settle payment with facilitatorgetNetwork()- Get the network in CAIP-2 formatgetTreasuryAddress()- Get the treasury address
RouteConfig Format
The createPaymentRequirements method expects:
{
amount: string; // Payment amount in atomic units (string)
asset: {
address: string; // Token mint address (USDC)
decimals: number; // Token decimals (6 for USDC)
},
description?: string; // Optional human-readable description
mimeType?: string; // Optional, defaults to 'application/json'
maxTimeoutSeconds?: number; // Optional, defaults to 300
}Network Configuration
CAIP-2 Network Identifiers
x402 v2 uses CAIP-2 format for network identifiers:
| Network | Simple Format | CAIP-2 Format |
| ------- | --------------- | ----------------------------------------- |
| Mainnet | solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Devnet | solana-devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
The library automatically converts simple network names to CAIP-2 format internally. You can use either format in your configuration.
Network Utility Functions
import {
toCAIP2Network,
toSimpleNetwork,
isSolanaNetwork,
isSolanaMainnet,
isSolanaDevnet,
} from '@payai/x402-solana/types';
// Convert between formats
const caip2 = toCAIP2Network('solana-devnet'); // 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'
const simple = toSimpleNetwork('solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'); // 'solana-devnet'
// Type guards
isSolanaNetwork('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'); // true
isSolanaMainnet('solana'); // true
isSolanaDevnet('solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'); // trueConfiguration
Environment Variables
# Network (optional, defaults to devnet)
NEXT_PUBLIC_NETWORK=solana-devnet
# Treasury wallet address (where payments are sent)
TREASURY_WALLET_ADDRESS=your_treasury_address
# Optional: Custom RPC URLs
NEXT_PUBLIC_SOLANA_RPC_DEVNET=https://api.devnet.solana.com
NEXT_PUBLIC_SOLANA_RPC_MAINNET=https://api.mainnet-beta.solana.com
# Base URL for resource field
NEXT_PUBLIC_BASE_URL=http://localhost:3000USDC Mint Addresses
When creating payment requirements, you need to specify the USDC token mint address:
- Devnet:
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU - Mainnet:
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Example with environment-based selection:
const USDC_MINT =
process.env.NODE_ENV === 'production'
? 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // mainnet
: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'; // devnet
const paymentRequirements = await x402.createPaymentRequirements(
{
amount: '1000000', // $1.00 USDC
asset: {
address: USDC_MINT,
decimals: 6,
},
description: 'Payment',
},
`${process.env.BASE_URL}/api/endpoint`
);Wallet Adapter Interface
The package works with any wallet that implements this interface:
interface WalletAdapter {
// Support for Anza wallet-adapter standard
publicKey?: { toString(): string };
// Alternative for custom implementations
address?: string;
// Required for signing
signTransaction: (tx: VersionedTransaction) => Promise<VersionedTransaction>;
}Compatible Wallet Providers
Solana Wallet Adapter (@solana/wallet-adapter-react)
The official Solana wallet adapter provides the most flexibility and supports multiple wallets:
import { useWallet } from '@solana/wallet-adapter-react';
const wallet = useWallet();
const walletAdapter = {
publicKey: wallet.publicKey,
signTransaction: wallet.signTransaction,
};Privy (@privy-io/react-auth)
Privy wallets work out of the box:
import { useSolanaWallets } from '@privy-io/react-auth/solana';
const { wallets } = useSolanaWallets();
const wallet = wallets[0]; // Already implements the interfaceDirect Wallet SDKs
You can also use wallet SDKs directly:
// Phantom
const phantomProvider = window.phantom?.solana;
const walletAdapter = {
address: phantomProvider.publicKey.toString(),
signTransaction: tx => phantomProvider.signTransaction(tx),
};
// Solflare
const solflareProvider = window.solflare;
const walletAdapter = {
address: solflareProvider.publicKey.toString(),
signTransaction: tx => solflareProvider.signTransaction(tx),
};Payment Amounts
Payment amounts are in USDC atomic units (6 decimals) as strings:
- 1 USDC =
"1000000"atomic units - $0.01 =
"10000"atomic units - $2.50 =
"2500000"atomic units
Helper functions:
import { toAtomicUnits, fromAtomicUnits } from '@payai/x402-solana/utils';
const atomicUnits = toAtomicUnits(2.5, 6); // "2500000"
const usd = fromAtomicUnits('2500000', 6); // 2.5Protocol Details
PaymentRequired Response (v2)
When a resource requires payment, the server returns a 402 status with this body:
{
"x402Version": 2,
"resource": {
"url": "https://api.example.com/v1/ai/generate",
"description": "AI text generation",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"amount": "2500000",
"payTo": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHEBg4",
"maxTimeoutSeconds": 300,
"asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"extra": {
"feePayer": "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
}
}
],
"error": "Payment required"
}PaymentPayload (v2)
The client sends payment via PAYMENT-SIGNATURE header containing base64-encoded JSON:
{
"x402Version": 2,
"resource": {
"url": "https://api.example.com/v1/ai/generate",
"description": "AI text generation",
"mimeType": "application/json"
},
"accepted": {
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"amount": "2500000",
"payTo": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHEBg4",
"maxTimeoutSeconds": 300,
"asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"extra": {
"feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4"
}
},
"payload": {
"transaction": "<base64-encoded-signed-transaction>"
}
}Testing
Run the test suite:
npm testThe tests verify:
✅ Package imports work correctly
✅ Client can be created with wallet adapter
✅ Automatic 402 payment handling works
✅ Transaction signing and submission succeed
✅ Payment verification and settlement complete
Architecture
src/
├── client/ # Client-side code
│ ├── transaction-builder.ts # Solana transaction construction
│ ├── payment-interceptor.ts # 402 payment fetch interceptor
│ └── index.ts # Main client export
├── server/ # Server-side code
│ ├── facilitator-client.ts # Facilitator API communication
│ ├── payment-handler.ts # Payment verification & settlement
│ └── index.ts # Main server export
├── types/ # TypeScript types
│ ├── x402-protocol.ts # x402 v2 spec types (CAIP-2 networks)
│ ├── solana-payment.ts # Solana-specific types
│ └── index.ts
├── utils/ # Utilities
│ ├── helpers.ts # Helper functions
│ └── index.ts
└── index.ts # Main package exportDevelopment
Running Tests
npm testLinting
npm run lint
npm run lint:fix # Auto-fix issuesType Checking
npm run typecheckBuilding
npm run buildFuture Enhancements
- [ ] Add @solana/kit adapter for AI agents
- [ ] Support for multiple payment tokens
- [ ] Add transaction retry logic
- [ ] Support for partial payments
- [ ] Extensions support (SIWx, Discovery)
License
MIT
Credits
Built on top of:
Support
Version
Current version: 2.0.0 (x402 Protocol v2)
