Quick Start

Get up and running with Vanta SDK in under 5 minutes. This guide covers everything from installation to accepting your first payment.

Prerequisites

Before you begin, make sure you have:

  • Node.js 18.0 or later installed
  • A package manager (npm, yarn, or pnpm)
  • An Ethereum wallet address for receiving payments
  • Basic familiarity with HTTP APIs and TypeScript

Installation

Install the Vanta SDK core package and the server middleware for your framework. The SDK is split into modular packages so you only include what you need.

npm install @vanta/sdk @vanta/middleware

For client-side applications that need to automatically handle 402 responses, also install the client package:

npm install @vanta/client

Example 1: Protect a Route with HTTP 402

The simplest way to monetize an API endpoint is to wrap it with Vanta middleware. When a client makes a request without payment, they receive an HTTP 402 response with a payment challenge.

server.ts
import express from 'express'
import { VantaMiddleware } from '@vanta/middleware'

const app = express()

// Configure Vanta middleware for your protected route
const paymentMiddleware = VantaMiddleware({
  // Price per request in ETH (0.001 ETH ≈ $2.50 at $2500/ETH)
  price: '0.001',
  
  // Your wallet address to receive payments
  recipient: '0x742d35Cc6634C0532925a3b844Bc4e7595f0aB42',
  
  // Network for payments (base, ethereum, optimism, arbitrum)
  network: 'base',
  
  // Optional: Custom challenge expiry (default: 5 minutes)
  challengeExpiry: 300,
  
  // Optional: Memo shown to users
  memo: 'Premium API Access',
})

// Apply middleware to protect your route
app.get('/api/premium/data', paymentMiddleware, (req, res) => {
  // This only runs after successful payment verification
  res.json({
    data: 'This is premium content!',
    timestamp: new Date().toISOString(),
  })
})

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

What's Happening

  • Request without payment: Client makes a normal HTTP request to your endpoint
  • 402 Response: Middleware intercepts and returns HTTP 402 with a payment challenge in the WWW-Authenticate header
  • Challenge contains: Price, recipient address, network, and a unique challenge ID
  • Client pays: After payment, client retries with proof in the Authorization header
  • Verification: Middleware verifies the payment on-chain before passing to your handler

Example 2: Verify Payment and Issue Access Token

For better UX, you can issue short-lived access tokens after payment verification. This allows clients to make multiple requests without paying each time.

server-with-tokens.ts
import express from 'express'
import { VantaMiddleware, VantaTokenIssuer } from '@vanta/middleware'

const app = express()

// Create a token issuer for session management
const tokenIssuer = new VantaTokenIssuer({
  secret: process.env.VANTA_TOKEN_SECRET!,
  
  // Token expires after 1 hour
  expiresIn: '1h',
  
  // Optional: Include custom claims
  claims: (req) => ({
    userId: req.headers['x-user-id'],
    tier: 'premium',
  }),
})

// Payment middleware with token issuance
const paymentMiddleware = VantaMiddleware({
  price: '0.01',  // 0.01 ETH for 1 hour of access
  recipient: '0x742d35Cc6634C0532925a3b844Bc4e7595f0aB42',
  network: 'base',
  
  // Issue token after successful payment
  onPaymentVerified: async (payment, req, res) => {
    const token = await tokenIssuer.issue(req, {
      paymentId: payment.transactionHash,
      amount: payment.amount,
    })
    
    // Include token in response headers
    res.setHeader('X-Vanta-Token', token)
    res.setHeader('X-Vanta-Token-Expires', tokenIssuer.getExpiry(token))
  },
})

// Token validation middleware for subsequent requests
const tokenMiddleware = tokenIssuer.middleware({
  // If token is invalid or expired, fall back to payment
  fallback: paymentMiddleware,
})

// Protected routes use token middleware
app.get('/api/premium/*', tokenMiddleware, (req, res, next) => {
  // req.vanta contains token claims if authenticated via token
  console.log('Authenticated:', req.vanta?.claims)
  next()
})

app.get('/api/premium/data', (req, res) => {
  res.json({ data: 'Premium content', claims: req.vanta?.claims })
})

app.get('/api/premium/more-data', (req, res) => {
  res.json({ data: 'More premium content' })
})

app.listen(3000)

What's Happening

  • First request: Client pays and receives an access token in the response header
  • Token storage: Client stores the token (localStorage, memory, etc.)
  • Subsequent requests: Client includes token in Authorization: Bearer vanta_tk_...
  • Token validation: tokenMiddleware validates without blockchain calls
  • Token expiry: After 1 hour, client must pay again for a new token
  • Fallback: Invalid/expired tokens trigger the payment flow automatically

Example 3: Client-Side Integration

