Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nhestrompia/shielded-x402/llms.txt

Use this file to discover all available pages before exploring further.

Overview

createShieldedFetch creates a drop-in replacement for the standard fetch API that automatically handles 402 Payment Required responses. When a merchant returns 402 PAYMENT-REQUIRED, the shielded fetch wrapper:
  1. Parses the payment requirement from the response
  2. Resolves the spending context (note, witness, nullifier secret)
  3. Builds the payment proof using ShieldedClientSDK
  4. Retries the request with the X-Payment-Signature header
  5. Returns the final response

Function Signature

import { createShieldedFetch } from '@shielded-x402/client';

const shieldedFetch = createShieldedFetch(
  options: CreateShieldedFetchOptions
): ShieldedFetch
options
CreateShieldedFetchOptions
required
ShieldedFetch
function
A fetch-compatible function
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>

Basic Usage

import {
  ShieldedClientSDK,
  LocalNoteIndexer,
  buildWitnessFromCommitments,
  createShieldedFetch
} from '@shielded-x402/client';

const sdk = new ShieldedClientSDK({
  endpoint: RELAYER_URL,
  signer: async (message) => account.signMessage({ message })
});

const noteStore = new LocalNoteIndexer();
const nullifierSecretsByCommitment = new Map<string, Hex>();

const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext: async ({ requirement }) => {
    // Find a note with sufficient balance
    const note = noteStore.getNotes().find(
      (n) => n.amount >= BigInt(requirement.amount)
    );
    if (!note) {
      throw new Error('no spendable note with sufficient balance');
    }

    // Build Merkle witness
    const commitments = noteStore.getCommitments();
    const witness = buildWitnessFromCommitments(commitments, note.leafIndex);

    // Retrieve nullifier secret
    const nullifierSecret = nullifierSecretsByCommitment.get(note.commitment);
    if (!nullifierSecret) {
      throw new Error('missing nullifier secret for selected note');
    }

    return { note, witness, nullifierSecret };
  }
});

// Use like regular fetch - 402 payments are handled automatically
const response = await shieldedFetch('https://api.example.com/paid/data');
const data = await response.json();
console.log(data);

Advanced Examples

With Proof Provider

Generate real zero-knowledge proofs for production use:
import { createProofProvider } from '@shielded-x402/client';

const proofProvider = await createProofProvider();

const sdk = new ShieldedClientSDK({
  endpoint: RELAYER_URL,
  signer: async (message) => account.signMessage({ message }),
  proofProvider // Enable real ZK proofs
});

const shieldedFetch = createShieldedFetch({ sdk, resolveContext });

const response = await shieldedFetch('https://api.example.com/premium-ai');

Custom Note Selection

Implement custom logic for selecting which note to spend:
const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext: async ({ requirement, request }) => {
    const requiredAmount = BigInt(requirement.amount);
    
    // Find smallest note that covers the payment (minimize change)
    const notes = noteStore.getNotes()
      .filter(n => n.amount >= requiredAmount)
      .sort((a, b) => Number(a.amount - b.amount));
    
    const note = notes[0];
    if (!note) {
      throw new Error(`no note with balance >= ${requiredAmount}`);
    }
    
    console.log(
      `Selected note with ${note.amount}, change will be ${note.amount - requiredAmount}`
    );
    
    const commitments = noteStore.getCommitments();
    const witness = buildWitnessFromCommitments(commitments, note.leafIndex);
    const nullifierSecret = nullifierSecretsByCommitment.get(note.commitment);
    
    return { note, witness, nullifierSecret };
  }
});

Track Change Notes

Store change notes returned from payments:
const sdk = new ShieldedClientSDK({
  endpoint: RELAYER_URL,
  signer: async (message) => account.signMessage({ message })
});

const shieldedFetch = createShieldedFetch({
  sdk: {
    prepare402Payment: async (requirement, note, witness, nullifierSecret, baseHeaders) => {
      const prepared = await sdk.prepare402Payment(
        requirement,
        note,
        witness,
        nullifierSecret,
        baseHeaders
      );
      
      // Store the change note
      noteStore.ingestSpend({
        merchantCommitment: prepared.response.merchantCommitment,
        changeCommitment: prepared.response.changeCommitment
      });
      
      // Store change note's nullifier secret
      nullifierSecretsByCommitment.set(
        prepared.changeNote.commitment,
        prepared.changeNullifierSecret
      );
      
      console.log('Change note created:', prepared.changeNote.amount);
      
      return prepared;
    }
  },
  resolveContext: async ({ requirement }) => {
    const note = noteStore.getNotes().find(
      (n) => n.amount >= BigInt(requirement.amount)
    );
    if (!note) throw new Error('no spendable note');
    
    const commitments = noteStore.getCommitments();
    const witness = buildWitnessFromCommitments(commitments, note.leafIndex);
    const nullifierSecret = nullifierSecretsByCommitment.get(note.commitment);
    if (!nullifierSecret) throw new Error('missing nullifier secret');
    
    return { note, witness, nullifierSecret };
  }
});

Custom Requirement Adapters

Handle non-standard 402 response formats:
import { createGenericX402V2Adapter } from '@shielded-x402/client';

