Skip to main content

Overview

Profiles enable multi-tenancy in Lasso by providing isolated routing configurations with independent chains, providers, rate limits, and metrics. Each (profile, chain) pair runs in an isolated supervision tree while sharing provider infrastructure for efficiency.

Profile Structure

Profiles are YAML files in config/profiles/ with frontmatter metadata:
---
name: Lasso Public
slug: default
rps_limit: 100
burst_limit: 500
---

chains:
  ethereum:
    chain_id: 1
    monitoring:
      probe_interval_ms: 12000
    providers:
      - id: "ethereum_llamarpc"
        url: "https://eth.llamarpc.com"
        ws_url: "wss://eth.llamarpc.com"
        archival: true
        priority: 1

Frontmatter Fields

name (required)
  • Display name for the profile
  • Used in dashboard and logs
slug (required)
  • URL-safe identifier
  • Used in API routes: /rpc/profile/:slug/:chain
  • Must be unique across all profiles
rps_limit (optional)
  • Requests per second limit
  • Default: 100
burst_limit (optional)
  • Maximum burst size for rate limiting
  • Default: 500

Profile-Scoped Configuration

Each profile can configure:

Chains

Multiple blockchain networks with independent provider sets:
chains:
  ethereum:
    chain_id: 1
    name: "Ethereum Mainnet"
    block_time_ms: 12000
    providers: [...]
  
  arbitrum:
    chain_id: 42161
    name: "Arbitrum One"
    block_time_ms: 250
    providers: [...]

Providers

Provider configuration per chain:
providers:
  - id: "ethereum_drpc"
    name: "dRPC Ethereum"
    priority: 2
    url: "https://eth.drpc.org"
    ws_url: "wss://eth.drpc.org"
    archival: true
    subscribe_new_heads: true
    capabilities:
      limits:
        max_block_range: 10000
      error_rules:
        - code: 30
          message_contains: "timeout on the free tier"
          category: rate_limit
Provider Fields:
FieldTypeRequiredDescription
idstringYesUnique identifier within profile
namestringNoDisplay name
priorityintegerNoPriority for priority strategy (lower = higher priority)
urlstringConditionalHTTP endpoint (required for HTTP transport)
ws_urlstringNoWebSocket endpoint
archivalbooleanNoWhether provider has archival data (default: false)
subscribe_new_headsbooleanNoSubscribe to newHeads for block tracking (default: false)
capabilitiesobjectNoMethod support and error classification

Environment Variables

Provider URLs support ${ENV_VAR} substitution:
providers:
  - id: "alchemy"
    url: "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
Unresolved environment variables (literal ${VAR_NAME} in URLs) will crash at startup. This prevents silent failures.

Monitoring Configuration

Per-chain health monitoring settings:
monitoring:
  probe_interval_ms: 12000  # Health check frequency
  lag_alert_threshold_blocks: 5  # Log warning when provider lags this many blocks

Selection Configuration

Provider eligibility filtering:
selection:
  max_lag_blocks: 1  # Exclude providers lagging more than this
  archival_threshold: 128  # Blocks before considering data "archival"

WebSocket Configuration

Subscription management and failover:
websocket:
  subscribe_new_heads: true  # Subscribe to newHeads for real-time tracking
  new_heads_timeout_ms: 35000  # Timeout before marking subscription stale
  failover:
    max_backfill_blocks: 100  # Max blocks to fetch during failover
    backfill_timeout_ms: 30000  # Timeout for backfill requests

Profile Isolation

Each (profile, chain) pair runs in an isolated supervision tree:
ProfileChainSupervisor
└── ChainSupervisor {profile, chain}
    ├── TransportRegistry (transport channel management)
    ├── ClientSubscriptionRegistry (WebSocket fan-out)
    ├── UpstreamSubscriptionPool (subscription multiplexing)
    └── StreamSupervisor (per-subscription continuity)

Isolated Resources

Per (profile, chain):
  • Independent circuit breaker state
  • Isolated metrics and benchmarking
  • Separate rate limits
  • Dedicated WebSocket subscriptions
  • Independent routing decisions

Shared Resources

To optimize resource usage, provider infrastructure is shared across profiles: Shared across profiles (keyed by instance_id):
  • Provider instances (same URL + chain = same instance)
  • Circuit breakers (HTTP and WebSocket)
  • WebSocket connections
  • Block height tracking
  • Health probes
Instance ID derivation:
instance_id = :crypto.hash(:sha256, "#{chain}:#{url}:#{auth_hash}") |> Base.encode16(case: :lower)
Two profiles using https://eth.llamarpc.com for Ethereum will share:
  • The same WebSocket connection
  • The same circuit breakers
  • The same block height data
But maintain separate:
  • Routing policies
  • Metrics aggregation
  • Rate limits

URL Routing

Profiles are accessed via URL paths:

Profile-Specific Routes

# Basic routing
POST /rpc/profile/:profile/:chain

# Strategy selection
POST /rpc/profile/:profile/fastest/:chain
POST /rpc/profile/:profile/load-balanced/:chain
POST /rpc/profile/:profile/latency-weighted/:chain

# Provider override
POST /rpc/profile/:profile/provider/:provider_id/:chain

Default Profile Routes

Routes without /profile/:profile use the “default” profile:
# Uses "default" profile
POST /rpc/:chain
POST /rpc/fastest/:chain
POST /rpc/provider/:provider_id/:chain
The “default” profile must exist in config/profiles/default.yml. Lasso validates this at startup.

Profile Loading