The Vanta client automatically handles 402 responses, prompts for payment, and retries requests. It works with any wallet provider that supports EIP-1193.

client.ts
import { VantaClient } from '@vanta/client'
import { ethers } from 'ethers'

// Initialize with a wallet provider (e.g., MetaMask, WalletConnect)
const provider = new ethers.BrowserProvider(window.ethereum)

const vantaClient = new VantaClient({
  // Wallet provider for signing transactions
  provider,
  
  // Optional: Auto-approve payments under this amount (in ETH)
  autoApproveThreshold: '0.01',
  
  // Optional: Custom confirmation handler
  onPaymentRequired: async (challenge) => {
    // Show UI to user, return true to proceed or false to cancel
    const confirmed = await showPaymentModal({
      amount: challenge.price,
      recipient: challenge.recipient,
      memo: challenge.memo,
    })
    return confirmed
  },
  
  // Optional: Payment success callback
  onPaymentSuccess: (receipt) => {
    console.log('Payment confirmed:', receipt.transactionHash)
    showToast('Payment successful!')
  },
  
  // Optional: Store and reuse access tokens
  tokenStorage: {
    get: (key) => localStorage.getItem(`vanta_token_${key}`),
    set: (key, token) => localStorage.setItem(`vanta_token_${key}`, token),
    remove: (key) => localStorage.removeItem(`vanta_token_${key}`),
  },
})

// Use like fetch() - 402 responses are handled automatically
async function getPremiumData() {
  try {
    const response = await vantaClient.fetch('https://api.example.com/premium/data')
    const data = await response.json()
    console.log('Got premium data:', data)
    return data
  } catch (error) {
    if (error.code === 'PAYMENT_CANCELLED') {
      console.log('User cancelled payment')
    } else if (error.code === 'PAYMENT_FAILED') {
      console.error('Payment failed:', error.message)
    } else {
      throw error
    }
  }
}

// Or use the request method for more control
async function getPremiumDataWithOptions() {
  const { data, receipt, token } = await vantaClient.request({
    url: 'https://api.example.com/premium/data',
    method: 'GET',
    
    // Skip payment confirmation for this request
    autoApprove: true,
    
    // Custom headers
    headers: {
      'X-Custom-Header': 'value',
    },
  })
  
  console.log('Data:', data)
  console.log('Payment receipt:', receipt)
  console.log('Access token for future requests:', token)
}

What's Happening

  • Initial request: Client makes request, server returns 402 with challenge
  • Challenge parsing: VantaClient extracts price, recipient, and network from headers
  • User confirmation: If above threshold, onPaymentRequired prompts user
  • Payment execution: Client signs and broadcasts transaction via provider
  • Retry with proof: After confirmation, request retries with payment proof
  • Token caching: If server returns a token, it's stored and reused for future requests

Example 4: Metered Usage with Quotas

For usage-based billing, Vanta supports quota keys that track consumption across requests. This enables per-token, per-byte, or time-based pricing models.

server-metered.ts
import express from 'express'
import { VantaMiddleware, QuotaManager } from '@vanta/middleware'

const app = express()

// Create a quota manager for usage tracking
const quotaManager = new QuotaManager({
  // Storage backend (Redis recommended for production)
  storage: new RedisStorage(process.env.REDIS_URL),
  
  // Default quota settings
  defaults: {
    // Price per unit (e.g., per 1000 tokens)
    pricePerUnit: '0.0001',
    
    // Units included in initial purchase
    initialUnits: 10000,
    
    // Minimum units to purchase
    minPurchase: 1000,
  },
})

// Metered middleware with quota tracking
const meteredMiddleware = VantaMiddleware({
  recipient: '0x742d35Cc6634C0532925a3b844Bc4e7595f0aB42',
  network: 'base',
  
  // Dynamic pricing based on requested units
  price: async (req) => {
    const units = parseInt(req.headers['x-requested-units'] || '1000')
    return quotaManager.calculatePrice(units)
  },
  
  // Quota key for tracking (e.g., API key, user ID)
  quotaKey: (req) => req.headers['x-api-key'] || req.ip,
  
  // Check and deduct quota
  onBeforeRequest: async (req, quotaKey) => {
    const estimate = estimateUsage(req)
    const hasQuota = await quotaManager.check(quotaKey, estimate)
    
    if (!hasQuota) {
      // Return amount needed to purchase more quota
      const needed = await quotaManager.getNeeded(quotaKey, estimate)
      return { requiresPayment: true, amount: needed }
    }
    
    return { requiresPayment: false }
  },
  
  // Credit quota after payment
  onPaymentVerified: async (payment, req) => {
    const quotaKey = req.headers['x-api-key'] || req.ip
    const units = quotaManager.unitsFromPayment(payment.amount)
    await quotaManager.credit(quotaKey, units)
    
    console.log(`Credited ${units} units to ${quotaKey}`)
  },
})

