Skip to content

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:

mkdir weather-api
cd weather-api
npm init -y
npm install express axios dotenv

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:

OPENWEATHER_API_KEY=your-openweather-api-key
PORT=3000

Test your API:

node server.js

# In another terminal
curl http://localhost:3000/api/weather/London

Step 2: Add x402 Payment Gateway

Install x402-deploy:

npm install @nirholas/universal-crypto-mcp

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:

node server.js

Test Free Endpoint

curl http://localhost:3000/api/health
# Returns: {"status":"ok","timestamp":"2026-01-29T..."}

Test Paid Endpoint Without Payment

curl http://localhost:3000/api/weather/London

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:

x402-deploy marketplace register

Verify registration:

x402-deploy marketplace search --query "weather"

Your API should appear in search results, making it discoverable by AI agents!


Step 6: Monitor Earnings

CLI Dashboard

x402-deploy 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

x402-deploy dashboard --web --port 8080

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

{
  "rateLimit": {
    "enabled": true,
    "maxRequests": 100,
    "windowSeconds": 3600,
    "perPayer": true
  }
}

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

# Debug payment verification
DEBUG=x402:payment node server.js

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

  1. Add More Features
  2. Historical weather data
  3. Weather alerts
  4. Climate statistics

  5. Optimize Performance

  6. Add caching
  7. Use CDN for static assets
  8. Implement connection pooling

  9. Scale Your Service

  10. Deploy to multiple regions
  11. Add load balancing
  12. Implement Redis for distributed cache

  13. Build a Client

  14. Create SDK for easy integration
  15. Build example integrations
  16. Write comprehensive docs

Complete Example

Find the complete code at: - GitHub: examples/paid-api - Live Demo: https://weather-api-x402.vercel.app


Resources


**Ready to build your paid API?** [Get Started →](./quick-start.md)