Skip to content

@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

bash
npm install @paygate/client-solana
# or
pnpm add @paygate/client-solana

How 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 → merchant

Supports both Token Program and Token-2022 Program - the client auto-detects which program governs the payment asset.


SolanaX402Client

typescript
import { SolanaX402Client } from '@paygate/client-solana'

const client = new SolanaX402Client(config: SolanaX402ClientConfig)

SolanaX402ClientConfig

typescript
interface SolanaX402ClientConfig {
  walletProvider: SolanaWalletProvider                          // wallet implementation
  network: 'mainnet-beta' | 'devnet' | 'testnet'               // Solana network
  rpcEndpoint?: string                                           // custom RPC (optional)
  facilitatorUrl?: string                                        // for discoverResources()
}

Methods

MethodReturnsDescription
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)

typescript
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')

PhantomWalletProvider also detects window.solana for 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:

typescript
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):

typescript
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):

typescript
{
  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:

typescript
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:

  1. Detects whether the payment asset uses Token Program or Token-2022 Program
  2. Finds the payer's and recipient's Associated Token Accounts (ATAs)
  3. Estimates the compute unit limit via simulation
  4. Fetches the latest blockhash (or uses the one from extra.recentBlockhash)
  5. 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

tsx
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

tsx
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

NetworkIdentifierRPC
Mainnetmainnet-betahttps://api.mainnet-beta.solana.com
Devnetdevnethttps://api.devnet.solana.com
Testnettestnethttps://api.testnet.solana.com

Custom RPC endpoints (Helius, QuickNode, etc.) are supported via rpcEndpoint in the config.

Released under the MIT License.