What Are Payment Challenges?
A payment challenge is a cryptographically secure, time-limited token that binds a specific payment request to a specific resource. It prevents replay attacks and ensures payments can only be used for their intended purpose.
Challenge Generation
Challenges are generated server-side with the following properties:
challenge-structure.ts
interface PaymentChallenge {
// Unique identifier for this challenge
id: string // e.g., "vnt_ch_1234abcd..."
// Payment details
price: string // "0.001"
currency: string // "ETH"
network: NetworkId // "base"
recipient: Address // "0x742d..."
// Challenge metadata
resource: string // "/api/premium/data"
method: string // "GET"
timestamp: number // Unix timestamp
expires: number // Unix timestamp
// Optional
memo?: string // User-facing description
metadata?: Record<string, unknown>
// Cryptographic proof
signature: string // Server signature
}Generation Process
// Internal challenge generation
function generateChallenge(config: ChallengeConfig): PaymentChallenge {
const id = `vnt_ch_${randomBytes(16).toString('hex')}`
const timestamp = Math.floor(Date.now() / 1000)
const expires = timestamp + (config.expiresIn || 300) // 5 min default
const payload = {
id,
price: config.price,
currency: config.currency || 'ETH',
network: config.network,
recipient: config.recipient,
resource: config.resource,
method: config.method,
timestamp,
expires,
memo: config.memo,
}
// Sign with server's private key
const signature = sign(payload, serverPrivateKey)
return { ...payload, signature }
}Security Properties
- Uniqueness: Each challenge has a unique ID using cryptographically secure random bytes
- Time-bound: Challenges expire after a configurable duration (default 5 minutes)
- Resource-bound: Challenges are tied to a specific endpoint and HTTP method
- Tamper-proof: Server signature prevents modification
- Single-use: Challenges are invalidated after successful payment verification
Challenge Storage
Vanta stores challenges to track their status and prevent replay attacks:
// Challenge states
enum ChallengeState {
PENDING = 'pending', // Awaiting payment
VERIFIED = 'verified', // Payment confirmed
EXPIRED = 'expired', // Time limit exceeded
CANCELLED = 'cancelled', // Manually invalidated
}
// Storage interface
interface ChallengeStorage {
create(challenge: PaymentChallenge): Promise<void>
get(id: string): Promise<StoredChallenge | null>
markVerified(id: string, txHash: string): Promise<void>
cleanup(): Promise<number> // Remove expired challenges
}Storage Backends
Vanta supports in-memory storage (development), Redis (recommended for production), and custom storage adapters.
Replay Protection
Several mechanisms prevent replay attacks:
- Challenge ID: Must match exactly between request and payment
- Single use: Challenge marked as used after verification
- Expiration: Challenges become invalid after their expiry time
- Transaction binding: Payment must include challenge ID in tx data
Configuration Options
VantaMiddleware({
// Required
price: '0.001',
recipient: '0x...',
network: 'base',
// Challenge configuration
challengeExpiry: 300, // 5 minutes (default)
challengeStorage: redisStorage, // Custom storage
// Dynamic price based on request
price: (req) => calculatePrice(req),
// Include metadata in challenge
metadata: (req) => ({
userId: req.headers['x-user-id'],
endpoint: req.path,
}),
})Next Steps
- Verification - How payment verification works
- Access Tokens - Post-payment authentication