RelayerClient
IP privacy via relayer network
The RelayerClient enables IP privacy by submitting transactions through third-party relayer services. This breaks the link between your IP address and on-chain transactions while maintaining full cryptographic security.
Current Status: Implementation complete. Public relayer network launching in v0.2.0. Self-hosting available now.
Without relayers:
- ❌ Your IP address is exposed to Solana RPC nodes
- ❌ RPC providers can link your transactions
- ❌ Network observers can correlate IP → transaction patterns
With relayers:
- ✅ Your IP is hidden from the blockchain
- ✅ Relayers cannot steal funds (you control the proof)
- ✅ Relayers cannot modify transactions (would invalidate proof)
- ✅ Multiple relayers ensure censorship resistance
RelayerClient(endpoint: str = "https://relayer.veil.network")Parameters:
endpoint- Relayer endpoint URL (default: official Veil relayer)
Example:
from veil import RelayerClient
# Use default relayer
relayer = RelayerClient()
# Use custom relayer
relayer = RelayerClient(endpoint="https://my-relayer.example.com")
# Use multiple relayers (for redundancy)
relayers = [
RelayerClient(endpoint="https://r1.veil.network"),
RelayerClient(endpoint="https://r2.veil.network"),
RelayerClient(endpoint="https://r3.veil.network")
]estimate_fee()
Estimate the relayer fee for a transaction.
def estimate_fee(
operation: str,
amount: int
) -> intParameters:
operation- Operation type:"transfer"- Private transfer"unshield_sol"- Withdraw to SOL"unshield_token"- Withdraw to SPL token
amount- Transaction amount in lamports
Returns: Estimated fee in lamports
Fee Structure:
- Default: 30 basis points (0.3% of amount)
- Maximum: 500 basis points (5% of amount)
- Minimum withdrawal: 10,000 lamports (to cover fees)
- Formula:
(amount * fee_bps) / 10000
Example:
from veil import RelayerClient
relayer = RelayerClient()
# Estimate fee for 1 SOL transfer
fee = relayer.estimate_fee(
operation="transfer",
amount=1_000_000_000 # 1 SOL
)
print(f"Relayer fee: {fee} lamports ({fee / 1e9:.4f} SOL)")
# Output: Relayer fee: 3000000 lamports (0.0030 SOL)
# That's 0.3% of 1 SOL
# Estimate fee for unshield
unshield_fee = relayer.estimate_fee(
operation="unshield_sol",
amount=500_000_000 # 0.5 SOL
)
print(f"Unshield fee: {unshield_fee / 1e9:.4f} SOL")
# Output: Unshield fee: 0.0015 SOLNote: Actual fees may vary based on:
- Network congestion
- Relayer-specific pricing
- Operation complexity
submit_private_transfer()
Submit a private transfer transaction via relayer (hides your IP address).
async def submit_private_transfer(
recipient: str,
amount: int,
sender_commitment: str,
proof: bytes,
nullifier: str,
new_commitment: str
) -> dictParameters:
recipient- Recipient's Solana address (base58)amount- Transfer amount in lamportssender_commitment- Sender's commitment (hex string)proof- zkSNARK proof bytes (256 bytes for Groth16)nullifier- Nullifier (hex string, 32 bytes)new_commitment- New commitment for recipient (hex string, 32 bytes)
Returns: Response dictionary with:
signature- Transaction signaturestatus- Transaction status ("confirmed","pending","failed")relayer_fee- Fee charged by relayer (in lamports)confirmation_time- Time to confirmation (in seconds)
Raises:
RuntimeError- If relayer submission failsValueError- If invalid parameters
Example:
import asyncio
from veil import VeilClient, RelayerClient
async def transfer_via_relayer():
# Step 1: Generate proof offline
client = VeilClient(rpc_url="https://api.devnet.solana.com")
tx = client.private_transfer(
input_note_secret="my_secret",
input_note_index=42,
recipient_pubkey="RecipientPublicKey...",
amount=500_000_000
)
print(f"✅ Proof generated locally (your IP not exposed)")
print(f"Proof size: {len(tx.proof)} bytes")
# Step 2: Submit via relayer
relayer = RelayerClient(endpoint="https://relayer.veil.network")
response = await relayer.submit_private_transfer(
recipient="RecipientPublicKey...",
amount=500_000_000,
sender_commitment="abc123...",
proof=tx.proof,
nullifier=tx.nullifier,
new_commitment=tx.commitment
)
print(f"\n✅ Submitted via relayer!")
print(f"Transaction: {response['signature']}")
print(f"Relayer fee: {response['relayer_fee'] / 1e9:.4f} SOL")
print(f"Confirmation time: {response['confirmation_time']:.1f}s")
print(f"Status: {response['status']}")
await client.close()
asyncio.run(transfer_via_relayer())Privacy Benefits:
- ✅ Your IP address is NOT visible to Solana RPC nodes
- ✅ Relayer submits from their infrastructure
- ✅ Transaction appears to come from relayer's IP
- ✅ Network observers cannot link your IP to transaction
Security Guarantees:
- ✅ Relayer CANNOT steal funds (you generated the proof)
- ✅ Relayer CANNOT modify transaction (would invalidate zkSNARK)
- ✅ Relayer CANNOT see amounts (encrypted in proof)
- ❌ Relayer CAN see transaction metadata (recipient, commitments)
select_relayer()
Select the best relayer from a list based on fees and performance.
@staticmethod
def select_relayer(
relayers: list[dict]
) -> dictParameters:
relayers- List of relayer info dictionaries with:endpoint- Relayer URLfee_bps- Fee in basis points (e.g., 30 = 0.3%)confirmation_time- Average confirmation time in seconds
Returns: Selected relayer dictionary (best price/performance ratio)
Selection Algorithm:
- Filter out relayers with
fee_bps > 500(5% maximum) - Calculate score:
score = fee_bps + (confirmation_time * 10) - Return relayer with lowest score (best trade-off)
Example:
from veil import RelayerClient
# List of available relayers
relayers = [
{
"endpoint": "https://r1.veil.network",
"fee_bps": 30, # 0.3%
"confirmation_time": 2.5
},
{
"endpoint": "https://r2.veil.network",
"fee_bps": 25, # 0.25%
"confirmation_time": 3.0
},
{
"endpoint": "https://r3.veil.network",
"fee_bps": 35, # 0.35%
"confirmation_time": 1.8
}
]
# Select best relayer
best = RelayerClient.select_relayer(relayers)
print(f"Selected: {best['endpoint']}")
print(f"Fee: {best['fee_bps']} bps ({best['fee_bps'] / 100}%)")
print(f"Confirmation time: {best['confirmation_time']}s")
# Use selected relayer
relayer = RelayerClient(endpoint=best['endpoint'])Output:
Selected: https://r3.veil.network
Fee: 35 bps (0.35%)
Confirmation time: 1.8s
Reasoning: Relayer 3 has the lowest score (35 + 1.8*10 = 53) despite higher fees, because it's much faster.
Strategy: Lowest Fee
Prioritize the cheapest relayer.
def select_lowest_fee(relayers: list[dict]) -> dict:
"""Select relayer with lowest fee."""
return min(relayers, key=lambda r: r['fee_bps'])
# Example
cheapest = select_lowest_fee(relayers)
relayer = RelayerClient(endpoint=cheapest['endpoint'])Use when:
- Cost is the primary concern
- Time is not critical
- You're willing to wait longer for confirmations
Strategy: Fastest
Prioritize the fastest relayer.
def select_fastest(relayers: list[dict]) -> dict:
"""Select relayer with fastest confirmation time."""
return min(relayers, key=lambda r: r['confirmation_time'])
# Example
fastest = select_fastest(relayers)
relayer = RelayerClient(endpoint=fastest['endpoint'])Use when:
- Speed is critical
- You're willing to pay higher fees
- Time-sensitive operations (arbitrage, MEV protection)
Strategy: Balanced
Balance between cost and speed (default in select_relayer()).
def select_balanced(relayers: list[dict]) -> dict:
"""Select relayer with best cost/speed trade-off."""
return min(
relayers,
key=lambda r: r['fee_bps'] + (r['confirmation_time'] * 10)
)
# Example
balanced = select_balanced(relayers)
relayer = RelayerClient(endpoint=balanced['endpoint'])Use when:
- You want a good middle ground
- Both cost and speed matter
- General-purpose transactions
Strategy: Random
Select a random relayer for maximum privacy.
import random
def select_random(relayers: list[dict]) -> dict:
"""Select random relayer for privacy."""
return random.choice(relayers)
# Example
random_relayer = select_random(relayers)
relayer = RelayerClient(endpoint=random_relayer['endpoint'])Use when:
- Maximum privacy (prevents relayer correlation)
- You don't trust any single relayer
- You want to distribute load across relayers
Full example with relayer discovery, selection, and submission with fallback.
import asyncio
from veil import VeilClient, RelayerClient
async def transfer_with_relayer_fallback():
"""Transfer via relayer with automatic fallback."""
# Step 1: Generate proof offline
client = VeilClient()
tx = client.private_transfer(
input_note_secret="my_secret",
input_note_index=42,
recipient_pubkey="Recipient...",
amount=500_000_000
)
# Step 2: Define relayer pool
relayers = [
{
"endpoint": "https://r1.veil.network",
"fee_bps": 30,
"confirmation_time": 2.5
},
{
"endpoint": "https://r2.veil.network",
"fee_bps": 25,
"confirmation_time": 3.0
},
{
"endpoint": "https://r3.veil.network",
"fee_bps": 35,
"confirmation_time": 1.8
}
]
# Step 3: Try relayers in order (balanced strategy)
sorted_relayers = sorted(
relayers,
key=lambda r: r['fee_bps'] + (r['confirmation_time'] * 10)
)
last_error = None
for relayer_info in sorted_relayers:
try:
print(f"Trying relayer: {relayer_info['endpoint']}")
relayer = RelayerClient(endpoint=relayer_info['endpoint'])
response = await relayer.submit_private_transfer(
recipient="Recipient...",
amount=500_000_000,
sender_commitment="commitment...",
proof=tx.proof,
nullifier=tx.nullifier,
new_commitment=tx.commitment
)
print(f"✅ Success via {relayer_info['endpoint']}")
print(f"Transaction: {response['signature']}")
print(f"Fee paid: {response['relayer_fee'] / 1e9:.4f} SOL")
await client.close()
return response
except Exception as e:
print(f"❌ Failed via {relayer_info['endpoint']}: {e}")
last_error = e
continue
# All relayers failed
await client.close()
raise RuntimeError(f"All relayers failed. Last error: {last_error}")
# Run
asyncio.run(transfer_with_relayer_fallback())Output:
Trying relayer: https://r3.veil.network
✅ Success via https://r3.veil.network
Transaction: 5KqW7...
Fee paid: 0.0018 SOL
Build your own selection logic based on custom criteria.
def select_relayer_advanced(
relayers: list[dict],
max_fee_bps: int = 50,
max_confirmation_time: float = 5.0,
preference: str = "speed" # or "cost"
) -> dict:
"""
Advanced relayer selection with custom criteria.
Args:
relayers: List of relayer info
max_fee_bps: Maximum acceptable fee (default 50 = 0.5%)
max_confirmation_time: Maximum acceptable time (default 5s)
preference: "speed" or "cost"
Returns:
Best relayer matching criteria
"""
# Filter by constraints
filtered = [
r for r in relayers
if r['fee_bps'] <= max_fee_bps
and r['confirmation_time'] <= max_confirmation_time
]
if not filtered:
raise ValueError("No relayers match criteria")
# Select based on preference
if preference == "speed":
return min(filtered, key=lambda r: r['confirmation_time'])
elif preference == "cost":
return min(filtered, key=lambda r: r['fee_bps'])
else:
# Balanced
return min(
filtered,
key=lambda r: r['fee_bps'] + (r['confirmation_time'] * 10)
)
# Example usage
relayer = select_relayer_advanced(
relayers,
max_fee_bps=40, # No more than 0.4%
max_confirmation_time=3.0, # No more than 3 seconds
preference="speed"
)Trust Model
What relayers CAN do:
- ✅ See transaction metadata (recipient, commitments, nullifiers)
- ✅ Choose to submit or not submit transactions (censorship)
- ✅ Delay submission (but not indefinitely)
- ✅ Charge fees (within 0-5% range)
What relayers CANNOT do:
- ❌ Steal your funds (proof is cryptographically bound to you)
- ❌ Modify transaction details (would invalidate zkSNARK)
- ❌ See amounts (hidden in commitments)
- ❌ Link sender to recipient (unlinkable via zero-knowledge)
- ❌ Determine which commitment you're spending (Merkle privacy)
Best Practices
For Maximum Privacy:
# 1. Connect via Tor/VPN when contacting relayer
# This hides your IP from the relayer itself
# 2. Use random relayer selection
relayer = select_random(relayers)
# 3. Rotate relayers for each transaction
# Don't use the same relayer repeatedly
# 4. Self-host relayers if possible
# Complete control, but requires infrastructureFor Maximum Reliability:
# 1. Use multiple relayers with fallback
# As shown in the multi-relayer example above
# 2. Monitor relayer health
# Check confirmation_time and fee_bps regularly
# 3. Set reasonable timeouts
# Don't wait indefinitely for slow relayersFor Minimum Cost:
# 1. Use lowest_fee strategy
relayer = select_lowest_fee(relayers)
# 2. Batch transactions if possible
# Reduce per-transaction overhead
# 3. Monitor fee market
# Switch relayers if fees spikeStatus: Infrastructure ready, public deployment guide coming in v0.2.0
Requirements
- Solana RPC access (dedicated node recommended)
- Rust 1.70+
- PostgreSQL (for transaction tracking)
- Domain + HTTPS certificate
Quick Start
# Clone relayer implementation
git clone https://github.com/veil-solana/veil-relayer
cd veil-relayer
# Configure
cp .env.example .env
# Edit .env with your RPC URL, database, etc.
# Build
cargo build --release
# Run
./target/release/veil-relayer --port 8080Relayer endpoints:
POST /submit- Submit transactionGET /fee_estimate- Estimate feesGET /health- Health check
Configuration:
[relayer]
rpc_url = "https://api.mainnet-beta.solana.com"
fee_bps = 30 # 0.3%
max_fee_bps = 500 # 5%
min_withdrawal = 10000 # 10k lamports
[database]
url = "postgresql://user:pass@localhost/veil_relayer"Fee Calculation
def calculate_relayer_fee(amount: int, fee_bps: int) -> int:
"""
Calculate relayer fee in lamports.
Args:
amount: Transaction amount in lamports
fee_bps: Fee in basis points (30 = 0.3%)
Returns:
Fee in lamports
"""
return (amount * fee_bps) // 10000
# Examples
print(calculate_relayer_fee(1_000_000_000, 30)) # 3,000,000 (0.003 SOL)
print(calculate_relayer_fee(100_000_000, 50)) # 500,000 (0.0005 SOL)Fee Breakdown (1 SOL Transfer)
| Fee Type | Amount | Percentage |
|---|---|---|
| Network fee | ~0.0002 SOL | 0.02% |
| Relayer fee (30 bps) | ~0.003 SOL | 0.3% |
| Total | ~0.0032 SOL | 0.32% |
Comparison:
- Direct submission: 0.0002 SOL (network only)
- Via relayer: 0.0032 SOL (network + relayer)
- Privacy premium: 0.003 SOL (~100/SOL)
Error: "All relayers failed"
Cause: All relayers rejected or timed out
Solutions:
- Check relayer health endpoints (
GET /health) - Verify network connectivity
- Try different relayers
- Fall back to direct submission (no IP privacy)
Error: "Fee too high"
Cause: Relayer fee exceeds maximum
Solutions:
- Select different relayer with lower fees
- Increase your
max_fee_bpstolerance - Wait for fee market to stabilize
Error: "Transaction expired"
Cause: Relayer took too long to submit
Solutions:
- Use faster relayer (check
confirmation_time) - Set shorter timeout
- Regenerate proof with fresh Merkle root
| Operation | Time | Notes |
|---|---|---|
| estimate_fee() | <10ms | Local calculation |
| select_relayer() | <10ms | Local comparison |
| submit_private_transfer() | 3-5s | Network + confirmation |
Network overhead:
- HTTP request: ~100-200ms
- Relayer processing: ~500ms
- Blockchain confirmation: ~2-3s
- Total: ~3-5 seconds (vs ~2-3s direct)
Privacy-performance trade-off: +1-2 seconds for IP privacy
- VeilClient - Main SDK client
- Relayers Concept - Deep dive into relayer architecture
- Privacy Model - Privacy guarantees and limitations
- Quick Start - Example 4: Using Relayers
On This Page
- RelayerClient
- Why Use Relayers?
- Constructor
- Fee Estimation
- estimate_fee()
- Transaction Submission
- submit_private_transfer()
- Relayer Selection
- select_relayer()
- Relayer Selection Strategies
- Strategy: Lowest Fee
- Strategy: Fastest
- Strategy: Balanced
- Strategy: Random
- Complete Example: Multi-Relayer Setup
- Advanced: Custom Relayer Selection
- Security Considerations
- Trust Model
- Best Practices
- Self-Hosting a Relayer
- Requirements
- Quick Start
- Fee Economics
- Fee Calculation
- Fee Breakdown (1 SOL Transfer)
- Troubleshooting
- Error: "All relayers failed"
- Error: "Fee too high"
- Error: "Transaction expired"
- Performance Characteristics
- See Also