Payment Challenges

How Vanta SDK generates and manages secure payment challenges.

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:

  1. Challenge ID: Must match exactly between request and payment
  2. Single use: Challenge marked as used after verification
  3. Expiration: Challenges become invalid after their expiry time
  4. 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