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 Solana integration provides a complete on-chain payment gateway with zero-knowledge proof verification. It uses a custom Solana program (x402_gateway) that integrates with a Groth16 verifier deployed via Sunspot.
Architecture
The Solana integration consists of three main components:
SMT Exclusion Circuit : Noir circuit proving a payer is not blacklisted
x402_gateway Program : Native Solana program handling payment authorization and execution
Client Adapter : TypeScript helpers for relayer-side transaction submission
Directory Structure
chains/solana/
├── circuits/smt_exclusion/ # Noir ZK circuit
│ ├── src/main.nr # Circuit source
│ ├── Nargo.toml # Noir config
│ └── target/ # Generated proofs and witnesses
├── programs/x402_gateway/ # Solana program
│ ├── src/lib.rs # Gateway implementation
│ └── Cargo.toml # Rust dependencies
├── client/ # TypeScript adapter
│ ├── adapter.ts # Transaction helpers
│ └── encoding.ts # Data encoding utilities
└── scripts/ # Deployment scripts
├── deploy-verifier.sh
├── deploy-gateway.sh
└── init-gateway.ts
Gateway Program
The x402_gateway program is a native Solana program that manages payment authorization and settlement.
Program ID
solana_program::declare_id!("6F2rv4dbwJ7A3F9Q8NpL6X2kYQ6Zxj2Y8ywmupfHP2aG");
This is the default program ID. When you deploy, you’ll get a new program ID specific to your deployment.
State Account
The gateway uses a Program Derived Address (PDA) to store state:
const STATE_SIZE : usize = 8 + 32 + 32 + 32 ; // 104 bytes
// State structure:
// - discriminator: [u8; 8] ("x402_smt")
// - admin: Pubkey (32 bytes)
// - smt_root: [u8; 32] (32 bytes)
// - verifier_program: Pubkey (32 bytes)
Instructions
The gateway supports three instructions:
1. Initialize State
pub const INITIALIZE_STATE : u8 = 0 ;
Creates the state PDA and sets the verifier program ID.
Accounts:
[signer, writable] admin
[writable] state_account (PDA)
[] system_program
Data:
verifier_program_id: [u8; 32]
2. Set SMT Root
pub const SET_SMT_ROOT : u8 = 1 ;
Updates the Sparse Merkle Tree root used for exclusion proofs.
Accounts:
[signer] admin
[writable] state_account (PDA)
Data:
3. Pay Authorized
pub const PAY_AUTHORIZED : u8 = 2 ;
Executes a payment after verifying the ZK proof.
Accounts:
[signer, writable] payer
[writable] recipient
[] state_account
[] verifier_program
[] system_program
Data (512 bytes total):
auth_id: [u8; 32] - Authorization ID
amount_lamports: u64 - Transfer amount
auth_expiry_unix: u64 - Expiration timestamp
proof: [u8; 388] - Groth16 proof
public_witness: [u8; 76] - Public inputs
Error Codes
pub enum GatewayError {
InvalidDataLength = 0 ,
InvalidStateAccount = 1 ,
SmtRootMismatch = 2 ,
InvalidStatePda = 3 ,
InvalidZkVerifier = 4 ,
AuthorizationExpired = 5 ,
}
ZK Circuit
The SMT exclusion circuit proves that a payer is not on the blacklist.
Circuit Location
chains/solana/circuits/smt_exclusion/
├── src/main.nr # Noir circuit source
├── Nargo.toml # Circuit configuration
├── Prover.toml # Prover inputs
└── target/ # Build artifacts
├── smt_exclusion.proof # Groth16 proof (388 bytes)
├── smt_exclusion.pw # Public witness (76 bytes)
├── smt_exclusion.vk # Verification key
└── smt_exclusion.json # Circuit JSON
The circuit generates:
Proof : 388 bytes (Groth16 proof for Solana)
Public Witness : 76 bytes containing:
Bytes 12-44: SMT root (32 bytes)
Other public inputs
const SOLANA_PROOF_LEN = 388;
const SOLANA_WITNESS_LEN = 76;
Deployment
Prerequisites
Solana CLI installed and configured
Anchor framework (optional, for development)
Noir toolchain (nargo, sunspot)
Node.js and pnpm
Solana wallet with devnet SOL
Environment Variables
# Required
export SOLANA_DEPLOYER_KEYPAIR = "path/to/deployer-keypair.json"
export SOLANA_CLUSTER = "devnet"
export SOLANA_ADMIN_KEYPAIR_PATH = "path/to/admin-keypair.json"
export GNARK_VERIFIER_BIN = "path/to/sunspot/verifier-bin"
# Optional
export SOLANA_RPC_URL = "https://api.devnet.solana.com"
export SOLANA_WS_URL = "wss://api.devnet.solana.com"
Step 1: Install Dependencies
pnpm --dir chains/solana/client install
Step 2: Deploy Verifier
Deploy the Groth16 verifier program using Sunspot:
pnpm solana:deploy:verifier
# or directly:
bash chains/solana/scripts/deploy-verifier.sh
This will output:
Verifier program deployed: VerifyProgramID123...
Save this program ID for the next step.
Step 3: Deploy Gateway
pnpm solana:deploy:gateway
# or:
bash chains/solana/scripts/deploy-gateway.sh
Output:
Gateway program deployed: GatewayProgramID456...
Step 4: Initialize Gateway
Set the verifier program ID and SMT root:
export SOLANA_GATEWAY_PROGRAM_ID = "GatewayProgramID456..."
export SOLANA_VERIFIER_PROGRAM_ID = "VerifyProgramID123..."
pnpm solana:init:gateway
# or:
pnpm tsx chains/solana/scripts/init-gateway.ts
The script will:
Derive the state PDA automatically
Create and initialize the state account
Set the SMT root from:
SOLANA_SMT_ROOT_HEX environment variable, or
Extract from chains/solana/circuits/smt_exclusion/target/smt_exclusion.pw
Output:
[solana] gateway init inputs
rpcUrl=https://api.devnet.solana.com
gatewayProgramId=Gateway...
verifierProgramId=Verifier...
adminAddress=Admin...
stateAccount=State...
smtRootHex=0x1234...
[solana] InitializeState tx=5YNmS3...
[solana] SetSmtRoot tx=3kQpA7...
[solana] ready state account: StateAccount...
Save the stateAccount address for relayer configuration.
Client Adapter
The TypeScript adapter provides three functions for interacting with the gateway:
1. submitInitializeState
import { submitInitializeState } from './chains/solana/client/adapter' ;
const result = await submitInitializeState ({
rpcUrl: 'https://api.devnet.solana.com' ,
wsUrl: 'wss://api.devnet.solana.com' ,
gatewayProgramId: 'Gateway...' as Address ,
verifierProgramId: 'Verifier...' as Address ,
stateAccount: 'State...' as Address ,
payerKeypairPath: './admin-keypair.json'
});
console . log ( 'Initialized:' , result . txSignature );
2. submitSetSmtRoot
import { submitSetSmtRoot } from './chains/solana/client/adapter' ;
const result = await submitSetSmtRoot ({
rpcUrl: 'https://api.devnet.solana.com' ,
wsUrl: 'wss://api.devnet.solana.com' ,
gatewayProgramId: 'Gateway...' as Address ,
stateAccount: 'State...' as Address ,
smtRootHex: '0x1234567890abcdef...' as `0x ${ string } ` ,
payerKeypairPath: './admin-keypair.json'
});
console . log ( 'Root updated:' , result . txSignature );
3. submitPayAuthorized
import { submitPayAuthorized } from './chains/solana/client/adapter' ;
import fs from 'node:fs' ;
const proof = fs . readFileSync ( './target/smt_exclusion.proof' );
const publicWitness = fs . readFileSync ( './target/smt_exclusion.pw' );
const result = await submitPayAuthorized ({
rpcUrl: 'https://api.devnet.solana.com' ,
wsUrl: 'wss://api.devnet.solana.com' ,
gatewayProgramId: 'Gateway...' as Address ,
verifierProgramId: 'Verifier...' as Address ,
stateAccount: 'State...' as Address ,
recipient: 'Recipient...' as Address ,
amountLamports: 1_000_000 n , // 0.001 SOL
authIdHex: '0xabcdef...' as `0x ${ string } ` ,
authExpiryUnix: BigInt ( Math . floor ( Date . now () / 1000 ) + 300 ),
proof: proof ,
publicWitness: publicWitness ,
computeUnits: 1_000_000 , // Optional, defaults to 1M
payerKeypairPath: './payer-keypair.json'
});
console . log ( 'Payment executed:' , result . txSignature );
Data Encoding
The adapter uses little-endian encoding for Solana compatibility:
export function u64ToLeBytes(value: bigint): Uint8Array {
const out = new Uint8Array(8);
const view = new DataView(out.buffer);
view.setBigUint64(0, value, true); // true = little-endian
return out;
}
export function buildPayAuthorizedData(input: {
authIdHex: `0x${string}`;
amountLamports: bigint;
authExpiryUnix: bigint;
proof: Uint8Array;
publicWitness: Uint8Array;
}): Uint8Array {
const authId = authIdToBytes(input.authIdHex);
const amount = u64ToLeBytes(input.amountLamports);
const expiry = u64ToLeBytes(input.authExpiryUnix);
const out = new Uint8Array(32 + 8 + 8 + input.proof.length + input.publicWitness.length);
out.set(authId, 0);
out.set(amount, 32);
out.set(expiry, 40);
out.set(input.proof, 48);
out.set(input.publicWitness, 48 + input.proof.length);
return out;
}
Payment Relayer Configuration
Configure the Solana relayer with the deployed gateway:
# Chain configuration
export RELAYER_CHAIN_REF = "solana:devnet"
export RELAYER_PAYOUT_MODE = "solana"
# Gateway configuration
export SOLANA_RPC_URL = "https://api.devnet.solana.com"
export SOLANA_WS_URL = "wss://api.devnet.solana.com"
export SOLANA_GATEWAY_PROGRAM_ID = "Gateway..."
export SOLANA_VERIFIER_PROGRAM_ID = "Verifier..."
export SOLANA_STATE_ACCOUNT = "State..."
export SOLANA_PAYER_KEYPAIR_PATH = "./payer-keypair.json"
# Proof artifacts
export SOLANA_PROOF_PATH = "chains/solana/circuits/smt_exclusion/target/smt_exclusion.proof"
export SOLANA_PUBLIC_WITNESS_PATH = "chains/solana/circuits/smt_exclusion/target/smt_exclusion.pw"
# Optional
export SOLANA_COMPUTE_UNITS_LIMIT = "1000000"
Merchant Request Payload
When using the payment relayer for Solana, the merchant request body should contain:
const solanaRelayPayload : RelayPayRequestV1 = {
authorization: auth . authorization ,
sequencerSig: auth . sequencerSig ,
merchantRequest: {
url: 'https://merchant.example.com/pay' ,
method: 'POST' ,
headers: { 'content-type' : 'application/json' },
bodyBase64: Buffer . from (
JSON . stringify ({
rpcUrl: 'https://api.devnet.solana.com' ,
wsUrl: 'wss://api.devnet.solana.com' ,
gatewayProgramId: 'Gateway...' ,
verifierProgramId: 'Verifier...' ,
stateAccount: 'State...' ,
recipient: 'Recipient...' ,
amountLamports: '1000000' ,
computeUnits: 1000000 ,
authIdHex: auth . authorization . authId ,
authExpiryUnix: auth . authorization . expiresAt ,
proofBase64: proof . toString ( 'base64' ),
publicWitnessBase64: publicWitness . toString ( 'base64' ),
payerKeypairPath: './payer-keypair.json'
}),
'utf8'
). toString ( 'base64' )
}
};
Testing
Unit Tests
The gateway program includes comprehensive Rust tests:
pnpm solana:program:test
# or:
cd chains/solana/programs/x402_gateway && cargo test
Adapter Tests
Test the TypeScript encoding utilities:
pnpm solana:adapter:test
# or:
cd chains/solana/client && pnpm test
Integration Test
Run the full multi-chain example:
pnpm example:multi-chain:base-solana
This demonstrates:
Credit allocation
Authorization on both chains
Payment execution with real Solana transactions
Execution reporting
Verification Flow
The payment verification process:
Expiry Check : Verify auth_expiry_unix > current_timestamp
SMT Root Match : Compare witness SMT root (bytes 12-44) with stored root
ZK Verification : CPI into verifier program with proof + witness
Payment Execution : Transfer SOL from payer to recipient
let now = Clock::get()?.unix_timestamp;
if now > auth_expiry as i64 {
return Err(GatewayError::AuthorizationExpired.into());
}
let stored_smt_root = &state_data[40..72];
let witness_smt_root = &witness_data[12..44];
if witness_smt_root != stored_smt_root {
return Err(GatewayError::SmtRootMismatch.into());
}
let verify_ix = Instruction {
program_id: configured_verifier,
accounts: vec![],
data: verifier_data,
};
invoke(&verify_ix, &[])?;
invoke(
&system_instruction::transfer(payer.key, recipient.key, amount),
&[payer.clone(), recipient.clone(), system_program.clone()],
)?;
MVP Scope and Limitations
The current Solana integration is an MVP focused on payment execution. Some features are intentionally out of scope.
In Scope
✅ Native SOL transfers
✅ ZK proof verification
✅ Authorization expiry validation
✅ SMT root matching
✅ Transaction signature reporting
Out of Scope (MVP)
❌ Close authorization
❌ Challenge mechanism
❌ Finalization lifecycle
❌ SPL token transfers
❌ On-chain indexer integration
Troubleshooting
”InvalidStatePda”
The state account address doesn’t match the expected PDA. Verify:
solana find-program-derived-address \
$SOLANA_GATEWAY_PROGRAM_ID \
string:state \
pubkey: $ADMIN_ADDRESS \
--output json-compact
“SmtRootMismatch”
The SMT root in the public witness doesn’t match the stored root. Update the root:
export SOLANA_SMT_ROOT_HEX = "0x..."
pnpm tsx chains/solana/scripts/init-gateway.ts
“InvalidZkVerifier”
The verifier program ID in the state doesn’t match the provided account. Reinitialize with correct verifier ID.
”AuthorizationExpired”
The payment was submitted after the authorization expiry. Ensure:
Clock synchronization
Sufficient expiry window (recommended: 5+ minutes)
Timely submission
Compute Budget Exceeded
If transactions fail with compute budget errors:
export SOLANA_COMPUTE_UNITS_LIMIT = "1400000" # Maximum allowed
Next Steps
Multi-Chain Overview Understand the multi-chain architecture
Base/EVM Integration Add Base support alongside Solana