x402 Integration Tutorial: Step-by-Step Guide
December 13, 2025 ยท View on GitHub
This tutorial walks you through integrating x402 v2 payment protocol into the Solana MCP Server, enabling you to monetize your MCP tool calls with blockchain payments.
Table of Contents
- Prerequisites
- Quick Start
- Step 1: Enable x402 Feature
- Step 2: Configure Your Server
- Step 3: Set Up Facilitator Service
- Step 4: Configure Payment Requirements
- Step 5: Implement Client-Side Integration
- Step 6: Test Your Integration
- Step 7: Deploy to Production
- Troubleshooting
Prerequisites
Before you begin, ensure you have:
- Rust 1.70+ installed (
rustc --version) - Solana CLI tools (
solana --version) - A Solana wallet with devnet SOL for testing
- Basic understanding of Solana transactions and SPL tokens
- A facilitator service URL (or use mock facilitator for testing)
Quick Start
For the impatient, here's a 5-minute setup:
# 1. Clone the repo
git clone https://github.com/openSVM/solana-mcp-server.git
cd solana-mcp-server
# 2. Build with x402 feature
cargo build --release --features x402
# 3. Create config file
cat > config.json << 'EOF'
{
"rpc_url": "https://api.devnet.solana.com",
"x402": {
"enabled": true,
"facilitator_base_url": "https://facilitator.example.com",
"networks": {
"solana-devnet": {
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"assets": [
{
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"name": "USDC",
"decimals": 6
}
],
"pay_to": "YOUR_RECIPIENT_WALLET_ADDRESS",
"min_compute_unit_price": 1000,
"max_compute_unit_price": 100000
}
}
}
}
EOF
# 4. Run the server
./target/release/solana-mcp-server --config config.json
Now let's go through each step in detail.
Step 1: Enable x402 Feature
1.1 Build with x402 Feature Flag
The x402 functionality is behind a feature flag (default off) to ensure zero impact on existing installations.
# Build with x402 enabled
cargo build --release --features x402
# Or for development
cargo build --features x402
1.2 Verify x402 is Available
Check that x402 is compiled in:
# The binary should be larger with x402 enabled
ls -lh target/release/solana-mcp-server
# Run with --help to see x402-related options
./target/release/solana-mcp-server --help
Step 2: Configure Your Server
2.1 Create Configuration File
Create a config.json file with x402 configuration:
{
"rpc_url": "https://api.devnet.solana.com",
"commitment": "confirmed",
"x402": {
"enabled": true,
"facilitator_base_url": "https://facilitator.example.com",
"timeout_seconds": 30,
"max_retries": 3,
"networks": {
"solana-devnet": {
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"assets": [
{
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"name": "USDC",
"decimals": 6
}
],
"pay_to": "FeeRecipient11111111111111111111111111111111",
"min_compute_unit_price": 1000,
"max_compute_unit_price": 100000
}
}
}
}
2.2 Configuration Options Explained
| Option | Required | Description |
|---|---|---|
enabled | Yes | Set to true to activate x402 |
facilitator_base_url | Yes | Your facilitator service endpoint (HTTPS required) |
timeout_seconds | No | HTTP request timeout (default: 30) |
max_retries | No | Max retry attempts for facilitator calls (default: 3) |
networks | Yes | Map of network configurations (see below) |
Network Configuration:
| Field | Required | Description |
|---|---|---|
network | Yes | CAIP-2 network identifier (e.g., solana:EtWT...) |
assets | Yes | Array of accepted SPL tokens |
pay_to | Yes | Your wallet address to receive payments |
min_compute_unit_price | Yes | Minimum gas price (prevents too-low fees) |
max_compute_unit_price | Yes | Maximum gas price (prevents abuse) |
Asset Configuration:
| Field | Required | Description |
|---|---|---|
address | Yes | SPL token mint address |
name | Yes | Human-readable token name |
decimals | Yes | Token decimals (e.g., 6 for USDC, 9 for SOL) |
2.3 Get CAIP-2 Network Identifiers
CAIP-2 format: <namespace>:<reference>
Common Solana Networks:
Mainnet: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
Devnet: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
Testnet: solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
The reference is the genesis hash of the network.
# Get genesis hash for current network
solana genesis-hash
2.4 Common Token Addresses
Devnet:
{
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
}
Mainnet:
{
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
"SOL (Wrapped)": "So11111111111111111111111111111111111111112"
}
Step 3: Set Up Facilitator Service
The facilitator service validates and settles payments. You can use an existing facilitator or run your own.
3.1 Use Mock Facilitator (Testing)
For local development, create a mock facilitator:
// mock-facilitator.js
const express = require('express');
const app = express();
app.use(express.json());
// POST /verify - Always approve
app.post('/verify', (req, res) => {
console.log('Verify request:', JSON.stringify(req.body, null, 2));
res.json({
valid: true,
message: "Mock verification successful"
});
});
// POST /settle - Always succeed
app.post('/settle', (req, res) => {
console.log('Settle request:', JSON.stringify(req.body, null, 2));
res.json({
settled: true,
transaction_id: "mock_tx_" + Date.now(),
message: "Mock settlement successful"
});
});
// GET /supported - Return your configured networks
app.get('/supported', (req, res) => {
res.json({
networks: [
{
network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
schemes: ["exact"],
assets: ["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"]
}
]
});
});
app.listen(3001, () => {
console.log('Mock facilitator running on http://localhost:3001');
});
Run it:
# Install express
npm install express
# Run mock facilitator
node mock-facilitator.js
Update your config:
{
"x402": {
"facilitator_base_url": "http://localhost:3001",
...
}
}
3.2 Production Facilitator Setup
For production, you'll need a real facilitator service that:
- Validates Solana transactions against x402 v2 spec
- Checks payment amounts match requirements
- Verifies ATA destinations are correct
- Settles payments on-chain
- Returns signed receipts
Facilitator API Requirements:
POST /verify
- Input:
{ "x402Version": 2, "accepted": {...}, "payload": "..." } - Output:
{ "valid": true/false, "message": "..." }
POST /settle
- Input:
{ "x402Version": 2, "accepted": {...}, "payload": "..." } - Output:
{ "settled": true/false, "transaction_id": "...", "receipt": {...} }
GET /supported
- Output: List of supported networks, schemes, and assets
Step 4: Configure Payment Requirements
4.1 Define Which Tools Require Payment
In your MCP server code, add payment requirements to specific tools:
use solana_mcp_server::x402::mcp_integration::{create_payment_required_response, PaymentRequirementsBuilder};
// Example: Make getBalance require payment
pub async fn handle_get_balance(address: String, payment: Option<PaymentPayload>) -> Result<Response> {
// Check if payment is provided
if payment.is_none() {
// Return payment required error
let requirements = PaymentRequirementsBuilder::new(
"mcp://tool/getBalance",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Network
"1000000", // Amount (1 USDC in smallest units)
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
)
.with_timeout(300) // 5 minutes
.build();
return create_payment_required_response(requirements);
}
// Verify and settle payment
let config = load_x402_config()?;
let facilitator = FacilitatorClient::new(&config.facilitator_base_url);
// Verify payment
let verify_result = facilitator.verify(payment.unwrap()).await?;
if !verify_result.valid {
return Err(Error::InvalidPayment(verify_result.message));
}
// Settle payment
let settle_result = facilitator.settle(payment.unwrap()).await?;
if !settle_result.settled {
return Err(Error::SettlementFailed(settle_result.message));
}
// Process the actual request
let balance = get_balance_from_blockchain(address).await?;
Ok(Response {
result: json!({ "balance": balance }),
settlement: Some(settle_result),
})
}
4.2 Set Pricing Strategy
Fixed Pricing per Tool:
fn get_tool_price(tool_name: &str) -> &str {
match tool_name {
"getBalance" => "1000000", // 1 USDC
"getTransaction" => "2000000", // 2 USDC
"getTokenAccounts" => "5000000", // 5 USDC
_ => "1000000" // Default: 1 USDC
}
}
Dynamic Pricing:
fn calculate_price(tool_name: &str, params: &Value) -> String {
// Base price
let base = 1_000_000; // 1 USDC
// Add complexity multiplier
let multiplier = match tool_name {
"getMultipleAccounts" => {
let count = params["addresses"].as_array().unwrap().len();
count as u64
},
_ => 1
};
(base * multiplier).to_string()
}
Step 5: Implement Client-Side Integration
5.1 JavaScript/TypeScript Client
Create a client that handles payment flows:
import { Connection, Keypair, Transaction, PublicKey } from '@solana/web3.js';
import {
createTransferCheckedInstruction,
getAssociatedTokenAddress,
} from '@solana/spl-token';
class MCPClient {
private wallet: Keypair;
private connection: Connection;
private mcpEndpoint: string;
constructor(walletKeypair: Keypair, rpcUrl: string, mcpEndpoint: string) {
this.wallet = walletKeypair;
this.connection = new Connection(rpcUrl, 'confirmed');
this.mcpEndpoint = mcpEndpoint;
}
async callTool(toolName: string, params: any): Promise<any> {
// Step 1: Try calling without payment
let response = await this.makeRequest(toolName, params, null);
// Step 2: Check if payment is required
if (response.error && response.error.code === -40200) {
console.log('Payment required:', response.error.data);
// Step 3: Create and sign payment transaction
const paymentData = response.error.data;
const payment = await this.createPayment(paymentData);
// Step 4: Retry with payment
response = await this.makeRequest(toolName, params, payment);
}
if (response.error) {
throw new Error(`MCP Error: ${response.error.message}`);
}
return response.result;
}
private async makeRequest(
toolName: string,
params: any,
payment: any | null
): Promise<any> {
const request: any = {
jsonrpc: '2.0',
id: Date.now(),
method: `tools/call`,
params: {
name: toolName,
arguments: params,
},
};
// Add payment metadata if provided
if (payment) {
request.params._meta = { payment };
}
const response = await fetch(this.mcpEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
}
private async createPayment(paymentRequired: any): Promise<any> {
// Extract payment requirements
const { accepts } = paymentRequired;
const requirement = accepts[0]; // Use first accepted payment method
const {
network,
amount,
asset: mintAddress,
payTo,
scheme,
} = requirement;
// Get token decimals (assuming USDC = 6)
const decimals = 6;
// Create payment transaction
const transaction = new Transaction();
// Add compute budget
transaction.add(
ComputeBudgetProgram.setComputeUnitLimit({
units: 200_000,
})
);
transaction.add(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 10_000,
})
);
// Get source ATA
const mint = new PublicKey(mintAddress);
const sourceATA = await getAssociatedTokenAddress(
mint,
this.wallet.publicKey
);
// Get destination ATA
const destATA = await getAssociatedTokenAddress(
mint,
new PublicKey(payTo)
);
// Add transfer instruction
transaction.add(
createTransferCheckedInstruction(
sourceATA,
mint,
destATA,
this.wallet.publicKey,
BigInt(amount),
decimals
)
);
// Get recent blockhash
const { blockhash } = await this.connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = this.wallet.publicKey;
// Sign transaction
transaction.sign(this.wallet);
// Serialize to base64
const serialized = transaction.serialize().toString('base64');
// Return payment payload
return {
x402Version: 2,
accepted: requirement,
payload: serialized,
};
}
}
// Usage example
async function main() {
// Load wallet
const wallet = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.WALLET_SECRET_KEY!))
);
// Create client
const client = new MCPClient(
wallet,
'https://api.devnet.solana.com',
'http://localhost:3000/api/mcp'
);
// Call a paid tool
try {
const balance = await client.callTool('getBalance', {
address: 'DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK',
});
console.log('Balance:', balance);
} catch (error) {
console.error('Error:', error);
}
}
5.2 Python Client
import base64
import json
from solders.keypair import Keypair
from solders.transaction import Transaction
from solana.rpc.api import Client
import requests
class MCPClient:
def __init__(self, wallet_path: str, rpc_url: str, mcp_endpoint: str):
with open(wallet_path, 'r') as f:
secret = json.load(f)
self.wallet = Keypair.from_bytes(bytes(secret))
self.connection = Client(rpc_url)
self.mcp_endpoint = mcp_endpoint
def call_tool(self, tool_name: str, params: dict) -> dict:
# Try without payment
response = self._make_request(tool_name, params, None)
# Check if payment required
if 'error' in response and response['error']['code'] == -40200:
print('Payment required:', response['error']['data'])
# Create payment
payment_data = response['error']['data']
payment = self._create_payment(payment_data)
# Retry with payment
response = self._make_request(tool_name, params, payment)
if 'error' in response:
raise Exception(f"MCP Error: {response['error']['message']}")
return response['result']
def _make_request(self, tool_name: str, params: dict, payment: dict) -> dict:
request = {
'jsonrpc': '2.0',
'id': 1,
'method': 'tools/call',
'params': {
'name': tool_name,
'arguments': params,
}
}
if payment:
request['params']['_meta'] = {'payment': payment}
response = requests.post(
self.mcp_endpoint,
json=request,
headers={'Content-Type': 'application/json'}
)
return response.json()
def _create_payment(self, payment_required: dict) -> dict:
# Implement payment transaction creation
# Similar to JavaScript example above
pass
# Usage
client = MCPClient(
wallet_path='~/.config/solana/id.json',
rpc_url='https://api.devnet.solana.com',
mcp_endpoint='http://localhost:3000/api/mcp'
)
balance = client.call_tool('getBalance', {
'address': 'DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK'
})
print('Balance:', balance)
Step 6: Test Your Integration
6.1 Unit Testing
Test payment flow with mock facilitator:
# Start mock facilitator
node mock-facilitator.js &
# Run server with test config
cargo run --features x402 -- --config test-config.json
# Run integration tests
cargo test --features x402 x402_integration
6.2 Manual Testing
Test 1: Payment Required Response
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "getBalance",
"arguments": {"address": "DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK"}
}
}'
Expected response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -40200,
"message": "Payment Required",
"data": {
"x402Version": 2,
"resource": {"url": "mcp://tool/getBalance"},
"accepts": [...]
}
}
}
Test 2: Successful Payment
# Create a signed transaction (use your client code)
# Then submit with payment:
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type": application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "getBalance",
"arguments": {"address": "..."},
"_meta": {
"payment": {
"x402Version": 2,
"accepted": {...},
"payload": "BASE64_SIGNED_TRANSACTION"
}
}
}
}'
6.3 Monitor Logs
Enable debug logging:
RUST_LOG=debug ./target/release/solana-mcp-server --config config.json
Look for:
[x402] Payment required for tool: getBalance[x402] Verifying payment with facilitator[x402] Payment verified successfully[x402] Settling payment with facilitator[x402] Payment settled: tx_id=...
Step 7: Deploy to Production
7.1 Production Checklist
- Use mainnet configuration with real USDC/USDT
- Set strong compute unit price bounds (prevent abuse)
- Use HTTPS for facilitator (required)
- Enable structured logging with trace IDs
- Configure monitoring and alerts
- Test payment flows thoroughly
- Set up rate limiting (prevent DoS)
- Document pricing for users
- Implement refund policy
- Add terms of service
7.2 Production Configuration
{
"rpc_url": "https://api.mainnet-beta.solana.com",
"commitment": "confirmed",
"x402": {
"enabled": true,
"facilitator_base_url": "https://facilitator.yourcompany.com",
"timeout_seconds": 30,
"max_retries": 3,
"networks": {
"solana-mainnet": {
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"assets": [
{
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"name": "USDC",
"decimals": 6
}
],
"pay_to": "YOUR_MAINNET_WALLET",
"min_compute_unit_price": 5000,
"max_compute_unit_price": 50000
}
}
}
}
7.3 Deployment Methods
Docker:
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release --features x402
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/solana-mcp-server /usr/local/bin/
COPY config.json /etc/solana-mcp/config.json
CMD ["solana-mcp-server", "--config", "/etc/solana-mcp/config.json"]
docker build -t solana-mcp-server:x402 .
docker run -p 3000:3000 solana-mcp-server:x402
Systemd Service:
# /etc/systemd/system/solana-mcp.service
[Unit]
Description=Solana MCP Server with x402
After=network.target
[Service]
Type=simple
User=solana-mcp
ExecStart=/usr/local/bin/solana-mcp-server --config /etc/solana-mcp/config.json
Restart=on-failure
Environment="RUST_LOG=info"
[Install]
WantedBy=multi-user.target
sudo systemctl enable solana-mcp
sudo systemctl start solana-mcp
sudo systemctl status solana-mcp
Troubleshooting
Common Issues
1. "Payment Required but no x402 config found"
Cause: x402 feature not enabled or config missing
Solution:
# Rebuild with x402 feature
cargo build --release --features x402
# Verify config has x402 section
cat config.json | jq .x402
2. "Facilitator verification failed: connection timeout"
Cause: Facilitator service unreachable
Solution:
# Test facilitator connectivity
curl https://facilitator.example.com/supported
# Check firewall rules
# Increase timeout in config
3. "Invalid payment: compute unit price out of bounds"
Cause: Transaction gas price too high/low
Solution:
// Set appropriate compute unit price
transaction.add(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 10_000, // Must be between min/max in config
})
);
4. "ATA validation failed: destination mismatch"
Cause: Incorrect destination ATA derivation
Solution:
// Ensure you derive ATA correctly
const destATA = await getAssociatedTokenAddress(
mint,
new PublicKey(payTo), // Use payTo from payment requirements
false // allowOwnerOffCurve = false
);
5. "Settlement failed: insufficient funds"
Cause: Wallet doesn't have enough tokens
Solution:
# Check token balance
spl-token accounts
# Get devnet tokens
solana airdrop 2
spl-token create-account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
# Get testnet USDC from faucet
Debug Mode
Enable verbose logging:
RUST_LOG=solana_mcp_server=trace,x402=trace \
./target/release/solana-mcp-server --config config.json
Validate Configuration
# Test config loading
cargo run --features x402 -- --config config.json --validate-only
# Check facilitator /supported endpoint
curl https://facilitator.example.com/supported | jq
Next Steps
- Read the full documentation:
docs/x402-integration.md - Review use cases: Check 13 detailed scenarios in the docs
- Join community: Get help on Discord/GitHub
- Monitor payments: Set up analytics and dashboards
- Iterate: Start with one paid tool, expand gradually
Additional Resources
Questions? Open an issue on GitHub or reach out to the community!