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
The Client SDK provides utilities for managing shielded notes locally, including indexing commitments, tracking notes, and encrypting note data for storage or transmission.
LocalNoteIndexer
LocalNoteIndexer is an in-memory note indexer for tracking shielded deposits and spends.
Constructor
import { LocalNoteIndexer } from '@shielded-x402/client' ;
const noteStore = new LocalNoteIndexer ();
Methods
ingestDeposit
Record a new deposit event and store the associated note.
noteStore . ingestDeposit (
event : DepositEvent ,
note : ShieldedNote
): void
The full note details (amount, rho, pkHash, commitment, leafIndex)
Example
const { note , leafIndex } = await sdk . deposit ( 100_000_000 n , ownerPkHash );
noteStore . ingestDeposit (
{
commitment: note . commitment ,
leafIndex ,
amount: note . amount
},
note
);
ingestSpend
Record a spend event, adding merchant and change commitments to the tree.
noteStore . ingestSpend (
event : SpendEvent
): void
Commitment for the merchant’s note
Commitment for the change note
Example
const prepared = await sdk . prepare402Payment (
requirement ,
note ,
witness ,
nullifierSecret
);
noteStore . ingestSpend ({
merchantCommitment: prepared . response . merchantCommitment ,
changeCommitment: prepared . response . changeCommitment
});
// Also store the change note for future spending
const changeNote = prepared . changeNote ;
noteStore . getNotes (). push ( changeNote );
getCommitments
Retrieve all commitments in tree order.
const commitments = noteStore . getCommitments (): Hex []
Array of commitment hashes in the order they were added to the tree
Example
import { buildWitnessFromCommitments } from '@shielded-x402/client' ;
const commitments = noteStore . getCommitments ();
const witness = buildWitnessFromCommitments ( commitments , note . leafIndex );
getNotes
Retrieve all stored notes.
const notes = noteStore . getNotes (): ShieldedNote []
Array of all shielded notes stored in the indexer
Example
const notes = noteStore . getNotes ();
const totalBalance = notes . reduce (( sum , n ) => sum + n . amount , 0 n );
console . log ( 'Total balance:' , totalBalance );
console . log ( 'Note count:' , notes . length );
// Find spendable note
const spendableNote = notes . find ( n => n . amount >= 50_000_000 n );
Note Encryption
The SDK provides utilities for encrypting and decrypting notes for secure storage or transmission.
Symmetric Encryption
encryptNoteSymmetric
Encrypt a note using AES-256-GCM with a symmetric key.
import { encryptNoteSymmetric } from '@shielded-x402/client' ;
const ciphertext = encryptNoteSymmetric (
note : ShieldedNote ,
key : Buffer
): Hex
Encrypted note (includes IV, auth tag, and ciphertext)
Example
import { randomBytes } from 'node:crypto' ;
const aesKey = randomBytes ( 32 );
const encrypted = encryptNoteSymmetric ( note , aesKey );
// Store encrypted note
await db . storeEncryptedNote ( note . commitment , encrypted );
decryptNoteSymmetric
Decrypt a note encrypted with encryptNoteSymmetric.
import { decryptNoteSymmetric } from '@shielded-x402/client' ;
const note = decryptNoteSymmetric (
ciphertext : Hex ,
key : Buffer
): ShieldedNote
32-byte AES key used for encryption
Example
const encrypted = await db . getEncryptedNote ( commitment );
const note = decryptNoteSymmetric ( encrypted , aesKey );
console . log ( 'Decrypted note:' , note . amount );
Public Key Encryption
generateNoteEncryptionKeyPair
Generate an ECDH key pair for note encryption.
import { generateNoteEncryptionKeyPair } from '@shielded-x402/client' ;
const keyPair = generateNoteEncryptionKeyPair (): NoteEncryptionKeyPair
ECDH private key (secp256k1)
ECDH public key (secp256k1, uncompressed)
Example
const keyPair = generateNoteEncryptionKeyPair ();
console . log ( 'Public key:' , keyPair . publicKey );
// Store private key securely
await wallet . storeKey ( 'note-encryption-key' , keyPair . privateKey );
encryptNoteForPublicKey
Encrypt a note for a recipient’s public key using ECIES.
import { encryptNoteForPublicKey } from '@shielded-x402/client' ;
const envelope = encryptNoteForPublicKey (
note : ShieldedNote ,
recipientPublicKey : Hex ,
aad ?: Uint8Array
): Hex
Recipient’s ECDH public key
Optional additional authenticated data
Encrypted envelope (version + ephemeral pubkey + IV + auth tag + ciphertext)
Example
const merchantPubKey = '0x04...' ; // Merchant's public key
const encryptedReceipt = encryptNoteForPublicKey (
merchantNote ,
merchantPubKey
);
// Send encrypted receipt to merchant
await sendToMerchant ( encryptedReceipt );
decryptNoteWithPrivateKey
Decrypt a note encrypted with encryptNoteForPublicKey.
import { decryptNoteWithPrivateKey } from '@shielded-x402/client' ;
const note = decryptNoteWithPrivateKey (
envelopeCiphertext : Hex ,
recipientPrivateKey : Hex ,
aad ?: Uint8Array
): ShieldedNote
Encrypted envelope from encryptNoteForPublicKey
Recipient’s ECDH private key
Additional authenticated data (must match encryption AAD)
Example
const merchantPrivateKey = await wallet . getKey ( 'note-encryption-key' );
const note = decryptNoteWithPrivateKey (
encryptedReceipt ,
merchantPrivateKey
);
console . log ( 'Received note:' , note . amount );
Serialization
serializeNote
Serialize a note to JSON string.
import { serializeNote } from '@shielded-x402/client' ;
const json = serializeNote ( note : ShieldedNote ): string
JSON string representation
Example
const json = serializeNote ( note );
await fs . writeFile ( 'note.json' , json );
deserializeNote
Deserialize a JSON string to a note.
import { deserializeNote } from '@shielded-x402/client' ;
const note = deserializeNote ( value : string ): ShieldedNote
JSON string from serializeNote
Example
const json = await fs . readFile ( 'note.json' , 'utf8' );
const note = deserializeNote ( json );
console . log ( 'Loaded note:' , note . commitment );
Complete Example
import {
ShieldedClientSDK ,
LocalNoteIndexer ,
buildWitnessFromCommitments ,
encryptNoteSymmetric ,
decryptNoteSymmetric
} from '@shielded-x402/client' ;
import { randomBytes } from 'node:crypto' ;
// Initialize SDK and indexer
const sdk = new ShieldedClientSDK ({
endpoint: RELAYER_URL ,
signer : async ( message ) => account . signMessage ({ message })
});
const noteStore = new LocalNoteIndexer ();
const nullifierSecrets = new Map < string , Hex >();
const encryptionKey = randomBytes ( 32 );
// Deposit
const { note , leafIndex } = await sdk . deposit ( 100_000_000 n , ownerPkHash );
noteStore . ingestDeposit (
{ commitment: note . commitment , leafIndex , amount: note . amount },
note
);
const nullifierSecret = '0x...' ;
nullifierSecrets . set ( note . commitment , nullifierSecret );
// Encrypt and store
const encrypted = encryptNoteSymmetric ( note , encryptionKey );
await db . store ( note . commitment , encrypted );
// Later: spend
const notes = noteStore . getNotes ();
const spendableNote = notes . find ( n => n . amount >= 40_000_000 n );
if ( spendableNote ) {
const commitments = noteStore . getCommitments ();
const witness = buildWitnessFromCommitments ( commitments , spendableNote . leafIndex );
const secret = nullifierSecrets . get ( spendableNote . commitment );
const prepared = await sdk . prepare402Payment (
requirement ,
spendableNote ,
witness ,
secret
);
// Track spend
noteStore . ingestSpend ({
merchantCommitment: prepared . response . merchantCommitment ,
changeCommitment: prepared . response . changeCommitment
});
// Store change note
const changeNote = prepared . changeNote ;
noteStore . getNotes (). push ( changeNote );
nullifierSecrets . set ( changeNote . commitment , prepared . changeNullifierSecret );
const encryptedChange = encryptNoteSymmetric ( changeNote , encryptionKey );
await db . store ( changeNote . commitment , encryptedChange );
}
// Query balance
const allNotes = noteStore . getNotes ();
const totalBalance = allNotes . reduce (( sum , n ) => sum + n . amount , 0 n );
console . log ( 'Total balance:' , totalBalance );
Best Practices
LocalNoteIndexer stores notes in memory only. Persist to disk for production:class PersistentNoteIndexer extends LocalNoteIndexer {
async ingestDeposit ( event : DepositEvent , note : ShieldedNote ) {
super . ingestDeposit ( event , note );
await this . saveToDb ();
}
async ingestSpend ( event : SpendEvent ) {
super . ingestSpend ( event );
await this . saveToDb ();
}
private async saveToDb () {
const state = {
commitments: this . getCommitments (),
notes: this . getNotes ()
};
await db . saveNoteState ( state );
}
}
Encrypt Nullifier Secrets
Nullifier secrets must be kept secret. Always encrypt them: import { scryptSync } from 'node:crypto' ;
const password = await promptUserPassword ();
const key = scryptSync ( password , 'salt' , 32 );
const encryptedSecret = encryptNoteSymmetric (
{ ... note , rho: nullifierSecret },
key
);
Mark notes as spent to avoid double-spend attempts: interface TrackedNote extends ShieldedNote {
spent : boolean ;
spentAt ?: Date ;
}
const notes : TrackedNote [] = [];
async function spendNote ( note : TrackedNote ) {
if ( note . spent ) {
throw new Error ( 'Note already spent' );
}
// ... perform spend
note . spent = true ;
note . spentAt = new Date ();
}
For O(1) lookups, index notes by commitment: const notesByCommitment = new Map < string , ShieldedNote >();
noteStore . getNotes (). forEach ( note => {
notesByCommitment . set ( note . commitment , note );
});
const note = notesByCommitment . get ( commitment );
Type Definitions
DepositEvent
interface DepositEvent {
commitment : Hex ; // Note commitment
leafIndex : number ; // Position in Merkle tree
amount : bigint ; // Deposit amount
}
SpendEvent
interface SpendEvent {
merchantCommitment : Hex ; // Merchant's note commitment
changeCommitment : Hex ; // Spender's change note commitment
}
NoteState
interface NoteState {
commitments : Hex []; // All commitments in tree order
notes : ShieldedNote []; // All tracked notes
}
StoredNote
interface StoredNote {
note : ShieldedNote ; // The note
ciphertext ?: Hex ; // Optional encrypted form
}
See Also