Skip to main content
Copy-paste patterns for validating payment requests for APIs, agents/tools, and protected resources.

Basic validation (framework-agnostic)

At a minimum, validation has three steps:
  1. Extract a Bearer token from the Authorization header
  2. Validate it against the request body
  3. Return 402 Payment Required (optionally including plans) when missing/invalid
import { Payments } from '@nevermined-io/payments'

const payments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: 'sandbox'
})

export async function validateRequest(token: string, body: unknown) {
  const result = await payments.requests.isValidRequest(token, body)

  return {
    isValid: result.isValid,
    balance: result.balance,
    reason: result.reason
  }
}

// Pseudo-usage
// - Extract token from Authorization: Bearer <token>
// - Call validateRequest(token, body)
// - If invalid, return 402

Validation with minimum credits

Check that the user has enough credits for the operation:
export async function validateWithMinimum(
  token: string,
  body: unknown,
  minCredits: number
) {
  const result = await payments.requests.isValidRequest(token, body)

  if (!result.isValid) {
    return { valid: false, reason: 'invalid_token' as const }
  }

  if (result.balance < minCredits) {
    return {
      valid: false,
      reason: 'insufficient_credits' as const,
      required: minCredits,
      available: result.balance
    }
  }

  return { valid: true, balance: result.balance }
}

// Example: expensive operation
const validation = await validateWithMinimum(token, body, 10)
if (!validation.valid) {
  // return 402
}

Validation response handling

Handle all possible validation outcomes:
async function handleValidation(token: string, body: any) {
  try {
    const result = await payments.requests.isValidRequest(token, body)

    if (!result.isValid) {
      // Determine specific error
      switch (result.reason) {
        case 'TOKEN_EXPIRED':
          return {
            status: 402,
            error: 'Access token has expired',
            code: 'TOKEN_EXPIRED',
            action: 'refresh_token'
          }

        case 'INSUFFICIENT_BALANCE':
          return {
            status: 402,
            error: 'Insufficient credits',
            code: 'INSUFFICIENT_BALANCE',
            action: 'purchase_credits'
          }

        case 'PLAN_EXPIRED':
          return {
            status: 402,
            error: 'Plan has expired',
            code: 'PLAN_EXPIRED',
            action: 'renew_plan'
          }

        default:
          return {
            status: 402,
            error: 'Invalid token',
            code: 'INVALID_TOKEN',
            action: 'get_new_token'
          }
      }
    }

    return { status: 200, balance: result.balance }
  } catch (error) {
    console.error('Validation error:', error)
    return {
      status: 500,
      error: 'Validation service unavailable',
      code: 'SERVICE_ERROR'
    }
  }
}

Caching validation results

For high-traffic endpoints, cache validation briefly:
import { LRUCache } from 'lru-cache'

const validationCache = new LRUCache<string, any>({
  max: 1000,
  ttl: 1000 * 30 // 30 seconds
})

async function validateWithCache(token: string, body: any) {
  // Create cache key from token hash
  const cacheKey = hashToken(token)
  const cached = validationCache.get(cacheKey)

  if (cached) {
    return cached
  }

  const result = await payments.requests.isValidRequest(token, body)

  if (result.isValid) {
    validationCache.set(cacheKey, result)
  }

  return result
}

function hashToken(token: string): string {
  return crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
}

Next steps