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/middlewareFor client-side applications that need to automatically handle 402 responses, also install the client package:
npm install @vanta/clientExample 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.
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-Authenticateheader - Challenge contains: Price, recipient address, network, and a unique challenge ID
- Client pays: After payment, client retries with proof in the
Authorizationheader - 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.
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:
tokenMiddlewarevalidates 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.
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:
VantaClientextracts price, recipient, and network from headers - User confirmation: If above threshold,
onPaymentRequiredprompts 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.
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:
onBeforeRequestverifies 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 defaultPAYMENT_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 defaultINSUFFICIENT_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 challengeWRONG_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 challengeSecurity 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.
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:
- Understanding HTTP 402 - Deep dive into the protocol
- Request Lifecycle - How payments flow through your system
- Recipes - Framework-specific integration guides
- API Reference - Complete SDK documentation
You're Ready!