const customAdapter = {
  canAdapt: (response: Response) => {
    return response.headers.has('X-Custom-Payment-Required');
  },
  parseRequirement: async (response: Response, context: any) => {
    const customHeader = response.headers.get('X-Custom-Payment-Required');
    const parsed = JSON.parse(customHeader);
    
    return {
      scheme: 'exact',
      network: parsed.network,
      asset: parsed.asset,
      payTo: parsed.payTo,
      rail: 'shielded-usdc',
      amount: parsed.amount,
      challengeNonce: parsed.nonce,
      challengeExpiry: String(Date.now() + 60000),
      merchantPubKey: parsed.merchantPubKey,
      verifyingContract: parsed.payTo
    };
  },
  rewriteHeaders: (headers: Headers, context: any) => {
    // Custom header transformation
    return headers;
  }
};

const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext,
  adapters: [customAdapter, createGenericX402V2Adapter()]
});

With Custom Fetch Implementation

Use a custom fetch implementation (e.g., for testing or proxying):
import nodeFetch from 'node-fetch';

const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext,
  fetchFn: nodeFetch as unknown as typeof fetch
});

Request Flow

1

Initial Request

The wrapped fetch sends the original request to the merchant API.
2

402 Response

If the server returns 402 PAYMENT-REQUIRED with an X-Payment-Required header, the wrapper captures it.
3

Parse Requirement

The payment requirement is parsed using the configured adapters.
4

Resolve Context

Your resolveContext function is called to select a note and provide spending credentials.
5

Build Proof

The SDK builds the payment proof (and generates ZK proof if proofProvider is configured).
6

Retry Request

The request is retried with the X-Payment-Signature header containing the proof.
7

Return Response

The final response (successful or error) is returned to the caller.

Error Handling

const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext: async ({ requirement, request }) => {
    try {
      const note = noteStore.getNotes().find(
        (n) => n.amount >= BigInt(requirement.amount)
      );
      
      if (!note) {
        throw new Error(
          `Insufficient balance: need ${requirement.amount}, have ${noteStore.getTotalBalance()}`
        );
      }
      
      const commitments = noteStore.getCommitments();
      const witness = buildWitnessFromCommitments(commitments, note.leafIndex);
      const nullifierSecret = nullifierSecretsByCommitment.get(note.commitment);
      
      if (!nullifierSecret) {
        throw new Error(`Missing nullifier secret for note ${note.commitment}`);
      }
      
      return { note, witness, nullifierSecret };
    } catch (error) {
      console.error('Failed to resolve payment context:', error);
      throw error;
    }
  }
});

try {
  const response = await shieldedFetch('https://api.example.com/paid/data');
  if (!response.ok) {
    console.error(`Request failed: ${response.status} ${response.statusText}`);
  }
  const data = await response.json();
} catch (error) {
  console.error('Payment or request failed:', error);
}

Best Practices

Always verify the selected note has sufficient balance before building the proof:
const requiredAmount = BigInt(requirement.amount);
if (note.amount < requiredAmount) {
  throw new Error(
    `Selected note has insufficient balance: ${note.amount} < ${requiredAmount}`
  );
}
The wrapper only intercepts 402 responses. All other responses (including errors) are returned as-is:
const response = await shieldedFetch(url);
if (response.status === 401) {
  console.log('Authentication required');
} else if (response.status === 403) {
  console.log('Forbidden');
} else if (!response.ok) {
  console.log('Other error:', response.status);
}
For performance, consider caching Merkle witnesses if commitments don’t change frequently:
const witnessCache = new Map<string, MerkleWitness>();

const getWitness = (note: ShieldedNote) => {
  const cached = witnessCache.get(note.commitment);
  if (cached) return cached;
  
  const commitments = noteStore.getCommitments();
  const witness = buildWitnessFromCommitments(commitments, note.leafIndex);
  witnessCache.set(note.commitment, witness);
  return witness;
};
Track payments for debugging and accounting:
const shieldedFetch = createShieldedFetch({
  sdk,
  resolveContext: async ({ requirement, request }) => {
    console.log('Payment required:', {
      url: request.url,
      amount: requirement.amount,
      merchant: requirement.verifyingContract
    });
    
    const context = resolveSpendingContext(requirement);
    
    console.log('Spending note:', {
      commitment: context.note.commitment,
      amount: context.note.amount,
      change: context.note.amount - BigInt(requirement.amount)
    });
    
    return context;
  }
});

Type Definitions

ShieldedSpendContext

interface ShieldedSpendContext {
  note: ShieldedNote;           // Note to spend
  witness: MerkleWitness;       // Merkle proof of inclusion
  nullifierSecret: Hex;         // Secret to derive nullifier
}

ResolveShieldedContextInput

interface ResolveShieldedContextInput {
  request: Request;                      // Original fetch request
  requirement: PaymentRequirement;       // Parsed payment requirement
  paymentRequiredResponse: Response;     // The 402 response
}

RequirementAdapter

interface RequirementAdapter {
  canAdapt: (response: Response) => boolean | Promise<boolean>;
  parseRequirement: (
    response: Response,
    context: any
  ) => Promise<PaymentRequirement>;
  rewriteHeaders?: (headers: Headers, context: any) => Headers;
}

See Also

Build docs developers (and LLMs) love