Docs
/api
/RelayerClient

RelayerClient

IP privacy via relayer network

RelayerClient

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.

Why Use Relayers?

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

Constructor

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")
]

Fee Estimation

estimate_fee()

Estimate the relayer fee for a transaction.

def estimate_fee(
    operation: str,
    amount: int
) -> int

Parameters:

  • 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 SOL

Note: Actual fees may vary based on:

  • Network congestion
  • Relayer-specific pricing
  • Operation complexity

Transaction Submission

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
) -> dict

Parameters:

  • recipient - Recipient's Solana address (base58)
  • amount - Transfer amount in lamports
  • sender_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 signature
  • status - 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 fails
  • ValueError - 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)

Relayer Selection

select_relayer()

Select the best relayer from a list based on fees and performance.

@staticmethod
def select_relayer(
    relayers: list[dict]
) -> dict

Parameters:

  • relayers - List of relayer info dictionaries with:
    • endpoint - Relayer URL
    • fee_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:

  1. Filter out relayers with fee_bps > 500 (5% maximum)
  2. Calculate score: score = fee_bps + (confirmation_time * 10)
  3. 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.


Relayer Selection Strategies

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

Complete Example: Multi-Relayer Setup

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

Advanced: Custom Relayer Selection

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"
)

Security Considerations

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 infrastructure

For 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 relayers

For 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 spike

Self-Hosting a Relayer

Status: 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 8080

Relayer endpoints:

  • POST /submit - Submit transaction
  • GET /fee_estimate - Estimate fees
  • GET /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 Economics

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 TypeAmountPercentage
Network fee~0.0002 SOL0.02%
Relayer fee (30 bps)~0.003 SOL0.3%
Total~0.0032 SOL0.32%

Comparison:

  • Direct submission: 0.0002 SOL (network only)
  • Via relayer: 0.0032 SOL (network + relayer)
  • Privacy premium: 0.003 SOL (~0.30at0.30 at 100/SOL)

Troubleshooting

Error: "All relayers failed"

Cause: All relayers rejected or timed out

Solutions:

  1. Check relayer health endpoints (GET /health)
  2. Verify network connectivity
  3. Try different relayers
  4. Fall back to direct submission (no IP privacy)

Error: "Fee too high"

Cause: Relayer fee exceeds maximum

Solutions:

  1. Select different relayer with lower fees
  2. Increase your max_fee_bps tolerance
  3. Wait for fee market to stabilize

Error: "Transaction expired"

Cause: Relayer took too long to submit

Solutions:

  1. Use faster relayer (check confirmation_time)
  2. Set shorter timeout
  3. Regenerate proof with fresh Merkle root

Performance Characteristics

OperationTimeNotes
estimate_fee()<10msLocal calculation
select_relayer()<10msLocal comparison
submit_private_transfer()3-5sNetwork + 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


See Also