
Security News
Feross on the 10 Minutes or Less Podcast: Nobody Reads the Code
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.
@payai/x402-solana
Advanced tools
A framework-agnostic implementation of the x402 payment protocol v2 for Solana clients (browsers) and servers
A reusable, framework-agnostic implementation of the x402 payment protocol v2 for Solana clients (browsers) and servers.
✅ 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
pnpm add @payai/x402-solana
Or with npm:
npm install @payai/x402-solana
Or with yarn:
yarn add @payai/x402-solana
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 |
The x402-solana client works with any wallet provider that implements the WalletAdapter interface. Below are examples using both Solana Wallet Adapter and Privy.
First, install the required packages:
npm install @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/wallet-adapter-base
Setup 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>
);
}
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();
}
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:
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.
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:
PAYMENT-SIGNATURE, PAYMENT-RESPONSE)host, content-length)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);
}
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);
});
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 handlingCreates 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 addressThe 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
}
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.
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'); // true
# 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:3000
When creating payment requirements, you need to specify the USDC token mint address:
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vExample 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`
);
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>;
}
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 interface
Direct 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 are in USDC atomic units (6 decimals) as strings:
"1000000" atomic units"10000" atomic units"2500000" atomic unitsHelper functions:
import { toAtomicUnits, fromAtomicUnits } from '@payai/x402-solana/utils';
const atomicUnits = toAtomicUnits(2.5, 6); // "2500000"
const usd = fromAtomicUnits('2500000', 6); // 2.5
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"
}
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>"
}
}
Run the test suite:
npm test
The 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
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 export
npm test
npm run lint
npm run lint:fix # Auto-fix issues
npm run typecheck
npm run build
MIT
Built on top of:
Current version: 2.0.0 (x402 Protocol v2)
FAQs
A framework-agnostic implementation of the x402 payment protocol v2 for Solana clients (browsers) and servers
We found that @payai/x402-solana demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.

Research
/Security News
Campaign of 108 extensions harvests identities, steals sessions, and adds backdoors to browsers, all tied to the same C2 infrastructure.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.