@paygate/client-solana
x402 client for the Solana network. Uses @solana/kit for transaction construction, supports SPL Token and Token-2022 programs, and implements partial signing - the user signs, the facilitator co-signs as fee payer and broadcasts.
Installation
npm install @paygate/client-solana
# or
pnpm add @paygate/client-solanaHow Solana Payments Work
Solana payments use partial signing. The payer signs a token transfer instruction but does not pay SOL fees - the facilitator's wallet acts as the fee payer.
Payer partially signs SPL transfer transaction (no SOL fees)
↓
Base64-encoded wire transaction sent via X-PAYMENT header
↓
Facilitator co-signs as fee payer
↓
Facilitator broadcasts the fully-signed transaction
↓
USDC SPL tokens move from payer → merchantSupports both Token Program and Token-2022 Program - the client auto-detects which program governs the payment asset.
SolanaX402Client
import { SolanaX402Client } from '@paygate/client-solana'
const client = new SolanaX402Client(config: SolanaX402ClientConfig)SolanaX402ClientConfig
interface SolanaX402ClientConfig {
walletProvider: SolanaWalletProvider // wallet implementation
network: 'mainnet-beta' | 'devnet' | 'testnet' // Solana network
rpcEndpoint?: string // custom RPC (optional)
facilitatorUrl?: string // for discoverResources()
}Methods
| Method | Returns | Description |
|---|---|---|
fetchWithPayment(url, options?) | Promise<Response> | Fetch a URL, auto-paying any 402. |
getAccount() | Promise<{ address: string }> | Returns the connected wallet address. |
getBalance() | Promise<number> | Returns SOL balance of the connected wallet. |
getTokenBalance(mintAddress) | Promise<number> | Returns SPL token balance (in UI units). |
discoverResources(facilitatorUrl?, filters?) | Promise<any> | Lists resources from the facilitator. |
Wallet Providers
PhantomWalletProvider - Browser (Phantom Extension)
import { SolanaX402Client, PhantomWalletProvider } from '@paygate/client-solana'
if (!PhantomWalletProvider.isAvailable()) {
throw new Error('Please install Phantom wallet')
}
const provider = new PhantomWalletProvider()
await provider.connect()
const client = new SolanaX402Client({
walletProvider: provider,
network: 'devnet',
})
const response = await client.fetchWithPayment('https://api.example.com/premium')
PhantomWalletProvideralso detectswindow.solanafor other wallets that expose the standard Solana wallet interface.
WalletAdapterProvider - @solana/wallet-adapter-react
For React apps using the standard @solana/wallet-adapter-react setup:
import { useWallet } from '@solana/wallet-adapter-react'
import { SolanaX402Client, WalletAdapterProvider } from '@paygate/client-solana'
function MyComponent() {
const wallet = useWallet()
async function fetchData() {
const provider = new WalletAdapterProvider(wallet)
const client = new SolanaX402Client({
walletProvider: provider,
network: 'devnet',
})
const response = await client.fetchWithPayment('https://api.example.com/premium')
return response.json()
}
}SolanaWalletProvider Interface
Implement this to support any other Solana wallet (e.g. Backpack, Solflare):
interface SolanaWalletProvider {
getAccount(): Promise<{ address: string; publicKey: PublicKey }>
getSigner(): Promise<TransactionSigner> // @solana/kit TransactionSigner
signTransaction<T extends Transaction | VersionedTransaction>(tx: T): Promise<T>
connect(): Promise<void>
disconnect(): Promise<void>
}Solana Payment Payload
What gets sent in the X-PAYMENT header (base64-encoded JSON):
{
x402Version: 1,
paymentRequirements: { ... },
paymentPayload: {
x402Version: 1,
scheme: "exact",
network: "solana-devnet",
payload: {
transaction: "<base64 wire-encoded partially-signed transaction>"
}
}
}The extra field in PaymentRequirements contains Solana-specific data:
extra: {
feePayer: string // facilitator's wallet (pays SOL fees)
decimals: number // token decimals
recentBlockhash?: string // optional blockhash (fetched fresh if omitted)
}Transaction Construction
The client automatically:
- Detects whether the payment asset uses Token Program or Token-2022 Program
- Finds the payer's and recipient's Associated Token Accounts (ATAs)
- Estimates the compute unit limit via simulation
- Fetches the latest blockhash (or uses the one from
extra.recentBlockhash) - Partially signs - only the payer's signature is added; the facilitator adds the fee payer signature on the server
Full Example - React App with Phantom
import { useState } from 'react'
import { SolanaX402Client, PhantomWalletProvider } from '@paygate/client-solana'
export function PremiumDataButton() {
const [data, setData] = useState(null)
const [status, setStatus] = useState('')
async function fetchData() {
if (!PhantomWalletProvider.isAvailable()) {
alert('Please install Phantom wallet')
return
}
const provider = new PhantomWalletProvider()
await provider.connect()
const client = new SolanaX402Client({
walletProvider: provider,
network: 'devnet',
})
const response = await client.fetchWithPayment(
'https://api.example.com/premium',
{
onPaymentRequired: async (req) => {
const amount = (Number(req.maxAmountRequired) / 1e6).toFixed(2)
setStatus(`Paying ${amount} USDC on Solana...`)
return true
},
onSigningRequired: () => setStatus('Approve in Phantom...'),
onPaymentComplete: (s) => {
setStatus(`Done! Tx: ${s?.transaction?.slice(0, 12)}...`)
},
}
)
setData(await response.json())
}
return (
<div>
<button onClick={fetchData}>Access Premium Data</button>
{status && <p>{status}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
)
}Full Example - React with @solana/wallet-adapter
import { useWallet } from '@solana/wallet-adapter-react'
import { SolanaX402Client, WalletAdapterProvider } from '@paygate/client-solana'
export function PremiumButton() {
const wallet = useWallet()
async function fetchPremium() {
if (!wallet.connected) {
await wallet.connect()
}
const client = new SolanaX402Client({
walletProvider: new WalletAdapterProvider(wallet),
network: 'devnet',
rpcEndpoint: 'https://api.devnet.solana.com',
})
const response = await client.fetchWithPayment('https://api.example.com/data', {
onPaymentComplete: (s) => console.log('Settled:', s?.transaction),
})
return response.json()
}
return <button onClick={fetchPremium}>Fetch Data</button>
}Supported Networks
| Network | Identifier | RPC |
|---|---|---|
| Mainnet | mainnet-beta | https://api.mainnet-beta.solana.com |
| Devnet | devnet | https://api.devnet.solana.com |
| Testnet | testnet | https://api.testnet.solana.com |
Custom RPC endpoints (Helius, QuickNode, etc.) are supported via rpcEndpoint in the config.