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

The protocol uses frozen cryptographic parameters to ensure consistency across implementations. All parameters are defined in packages/shared-types/src/crypto-spec.ts.

Cryptographic Specification

Version

const CRYPTO_SPEC = {
  version: 'v0.1.0',
  // ...
};
The current protocol version is v0.1.0. All implementations must use identical parameters.

Hash Function

Primitive

hashFunction: 'keccak256'
All hashing operations use Keccak-256 (not SHA3-256). This is the same hash function used by Ethereum.

Properties

  • Output size: 32 bytes (256 bits)
  • Security level: 128-bit collision resistance
  • Standard: FIPS 202 (Keccak)

Usage

  • Note commitments
  • Nullifier derivation
  • Challenge hash computation
  • Merkle tree construction
  • Domain separation

Merkle Tree

Depth

merkleTreeDepth: 24
The note commitment tree has a fixed depth of 24 levels.

Capacity

  • Maximum leaves: 2^24 = 16,777,216 notes
  • Path length: 24 siblings (768 bytes)

Structure

                    Root (Level 0)
                   /    \
                  /      \
            Level 1    Level 1
               /  \        /  \
             ...  ...    ...  ...
            /                    \
    Level 23                  Level 23
     /    \                    /    \
Leaf 0  Leaf 1  ...  Leaf 16777215

Merkle Path

Proof of inclusion requires 24 sibling hashes:
interface MerklePath {
  siblings: Hex[]; // 24 x 32-byte hashes
  leafIndex: number; // 0 to 16777215
}

Note Encoding

Scheme

noteEncoding: 'abi-packed-v1'
Notes are encoded using ABI-packed-v1 format for commitment computation.

Structure

interface NotePreimage {
  amount: bigint;    // 32 bytes (uint256)
  rho: Hex;         // 32 bytes (bytes32)
  pkHash: Hex;      // 32 bytes (bytes32)
}

Encoding

ABI-packed encoding concatenates values without padding:
encoded = abi.encodePacked(amount, rho, pkHash)
// Total: 96 bytes

Commitment

commitment = keccak256(abi.encodePacked(amount, rho, pkHash))

Nullifier Derivation

Formula

nullifierDerivation: 'keccak256(nullifierSecret, noteCommitment)'
Nullifiers are derived by hashing the secret and commitment:
nullifier = keccak256(nullifierSecret, noteCommitment)

Inputs

nullifierSecret
Hex
required
Private 32-byte secret known only to note owner
noteCommitment
Hex
required
Public 32-byte commitment to the note

Properties

  • Deterministic: Same inputs always produce same nullifier
  • One-way: Cannot reverse to find secret
  • Collision-resistant: Extremely unlikely to have duplicates

Output Commitments

Merchant Commitment

merchantCommitmentDerivation: 'keccak256(payAmount, merchantRho, merchantPkHash)'
Commitment to the merchant’s output note:
merchantCommitment = keccak256(
  toHexWord(payAmount),
  merchantRho,
  merchantPkHash
)

Change Commitment

changeCommitmentDerivation: 'keccak256(changeAmount, changeRho, changePkHash)'
Commitment to the sender’s change note:
changeCommitment = keccak256(
  toHexWord(changeAmount),
  changeRho,
  changePkHash
)

Domain Separators

Domain separators prevent cross-protocol and cross-context attacks.

Challenge Domain

challengeDomainSeparator: 'shielded-x402:v1:challenge'
challengeDomainHash: '0xe32e24a51c351093d339c0035177dc2da5c1b8b9563e414393edd75506dcc055'
Used for binding payments to merchant challenges. Domain hash is keccak256("shielded-x402:v1:challenge").

Commitment Domain

commitmentDomainSeparator: 'shielded-x402:v1:commitment'
Used for note commitment construction. Applied implicitly through the note encoding scheme.

Output Domain

outputDomainSeparator: 'shielded-x402:v1:output'
Used for merchant and change output commitments.

Nullifier Domain

nullifierDomainSeparator: 'shielded-x402:v1:nullifier'
Used for nullifier derivation to prevent cross-protocol nullifier reuse.

Utility Functions

Hex Word Conversion

Convert values to 32-byte hex words:
function toHexWord(value: bigint | number | string): Hex {
  const normalized = BigInt(value);
  if (normalized < 0n) {
    throw new Error('toHexWord expects a non-negative value');
  }
  return `0x${normalized.toString(16).padStart(64, '0')}`;
}
Example:
toHexWord(1000000n)
// "0x00000000000000000000000000000000000000000000000000000000000f4240"

Address Padding

Pad Ethereum addresses to 32-byte words:
function padAddressToWord(address: Hex): Hex {
  const stripped = address.toLowerCase().replace(/^0x/, '');
  if (stripped.length > 40) {
    throw new Error('address exceeds 20 bytes');
  }
  return `0x${stripped.padStart(64, '0')}`;
}
Example:
padAddressToWord("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb")
// "0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb"

Hex Validation

General Hex

Validate hex strings of any length:
function isHex(value: unknown): value is Hex {
  return typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value);
}

32-Byte Hex

Validate exactly 32-byte (64 hex character) values:
function isHex32(value: unknown): value is Hex {
  return typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value);
}

Normalization

Normalize hex strings to canonical format:
function normalizeHex(value: string): Hex {
  const trimmed = value.trim().toLowerCase();
  if (trimmed.startsWith('0x')) {
    return trimmed as Hex;
  }
  return `0x${trimmed}` as Hex;
}

Validation

Payment Validation

The protocol provides validation for shielded payment responses:
interface ShieldedPaymentValidationOptions {
  exactPublicInputsLength?: number;
  minPublicInputsLength?: number;
  maxProofHexLength?: number;
}

function validateShieldedPaymentResponseShape(
  payload: unknown,
  options?: ShieldedPaymentValidationOptions
): string | undefined;
Returns: undefined if valid, error message string if invalid.

Validation Checks

  • proof: Must be valid hex
  • publicInputs: Array of hex values
  • nullifier: Must be exactly 32 bytes
  • root: Must be exactly 32 bytes
  • merchantCommitment: Must be exactly 32 bytes
  • changeCommitment: Must be exactly 32 bytes
  • challengeHash: Must be exactly 32 bytes
  • encryptedReceipt: Must be valid hex

Example

const error = validateShieldedPaymentResponseShape(payment, {
  exactPublicInputsLength: 6,
  maxProofHexLength: 2000
});

if (error) {
  console.error('Invalid payment:', error);
} else {
  console.log('Payment is valid');
}

Security Considerations

Parameter Freeze

All cryptographic parameters are frozen for the MVP. Changing parameters would:
  • Break compatibility with existing deployments
  • Invalidate existing proofs
  • Require new smart contract deployments

Domain Separation

Domain separators prevent:
  • Cross-protocol attacks
  • Signature replay across contexts
  • Hash collision attacks

Random Values

All random values (rho, nullifierSecret) must use cryptographically secure randomness:
import { randomBytes } from 'crypto';

const rho = `0x${randomBytes(32).toString('hex')}` as Hex;
const nullifierSecret = `0x${randomBytes(32).toString('hex')}` as Hex;

Constant-Time Operations

Secret values should be compared using constant-time operations to prevent timing attacks:
import { timingSafeEqual } from 'crypto';

const equal = timingSafeEqual(
  Buffer.from(secret1.slice(2), 'hex'),
  Buffer.from(secret2.slice(2), 'hex')
);

Build docs developers (and LLMs) love