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.
- 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
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)
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.jsonNow let's go through each step in detail.
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 x402Check 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 --helpCreate 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
}
}
}
}| 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) |
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-hashDevnet:
{
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
}Mainnet:
{
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
"SOL (Wrapped)": "So11111111111111111111111111111111111111112"
}The facilitator service validates and settles payments. You can use an existing facilitator or run your own.
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.jsUpdate your config:
{
"x402": {
"facilitator_base_url": "http://localhost:3001",
...
}
}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
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),
})
}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()
}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);
}
}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)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_integrationTest 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"
}
}
}
}'Enable debug logging:
RUST_LOG=debug ./target/release/solana-mcp-server --config config.jsonLook 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=...
- 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
{
"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
}
}
}
}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:x402Systemd 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.targetsudo systemctl enable solana-mcp
sudo systemctl start solana-mcp
sudo systemctl status solana-mcpCause: 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 .x402Cause: Facilitator service unreachable
Solution:
# Test facilitator connectivity
curl https://facilitator.example.com/supported
# Check firewall rules
# Increase timeout in configCause: 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
})
);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
);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 faucetEnable verbose logging:
RUST_LOG=solana_mcp_server=trace,x402=trace \
./target/release/solana-mcp-server --config config.json# Test config loading
cargo run --features x402 -- --config config.json --validate-only
# Check facilitator /supported endpoint
curl https://facilitator.example.com/supported | jq- 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
Questions? Open an issue on GitHub or reach out to the community!