Building a Paid API Service
Learn how to create a monetized API service that accepts cryptocurrency payments using x402-deploy.
Overview
In this tutorial, you'll build a weather forecast API that charges $0.001 per request, accepts USDC payments on Base, and is discoverable by AI agents.
What You'll Learn: - Setting up x402-deploy - Implementing paid API endpoints - Deploying to production - Registering in the discovery network - Monitoring earnings
Prerequisites: - Node.js 18+ - A crypto wallet (MetaMask) - Basic Express.js knowledge
Time: ~30 minutes
Step 1: Create Your API
First, create a basic Express API:
Create server.js:
const express = require('express');
const axios = require('axios');
require('dotenv').config();
const app = express();
// Free endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Weather forecast endpoint (will be paid)
app.get('/api/weather/:city', async (req, res) => {
const { city } = req.params;
try {
// Get weather data from OpenWeatherMap
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric`
);
const weather = {
city: response.data.name,
temperature: response.data.main.temp,
feelsLike: response.data.main.feels_like,
description: response.data.weather[0].description,
humidity: response.data.main.humidity,
windSpeed: response.data.wind.speed,
timestamp: new Date().toISOString()
};
res.json(weather);
} catch (error) {
res.status(404).json({ error: 'City not found' });
}
});
// 7-day forecast endpoint (premium)
app.get('/api/forecast/:city', async (req, res) => {
const { city } = req.params;
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric`
);
const forecast = response.data.list.map(item => ({
time: item.dt_txt,
temperature: item.main.temp,
description: item.weather[0].description
}));
res.json({ city, forecast });
} catch (error) {
res.status(404).json({ error: 'City not found' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Weather API running on port ${PORT}`);
});
Create .env:
Test your API:
Step 2: Add x402 Payment Gateway
Install x402-deploy:
Create x402.config.json:
{
"name": "weather-api",
"type": "express",
"payment": {
"wallet": "YOUR_WALLET_ADDRESS",
"network": "eip155:8453",
"token": "USDC",
"facilitator": "https://facilitator.x402.dev"
},
"pricing": {
"routes": {
"GET /api/health": "free",
"GET /api/weather/*": "$0.001",
"GET /api/forecast/*": "$0.005"
}
},
"discovery": {
"enabled": true,
"instructions": "Weather forecast API providing current conditions and 7-day forecasts for cities worldwide. Use /api/weather/{city} for current weather or /api/forecast/{city} for extended forecast.",
"categories": ["weather", "data", "forecast"],
"examples": [
{
"prompt": "What's the weather in London?",
"response": "Temperature: 15°C, Conditions: Partly cloudy"
},
{
"prompt": "Get 7-day forecast for Tokyo",
"response": "7-day forecast with temperatures and conditions"
}
]
},
"rateLimit": {
"enabled": true,
"maxRequests": 100,
"windowSeconds": 3600,
"perPayer": true
},
"analytics": {
"enabled": true,
"verbose": false
}
}
Update server.js to wrap with x402:
const express = require('express');
const axios = require('axios');
const { wrapWithX402 } = require('@nirholas/universal-crypto-mcp/x402');
const config = require('./x402.config.json');
require('dotenv').config();
const app = express();
// Your existing routes...
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/weather/:city', async (req, res) => {
const { city } = req.params;
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric`
);
const weather = {
city: response.data.name,
temperature: response.data.main.temp,
feelsLike: response.data.main.feels_like,
description: response.data.weather[0].description,
humidity: response.data.main.humidity,
windSpeed: response.data.wind.speed,
timestamp: new Date().toISOString()
};
res.json(weather);
} catch (error) {
res.status(404).json({ error: 'City not found' });
}
});
app.get('/api/forecast/:city', async (req, res) => {
const { city } = req.params;
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric`
);
const forecast = response.data.list.map(item => ({
time: item.dt_txt,
temperature: item.main.temp,
description: item.weather[0].description
}));
res.json({ city, forecast });
} catch (error) {
res.status(404).json({ error: 'City not found' });
}
});
// Wrap with x402 payment gateway
const wrappedApp = wrapWithX402(app, config);
const PORT = process.env.PORT || 3000;
wrappedApp.listen(PORT, () => {
console.log(`Weather API with x402 payments running on port ${PORT}`);
console.log(`Accepting payments at wallet: ${config.payment.wallet}`);
});
Step 3: Test Payment Flow
Start your server:
Test Free Endpoint
Test Paid Endpoint Without Payment
Response (402 Payment Required):
{
"error": "payment_required",
"message": "Payment required to access this resource",
"payment": {
"price": "$0.001",
"wallet": "0x...",
"network": "eip155:8453",
"token": "USDC",
"instructions": "Send payment and include transaction hash in x-payment header"
}
}
Test with Payment
# First, make a payment on Base:
# - Send 0.001 USDC to the wallet address
# - Get the transaction hash
# Then make request with payment proof
curl -X GET http://localhost:3000/api/weather/London \
-H "x-payment: $(echo '{
"txHash": "0x...",
"payer": "0x...",
"amount": "1000",
"network": "eip155:8453"
}' | base64)"
Response (200 OK):
{
"city": "London",
"temperature": 15.2,
"feelsLike": 14.8,
"description": "partly cloudy",
"humidity": 72,
"windSpeed": 4.5,
"timestamp": "2026-01-29T12:00:00.000Z"
}
Step 4: Deploy to Production
Option A: Deploy to Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel
# Set environment variables
vercel env add OPENWEATHER_API_KEY production
vercel env add X402_WALLET_ADDRESS production
Option B: Deploy with x402-deploy CLI
# Install x402-deploy CLI
npm install -g @nirholas/universal-crypto-mcp
# Deploy (auto-detects best platform)
x402-deploy deploy --provider vercel
# Or deploy to Railway
x402-deploy deploy --provider railway
Option C: Docker Deployment
Create Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build and run:
docker build -t weather-api .
docker run -p 3000:3000 \
-e OPENWEATHER_API_KEY=your-key \
-e X402_WALLET_ADDRESS=0x... \
weather-api
Step 5: Register in Discovery Network
Your service is automatically registered when you start it with discovery enabled. To manually register or update:
Verify registration:
Your API should appear in search results, making it discoverable by AI agents!
Step 6: Monitor Earnings
CLI Dashboard
Output:
╔═══════════════════════════════════════════════════════════╗
║ x402 Analytics Dashboard ║
╚═══════════════════════════════════════════════════════════╝
📊 Earnings Summary
Total Revenue: $12.50
Total Payments: 1,245
Unique Payers: 87
Average Payment: $0.010
💰 Top Routes
GET /api/weather/* $8.32 (832 requests)
GET /api/forecast/* $4.18 (836 requests)
👥 Top Payers
0x742d... $2.45 (245 requests)
0x8b5f... $1.89 (189 requests)
0x3c2a... $1.23 (123 requests)
Web Dashboard
Then open http://localhost:8080
Export Data
# Export to CSV
x402-deploy analytics --export earnings.csv --format csv
# Export to JSON
x402-deploy analytics --export earnings.json --format json --period 30
Step 7: Add Webhooks (Optional)
Receive real-time payment notifications.
Create webhook endpoint in server.js:
app.post('/webhooks/payment', express.json(), (req, res) => {
const { event, data, signature } = req.body;
// Verify webhook signature
const crypto = require('crypto');
const secret = process.env.WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify({ event, data }))
.digest('hex');
if (`sha256=${expectedSignature}` !== signature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Handle payment event
if (event === 'payment.received') {
console.log(`💰 Payment received: ${data.amount} from ${data.payer}`);
console.log(` Route: ${data.route}`);
console.log(` Tx: ${data.txHash}`);
// Send notification, update database, etc.
}
res.json({ received: true });
});
Update x402.config.json:
{
"analytics": {
"enabled": true,
"webhookUrl": "https://your-api.vercel.app/webhooks/payment",
"webhookSecret": "your-secret-key"
}
}
Advanced Features
Dynamic Pricing
Offer volume discounts:
const { PricingEngine } = require('@nirholas/universal-crypto-mcp/x402');
const engine = new PricingEngine({
'GET /api/weather/*': '$0.001',
'GET /api/forecast/*': '$0.005'
});
// Volume-based tiers
engine.setDynamicPricing('GET /api/weather/*', {
basePrice: '$0.001',
tiers: [
{ minRequests: 0, maxRequests: 100, price: '$0.001' },
{ minRequests: 100, maxRequests: 1000, price: '$0.0008' },
{ minRequests: 1000, price: '$0.0005' }
]
});
// Use custom pricing engine
const wrappedApp = wrapWithX402(app, {
...config,
pricingEngine: engine
});
Custom Authentication
Add additional authentication:
const authenticateUser = async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// Verify API key
const valid = await verifyApiKey(apiKey);
if (!valid) {
return res.status(401).json({ error: 'Invalid API key' });
}
next();
};
// Apply before x402 middleware
app.use(authenticateUser);
const wrappedApp = wrapWithX402(app, config);
Response Caching
Cache expensive API responses:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
app.get('/api/weather/:city', async (req, res) => {
const { city } = req.params;
// Check cache
const cacheKey = `weather:${city.toLowerCase()}`;
const cached = cache.get(cacheKey);
if (cached) {
return res.json(cached);
}
// Fetch fresh data
const weather = await getWeatherData(city);
// Store in cache
cache.set(cacheKey, weather);
res.json(weather);
});
Testing
Unit Tests
const request = require('supertest');
const app = require('./server');
describe('Weather API', () => {
it('health check should be free', async () => {
const res = await request(app).get('/api/health');
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('status', 'ok');
});
it('weather endpoint requires payment', async () => {
const res = await request(app).get('/api/weather/London');
expect(res.status).toBe(402);
expect(res.body).toHaveProperty('error', 'payment_required');
});
it('accepts valid payment', async () => {
const paymentProof = createMockPayment('0x...', '1000');
const res = await request(app)
.get('/api/weather/London')
.set('x-payment', paymentProof);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('city');
});
});
Integration Tests
describe('Payment Flow', () => {
it('full payment flow', async () => {
// 1. Request without payment
let res = await request(app).get('/api/weather/London');
expect(res.status).toBe(402);
const { payment } = res.body;
// 2. Make payment on blockchain
const txHash = await sendPayment(payment.wallet, payment.price);
// 3. Request with payment proof
const proof = createPaymentProof(txHash);
res = await request(app)
.get('/api/weather/London')
.set('x-payment', proof);
expect(res.status).toBe(200);
});
});
Best Practices
1. Error Handling
app.get('/api/weather/:city', async (req, res) => {
try {
const weather = await getWeatherData(req.params.city);
res.json(weather);
} catch (error) {
if (error.code === 'CITY_NOT_FOUND') {
res.status(404).json({ error: 'City not found' });
} else {
console.error('Weather API error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
});
2. Input Validation
const { param, validationResult } = require('express-validator');
app.get('/api/weather/:city',
param('city').isAlpha().isLength({ min: 2, max: 50 }),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process request...
}
);
3. Rate Limiting
4. Monitoring
const { AnalyticsEngine } = require('@nirholas/universal-crypto-mcp/x402');
const analytics = new AnalyticsEngine({ enabled: true });
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
analytics.trackRequest(req.path, duration, res.statusCode);
});
next();
});
Troubleshooting
Payment Not Verified
Problem: Requests return 402 even after payment
Solutions: 1. Check transaction is confirmed on blockchain 2. Verify correct network (Base = eip155:8453) 3. Ensure payment amount matches price 4. Confirm wallet address is correct
Rate Limit Errors
Problem: Getting 429 errors
Solutions: 1. Increase rate limit in config 2. Check if testing with same address 3. Implement per-payer tracking
Discovery Not Working
Problem: Service not appearing in searches
Solutions:
1. Verify discovery.enabled is true
2. Check categories are valid
3. Wait 5 minutes for propagation
4. Verify service is publicly accessible
Next Steps
- Add More Features
- Historical weather data
- Weather alerts
-
Climate statistics
-
Optimize Performance
- Add caching
- Use CDN for static assets
-
Implement connection pooling
-
Scale Your Service
- Deploy to multiple regions
- Add load balancing
-
Implement Redis for distributed cache
-
Build a Client
- Create SDK for easy integration
- Build example integrations
- Write comprehensive docs
Complete Example
Find the complete code at: - GitHub: examples/paid-api - Live Demo: https://weather-api-x402.vercel.app