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:
Parses the payment requirement from the response
Resolves the spending context (note, witness, nullifier secret)
Builds the payment proof using ShieldedClientSDK
Retries the request with the X-Payment-Signature header
Returns the final response
Function Signature
import { createShieldedFetch } from '@shielded-x402/client' ;
const shieldedFetch = createShieldedFetch (
options : CreateShieldedFetchOptions
): ShieldedFetch
options
CreateShieldedFetchOptions
required
sdk
ShieldedPaymentBuilder
required
ShieldedClientSDK instance (or any object with prepare402Payment method)
Function to resolve spending context for the payment async ( input : ResolveShieldedContextInput ) => Promise < ShieldedSpendContext >
The original fetch request
Parsed payment requirement from 402 response
Merkle witness for the note
Nullifier secret for the note
Custom requirement adapters (defaults to generic x402 v2 adapter)
Custom fetch implementation (defaults to global fetch)
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
Initial Request
The wrapped fetch sends the original request to the merchant API.
402 Response
If the server returns 402 PAYMENT-REQUIRED with an X-Payment-Required header, the wrapper captures it.
Parse Requirement
The payment requirement is parsed using the configured adapters.
Resolve Context
Your resolveContext function is called to select a note and provide spending credentials.
Build Proof
The SDK builds the payment proof (and generates ZK proof if proofProvider is configured).
Retry Request
The request is retried with the X-Payment-Signature header containing the proof.
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