Profiles are loaded at application startup:
# Load all profiles from config/profiles/*.yml
{:ok, profile_slugs} = Lasso.Config.ConfigStore.load_all_profiles()

# Build provider catalog (maps profiles to shared instances)
Lasso.Providers.Catalog.build_from_config()

# Start shared infrastructure
start_shared_infrastructure()

# Start chain supervisors per (profile, chain)
start_all_chains()

Configuration Backend

File Backend (default):
  • Loads from config/profiles/*.yml
  • Hot-reload not supported (requires restart)
Database Backend (SaaS extension, not in OSS):
  • Dynamic profile management
  • Hot-reload support
  • Multi-tenant isolation

Example Profiles

Production Profile

High rate limits, premium providers:
---
name: Production API
slug: production
rps_limit: 1000
burst_limit: 5000
---

chains:
  ethereum:
    chain_id: 1
    providers:
      - id: "alchemy_premium"
        url: "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
        priority: 1
        archival: true
      - id: "quicknode_premium"
        url: "https://api.quicknode.com/${QUICKNODE_KEY}"
        priority: 2
        archival: true

Development Profile

Lower rate limits, free public providers:
---
name: Development
slug: dev
rps_limit: 50
burst_limit: 200
---

chains:
  ethereum-sepolia:
    chain_id: 11155111
    providers:
      - id: "sepolia_drpc"
        url: "https://sepolia.drpc.org"
        priority: 1
      - id: "sepolia_publicnode"
        url: "https://ethereum-sepolia-rpc.publicnode.com"
        priority: 2

Analytics Profile

Archival data, specific method routing:
---
name: Analytics
slug: analytics
rps_limit: 100
burst_limit: 500
---

chains:
  ethereum:
    chain_id: 1
    routing:
      default_strategy: "fastest"
      method_overrides:
        eth_getLogs:
          strategy: "fastest"
          providers: ["quicknode_archival", "alchemy_archival"]
    providers:
      - id: "quicknode_archival"
        url: "https://api.quicknode.com/${QUICKNODE_KEY}"
        archival: true
      - id: "alchemy_archival"
        url: "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
        archival: true

Provider Capabilities

Profiles can declare provider capabilities for method filtering and error classification:
capabilities:
  # Method filtering
  unsupported_categories: [debug, trace, txpool, filters]
  unsupported_methods: ["eth_protocolVersion"]
  
  # Parameter limits
  limits:
    max_block_range: 10000  # For eth_getLogs
    max_block_age: 1000  # For state methods
    block_age_methods: [eth_call, eth_getBalance]
  
  # Error classification
  error_rules:
    - code: 30
      message_contains: "timeout on the free tier"
      category: rate_limit
    - code: -32701
      category: capability_violation
Capability Features: Method Filtering:
  • unsupported_categories: Block entire RPC method categories
  • unsupported_methods: Block specific method names
Parameter Validation:
  • max_block_range: Maximum block range for eth_getLogs
  • max_block_age: Maximum block age for state queries
  • block_age_methods: Methods subject to max_block_age
Error Classification:
  • Per-provider error rules evaluated top-to-bottom
  • First match wins
  • Affects retry behavior and circuit breaker penalties
See Provider Selection for how capabilities affect the filter pipeline.

Profile Validation

Lasso validates profiles at startup:

Required Validations

Default Profile Exists:
case ProfileValidator.validate("default") do
  {:ok, _} -> :ok
  {:error, _type, message} -> raise "Default profile validation failed: #{message}"
end
Environment Variables Resolved:
case ChainConfig.validate_no_unresolved_placeholders(chain_config) do
  :ok -> :ok
  {:error, {:unresolved_env_vars, providers}} -> raise "Unresolved env vars"
end
Chain Configuration Valid:
case ChainConfig.validate_chain_config(chain_config) do
  :ok -> :ok
  {:error, reason} -> Logger.warning("Chain validation failed: #{inspect(reason)}")
end

Profile Status

Get profile information at runtime:
# List all profiles
profiles = Lasso.Config.ConfigStore.list_profiles()
# => ["default", "production", "dev"]

# Get profile metadata
{:ok, meta} = ConfigStore.get_profile_meta("default")
# => %{name: "Lasso Public", slug: "default", rps_limit: 100, burst_limit: 500}

# Get profile chains
{:ok, chains} = ConfigStore.get_profile_chains("default")
# => %{"ethereum" => %{chain_id: 1, ...}, "arbitrum" => %{chain_id: 42161, ...}}

# Get chain status
status = Lasso.RPC.ChainSupervisor.get_chain_status("default", "ethereum")
# => %{chain_name: "ethereum", total_providers: 15, healthy_providers: 13, ...}

Profile Lifecycle

Startup

  1. ConfigStore.load_all_profiles() loads YAML files
  2. Catalog.build_from_config() creates provider instance mappings
  3. start_shared_infrastructure() starts instance supervisors and probes
  4. start_all_chains() starts (profile, chain) supervisors

Runtime

Profiles are read-only at runtime. Configuration changes require application restart.

Shutdown

Graceful shutdown drains in-flight requests:
{Plug.Cowboy.Drainer, refs: [LassoWeb.Endpoint.HTTP], shutdown: 30_000}
  • 30-second grace period for in-flight requests
  • Circuit breaker state preserved in ETS (survives GenServer restarts)
  • Benchmark metrics persisted to disk

Next Steps

Routing Strategies

Configure provider selection strategies

Provider Selection

Understand the 7-stage filter pipeline

Circuit Breakers

Configure fault tolerance thresholds

Architecture

Explore the OTP supervision tree