// AI endpoint with token metering
app.post('/api/ai/completion', meteredMiddleware, async (req, res) => {
  const quotaKey = req.headers['x-api-key'] || req.ip
  
  // Process the AI request
  const result = await processAIRequest(req.body)
  
  // Deduct actual usage
  const tokensUsed = result.usage.totalTokens
  await quotaManager.deduct(quotaKey, tokensUsed)
  
  // Include remaining quota in response
  const remaining = await quotaManager.getRemaining(quotaKey)
  
  res.json({
    result: result.content,
    usage: {
      tokensUsed,
      tokensRemaining: remaining,
    },
  })
})

// Endpoint to check quota without making a request
app.get('/api/quota', async (req, res) => {
  const quotaKey = req.headers['x-api-key'] || req.ip
  const remaining = await quotaManager.getRemaining(quotaKey)
  const priceForMore = quotaManager.calculatePrice(10000)
  
  res.json({
    remaining,
    priceFor10kTokens: priceForMore,
  })
})

app.listen(3000)

What's Happening

  • Quota key: Identifies the user/client for quota tracking (API key, IP, user ID)
  • Pre-request check: onBeforeRequest verifies sufficient quota exists
  • Dynamic pricing: Price calculated based on units needed to fulfill request
  • Quota credit: After payment, units are credited to the quota key
  • Usage deduction: Actual usage is deducted after request completes
  • Balance tracking: Clients can check remaining quota via /api/quota

Production Considerations

For production deployments, consider:

  • Use Redis or a distributed cache for quota storage
  • Implement rate limiting to prevent abuse
  • Add monitoring for payment failures and disputes
  • Set up alerts for low balance warnings

Troubleshooting

Common issues and their solutions when integrating Vanta SDK.

Common Errors

INVALID_CHALLENGE

The payment challenge has expired or was already used.

// Error response
{
  "error": "INVALID_CHALLENGE",
  "message": "Challenge expired or already used",
  "code": 402
}

// Solution: Request a fresh challenge
// Challenges expire after 5 minutes by default

PAYMENT_NOT_FOUND

The transaction hash provided doesn't exist or hasn't been confirmed.

// Error response
{
  "error": "PAYMENT_NOT_FOUND",
  "message": "Transaction not found on chain",
  "code": 402
}

// Solution: Wait for transaction confirmation
// Vanta requires at least 1 confirmation by default

INSUFFICIENT_PAYMENT

The payment amount doesn't match the challenge price.

// Error response
{
  "error": "INSUFFICIENT_PAYMENT",
  "message": "Payment amount 0.0005 ETH is less than required 0.001 ETH",
  "code": 402
}

// Solution: Ensure payment matches the exact price in the challenge

WRONG_RECIPIENT

The payment was sent to the wrong address.

// Error response
{
  "error": "WRONG_RECIPIENT",
  "message": "Payment recipient doesn't match challenge",
  "code": 402
}

// Solution: Verify recipient address matches the one in the challenge

Security Notes

Security Best Practices

  • Never expose private keys - Use environment variables and secure key management
  • Validate all inputs - Sanitize quota keys, user IDs, and custom claims
  • Use HTTPS - Payment proofs in headers must be transmitted securely
  • Monitor transactions - Set up alerts for unusual payment patterns
  • Rotate token secrets - Periodically rotate your VANTA_TOKEN_SECRET

Rate Limiting

Even with payments, you should implement rate limiting to prevent abuse and ensure fair usage.

server-with-rate-limit.ts
import { VantaMiddleware, RateLimiter } from '@vanta/middleware'

const rateLimiter = new RateLimiter({
  // Max requests per window
  max: 100,
  
  // Window size in seconds
  window: 60,
  
  // Key function (e.g., by IP, API key, or wallet address)
  key: (req) => req.headers['x-api-key'] || req.ip,
  
  // Optional: Different limits for different tiers
  tiers: {
    free: { max: 10, window: 60 },
    paid: { max: 100, window: 60 },
    premium: { max: 1000, window: 60 },
  },
  
  // Determine tier from request
  getTier: (req) => req.vanta?.claims?.tier || 'free',
})

// Apply rate limiting before payment middleware
app.use('/api/premium', rateLimiter.middleware())
app.use('/api/premium', paymentMiddleware)
app.get('/api/premium/data', handler)

Next Steps

Now that you have Vanta SDK integrated, explore these resources:

You're Ready!

You've successfully integrated Vanta SDK. Your API is now ready to accept x402 payments. If you run into issues, check the FAQ or open an issue on GitHub.