Explicit Client

The explicit client (X402Client) provides manual control over the X402 payment flow. You explicitly check for 402 responses, create payments, and retry requests.

When to Use

Use the explicit client when you need:

  • Fine-grained control over the payment flow

  • Custom payment logic

  • Different handling for different payment scenarios

  • Integration with custom payment systems

Basic Usage

import { X402Client } from '@shade402/client';
import { Keypair } from '@solana/web3.js';

const wallet = Keypair.generate();
const client = new X402Client(wallet, process.env.SOLANA_RPC_URL);

try {
  // Make initial request
  let response = await client.get('https://api.example.com/data');

  // Check if payment required
  if (client.paymentRequired(response)) {
    // Parse payment request
    const paymentRequest = client.parsePaymentRequest(response);

    // Create payment
    const authorization = await client.createPayment(paymentRequest);

    // Retry with payment
    response = await client.get('https://api.example.com/data', {
      payment: authorization,
    });
  }

  // Process response
  console.log(response.data);
} finally {
  await client.close();
}

HTTP Methods

All standard HTTP methods are supported:

// GET request
const getResponse = await client.get(url, options);

// POST request
const postResponse = await client.post(url, data, options);

// PUT request
const putResponse = await client.put(url, data, options);

// DELETE request
const deleteResponse = await client.delete(url, options);

// Generic request
const response = await client.request('GET', url, options);

Payment Flow

Step 1: Initial Request

const response = await client.get('https://api.example.com/data');

Step 2: Check Payment Required

if (client.paymentRequired(response)) {
  // Payment is required (status 402)
}

Step 3: Parse Payment Request

const paymentRequest = client.parsePaymentRequest(response);

console.log('Amount:', paymentRequest.maxAmountRequired);
console.log('Token:', paymentRequest.assetAddress);
console.log('Expires:', paymentRequest.expiresAt);

Step 4: Validate Payment Request

// Check if expired
if (paymentRequest.isExpired()) {
  throw new Error('Payment request expired');
}

// Check amount
const amount = parseFloat(paymentRequest.maxAmountRequired);
if (amount > maxAllowedAmount) {
  throw new Error('Payment amount too high');
}

Step 5: Create Payment

// Create payment with exact amount
const authorization = await client.createPayment(paymentRequest);

// Or specify custom amount (must be <= max_amount_required)
const authorization = await client.createPayment(paymentRequest, '0.005');

Step 6: Encrypt Resource (Optional)

// Encrypt resource if server public key is available
let encryptedResource: string | undefined;
try {
  encryptedResource = client.encryptResource(paymentRequest.resource);
} catch (error) {
  // Encryption optional, continue without it
}

Step 7: Retry Request

const response = await client.get('https://api.example.com/data', {
  payment: authorization,
  encryptedResource: encryptedResource,
});

Resource Encryption

The client supports resource encryption for enhanced privacy:

// Get 402 response (includes server public key in headers)
const response = await client.get(url);
const paymentRequest = client.parsePaymentRequest(response);

// Server public key is now cached
const publicKey = client.getServerPublicKey();

// Encrypt resource
const encryptedResource = client.encryptResource(paymentRequest.resource);

// Retry with encrypted resource
const retryResponse = await client.get(url, {
  payment: authorization,
  encryptedResource: encryptedResource,
});

Custom Payment Amount

You can pay less than the maximum amount if allowed:

const paymentRequest = client.parsePaymentRequest(response);

// Pay custom amount (must be <= max_amount_required)
const customAmount = '0.005'; // Half of max
const authorization = await client.createPayment(paymentRequest, customAmount);

Error Handling

Handle errors at each step:

try {
  let response = await client.get(url);

  if (client.paymentRequired(response)) {
    const paymentRequest = client.parsePaymentRequest(response);

    if (paymentRequest.isExpired()) {
      throw new Error('Payment request expired');
    }

    try {
      const authorization = await client.createPayment(paymentRequest);
      response = await client.get(url, { payment: authorization });
    } catch (error) {
      if (error instanceof InsufficientFundsError) {
        console.log('Insufficient funds');
      } else if (error instanceof PaymentExpiredError) {
        console.log('Payment expired');
      } else {
        throw error;
      }
    }
  }

  return response.data;
} catch (error) {
  console.error('Error:', error);
  throw error;
} finally {
  await client.close();
}

URL Validation

The client validates URLs to prevent SSRF attacks:

  • Only allows http:// and https:// schemes

  • Blocks localhost and private IPs (unless allowLocal is enabled)

For local development:

const client = new X402Client(
  wallet,
  rpcUrl,
  undefined,
  true // allowLocal - only for development!
);

Complete Example

import { X402Client } from '@shade402/client';
import { Keypair } from '@solana/web3.js';
import {
  PaymentExpiredError,
  InsufficientFundsError,
} from '@shade402/core';

async function fetchWithExplicitPayment(url: string) {
  const wallet = Keypair.generate();
  const client = new X402Client(wallet, process.env.SOLANA_RPC_URL);

  try {
    // Step 1: Initial request
    let response = await client.get(url);

    // Step 2: Check if payment required
    if (!client.paymentRequired(response)) {
      return response.data; // No payment needed
    }

    // Step 3: Parse payment request
    const paymentRequest = client.parsePaymentRequest(response);

    // Step 4: Validate
    if (paymentRequest.isExpired()) {
      throw new PaymentExpiredError(paymentRequest);
    }

    // Step 5: Create payment
    const authorization = await client.createPayment(paymentRequest);

    // Step 6: Encrypt resource (optional)
    let encryptedResource: string | undefined;
    try {
      encryptedResource = client.encryptResource(paymentRequest.resource);
    } catch (error) {
      // Continue without encryption
    }

    // Step 7: Retry with payment
    response = await client.get(url, {
      payment: authorization,
      encryptedResource,
    });

    return response.data;
  } catch (error) {
    if (error instanceof InsufficientFundsError) {
      console.error('Insufficient funds:', error.requiredAmount);
    } else if (error instanceof PaymentExpiredError) {
      console.error('Payment expired');
    } else {
      console.error('Unexpected error:', error);
    }
    throw error;
  } finally {
    await client.close();
  }
}

Best Practices

  1. Always close the client when done

  2. Check payment expiration before paying

  3. Validate payment amounts

  4. Handle errors appropriately

  5. Use resource encryption when available

  6. Never use allowLocal in production

  7. Store wallet keys securely

  8. Monitor payment transactions

Next Steps

Last updated